类装饰器与函数装饰器

29

在Python中,声明装饰器有两种方式:

基于类的方式

class mydecorator(object):
    def __init__(self, f):
        self.f = f

    def __call__(self, *k, **kw):
        # before f actions
        self.f(*k, **kw)
        # after f actions

基于函数的

def mydecorator(f):
    def decorator(*k, **kw):
        # before f actions
        f(*k, **kw)
        # after f actions

    return decorator          

这些声明之间有什么区别吗?在哪些情况下应该使用它们?


1
通过类,您可以利用描述符协议! - phant0m
@phant0m:缺点是,使用类时必须使用描述符协议(否则您的装饰器无法完全在方法上工作)。 - Rosh Oxymoron
@Rosh:嗯,是的,你说得很有道理 :) - phant0m
5个回答

21

当您创建一个返回另一个可调用对象的可调用对象时,使用函数方法更容易且更便宜。有两个主要区别:

  1. 函数方法可以自动处理方法,而如果您使用类方法,您需要了解描述符并定义一个__get__方法。
  2. 类方法使保留状态变得更加容易。您可以使用闭包,尤其是在Python 3中,但通常更喜欢使用类方法来保留状态。

此外,函数方法允许您在修改或存储后返回原始函数。

然而,装饰器可以返回除可调用对象之外的其他东西,或者比可调用对象更的东西。使用类,您可以:

  1. 向被装饰的可调用对象添加方法和属性,或对它们进行操作(不好)。
  2. 创建描述符,在类中放置时以特殊方式运行(例如classmethodproperty
  3. 使用继承实现类似但不同的装饰器。

如果您有任何疑问,请问自己:您是否希望您的装饰器返回一个表现得与函数完全相同的函数?使用返回函数的函数。您是否希望您的装饰器返回执行与函数不同的更多或其他操作的自定义对象?创建一个类并将其用作装饰器。


+1 惊人的是没有人注意到这个方法问题! - user395760
1
非常完整的答案。虽然来晚了,但这可能应该是被接受的答案。 - Neil G
函数方法自动工作,这是一个重要的区别。谢谢! - Nick Zalutskiy

20

如果你想在装饰器中保留状态,你应该使用一个类。

例如,这样是不起作用的:

def mydecorator(f):
    x = 0 
    def decorator():
        x += 1 # x is a nonlocal name and cant be modified
        return f(x)
    return decorator 

有很多方法可以解决这个问题,但最简单的方法是使用一个类

class mydecorator(object):
    def __init__(self, f):
        self.f = f
        self.x = 0

    def __call__(self, *k, **kw):
        self.x += 1
        return f(self.x)

3
我认为最简单的方法是说“nonlocal x; x += 1”。但这种方法在Python 3以下的版本中是否可行呢?-- 是的,看起来nonlocal是在Python 3中引入的。 - JAB
有关方法装饰器限制的更多信息, 可以在 https://dev59.com/xGw15IYBdhLWcg3wVaQa#6677524 答案中找到。 - Yossi
1
一个被多个模块使用的有状态修饰器会使任何对它的使用取决于其他使用它的模块在程序中的导入顺序,甚至包括完全不相关的模块!所以我的第一条经验法则是,如果你想在装饰器中保留状态,“停下来思考这是否是最佳方法”。当然,有时候它确实是最佳方法,在这种情况下,你应该使用类修饰器。 - Ben

7
事实上,并没有什么“两种方式”。只有一种方法(定义可调用对象),或者像Python中制作可调用对象那样有多种方法(可以是其他对象的方法、lambda表达式的结果、局部对象等任何可调用对象)。
函数定义是制作可调用对象最简单的方法,也是最简单的方法,在大多数情况下可能是最好的选择。使用类可以为您提供更多的可能性,以清晰地编写更复杂的情况(即使在最简单的情况下,它看起来也相当优雅),但并不是很明显它是做什么的。

2

不,有(超过)两种方法可以创建可调用对象。一种是使用def定义一个函数,这显然是可调用的。另一种是在类中定义一个__call__方法,这将使其实例成为可调用的。而类本身也是可调用对象。

装饰器只是一个可调用对象,它旨在接受一个函数作为其唯一参数并返回一个可调用对象。以下语法:

@decorate
def some_function(...):
    ...

只是一种稍微更好的写法:
def some_function(...):
    ...

some_function = decorate(some_function)

你提供的基于类的示例不是一个接受函数并返回函数的标准装饰器,而是一个初始化为可调用实例的函数。如果你没有将其作为类使用(它是否有其他方法?它的状态是否会改变?你是否创建了多个具有类封装的常见行为的实例?),那么这对我来说有点奇怪。但是,对于普通使用你的装饰函数来说,不会有什么区别(除非它是一个特别具有侵入性的装饰器),所以请按照自己感觉更自然的方式进行操作。

1

让我们来测试一下吧!

test_class = """
class mydecorator_class(object):
    def __init__(self, f):
        self.f = f

    def __call__(self, *k, **kw):
        # before f actions
        print 'hi class'
        self.f(*k, **kw)
        print 'goodbye class'
        # after f actions

@mydecorator_class
def cls():
    print 'class'

cls()
"""

test_deco = """
def mydecorator_func(f):
    def decorator(*k, **kw):
        # before f actions
        print 'hi function'
        f(*k, **kw)
        print 'goodbye function'
        # after f actions
    return decorator

@mydecorator_func
def fun():
    print 'func'

fun()
"""

if __name__ == "__main__":
    import timeit
    r = timeit.Timer(test_class).timeit(1000)
    r2 = timeit.Timer(test_deco).timeit(1000)
    print r, r2

我得到了这样的结果:0.0499339103699 0.0824959278107
这意味着装饰类快了两倍吗?

2
有趣,但我猜这取决于具体的实现。在其他Python实现(Jython、PyPy)或甚至不同版本的CPython中,你可能会得到完全不同的结果。 此外,运行'mydecorator_func'和类定义代码的时间可能会有所不同。这只是应用程序的启动时间,通常并不重要。装饰函数调用速度在两种情况下可能是相同的。 - Jacek Konieczny

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接