切换装饰器

17 投票
8 回答
9555 浏览
提问于 2025-04-17 14:22

有没有什么好的方法可以方便地开启和关闭装饰器,而不需要每次都去每个装饰器那里注释掉?比如你有一个用于性能测试的装饰器:

# 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 个回答

2

我觉得你应该用一个装饰器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
5

我一直在用以下的方法。这种方法和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。

11

你可以把条件直接加到装饰器里面:

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 里使用它,它就不会生效。

撰写回答