切换装饰器
有没有什么好的方法可以方便地开启和关闭装饰器,而不需要每次都去每个装饰器那里注释掉?比如你有一个用于性能测试的装饰器:
# deco.py
def benchmark(func):
def decorator():
# fancy benchmarking
return decorator
在你的模块中可能会有这样的代码:
# mymodule.py
from deco import benchmark
class foo(object):
@benchmark
def f():
# code
@benchmark
def g():
# more code
这样做是可以的,但有时候你并不在乎性能测试,也不想增加额外的负担。我之前是这样做的:添加另一个装饰器:
# anothermodule.py
def noop(func):
# do nothing, just return the original function
return func
然后注释掉导入那一行,再添加另一个:
# mymodule.py
#from deco import benchmark
from anothermodule import noop as benchmark
现在性能测试可以按文件来开启或关闭,只需要在相关模块中更改导入语句。每个装饰器都可以独立控制。
有没有更好的方法呢?如果能完全不编辑源文件,而是在其他地方指定哪些文件使用哪些装饰器,那就太好了。
8 个回答
我觉得你应该用一个装饰器a来装饰装饰器b,这样你就可以通过一个决策函数来控制装饰器b的开关。
听起来很复杂,但其实这个想法很简单。
假设你有一个叫做logger的装饰器:
from functools import wraps
def logger(f):
@wraps(f)
def innerdecorator(*args, **kwargs):
print (args, kwargs)
res = f(*args, **kwargs)
print res
return res
return innerdecorator
这个装饰器非常普通,我有十几个这样的装饰器,比如缓存器、记录器、注入东西的装饰器、性能测试等。我可以很容易地用一个if语句来扩展它,但这样做似乎不是个好主意;因为那样我就得修改十几个装饰器,这可真没意思。
那该怎么办呢?我们可以再往上走一步。假设我们有一个装饰器,可以装饰另一个装饰器?这个装饰器看起来是这样的:
@point_cut_decorator(logger)
def my_oddly_behaving_function
这个装饰器接受logger,虽然这并不是个很有趣的事情。但它还有足够的能力来决定是否将logger应用到my_oddly_behaving_function上。我把它叫做point_cut_decorator,因为它有一些面向切面编程的特点。所谓的point cut就是一组位置,在这些位置上,某些代码(建议)需要与执行流程交织在一起。point cut的定义通常在一个地方。这种技术看起来非常相似。
那么我们如何实现这个决策逻辑呢?我选择创建一个函数,它接受被装饰的对象、装饰器、文件和名称,这个函数只能决定是否应用装饰器。这些就是足够精确的坐标,可以准确定位。
这是point_cut_decorator的实现,我选择将决策函数实现为一个简单的函数,你可以扩展它,让它根据你的设置或配置来决定,如果你对所有四个坐标使用正则表达式,你将得到一个非常强大的东西:
from functools import wraps
myselector就是这个决策函数,当返回true时,装饰器会被应用;返回false时则不会应用。参数包括文件名、模块名、被装饰的对象,最后是装饰器。这让我们可以非常细致地控制行为。
def myselector(fname, name, decoratee, decorator):
print fname
if decoratee.__name__ == "test" and fname == "decorated.py" and decorator.__name__ == "logger":
return True
return False
这个装饰器会装饰一个函数,检查myselector,如果myselector说可以继续,它就会将装饰器应用到这个函数上。
def point_cut_decorator(d):
def innerdecorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
if myselector(__file__, __name__, f, d):
ps = d(f)
return ps(*args, **kwargs)
else:
return f(*args, **kwargs)
return wrapper
return innerdecorator
def logger(f):
@wraps(f)
def innerdecorator(*args, **kwargs):
print (args, kwargs)
res = f(*args, **kwargs)
print res
return res
return innerdecorator
这就是你如何使用它:
@point_cut_decorator(logger)
def test(a):
print "hello"
return "world"
test(1)
编辑:
这是我提到的正则表达式方法:
from functools import wraps
import re
正如你所看到的,我可以在某个地方指定一些规则,来决定是否应用装饰器:
rules = [{
"file": "decorated.py",
"module": ".*",
"decoratee": ".*test.*",
"decorator": "logger"
}]
然后我会遍历所有规则,如果某条规则匹配,就返回True;如果不匹配,就返回false。通过在生产环境中将规则设置为空,这样不会太影响你的应用性能:
def myselector(fname, name, decoratee, decorator):
for rule in rules:
file_rule, module_rule, decoratee_rule, decorator_rule = rule["file"], rule["module"], rule["decoratee"], rule["decorator"]
if (
re.match(file_rule, fname)
and re.match(module_rule, name)
and re.match(decoratee_rule, decoratee.__name__)
and re.match(decorator_rule, decorator.__name__)
):
return True
return False
我一直在用以下的方法。这种方法和CaptainMurphy建议的几乎一模一样,但它的好处是你不需要像调用函数那样去使用装饰器。
import functools
class SwitchedDecorator:
def __init__(self, enabled_func):
self._enabled = False
self._enabled_func = enabled_func
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, new_value):
if not isinstance(new_value, bool):
raise ValueError("enabled can only be set to a boolean value")
self._enabled = new_value
def __call__(self, target):
if self._enabled:
return self._enabled_func(target)
return target
def deco_func(target):
"""This is the actual decorator function. It's written just like any other decorator."""
def g(*args,**kwargs):
print("your function has been wrapped")
return target(*args,**kwargs)
functools.update_wrapper(g, target)
return g
# This is where we wrap our decorator in the SwitchedDecorator class.
my_decorator = SwitchedDecorator(deco_func)
# Now my_decorator functions just like the deco_func decorator,
# EXCEPT that we can turn it on and off.
my_decorator.enabled=True
@my_decorator
def example1():
print("example1 function")
# we'll now disable my_decorator. Any subsequent uses will not
# actually decorate the target function.
my_decorator.enabled=False
@my_decorator
def example2():
print("example2 function")
在上面的例子中,example1会被装饰,而example2则不会被装饰。当我需要根据模块来启用或禁用装饰器时,我只需写一个函数,每次需要不同的副本时就创建一个新的SwitchedDecorator。
你可以把条件直接加到装饰器里面:
def use_benchmark(modname):
return modname == "mymodule"
def benchmark(func):
if not use_benchmark(func.__module__):
return func
def decorator():
# fancy benchmarking
return decorator
如果你在 mymodule.py
这个文件里使用这个装饰器,它就会生效;但如果你在 othermodule.py
里使用它,它就不会生效。