装饰器类和装饰器函数的区别

13

我猜那就是它们的称呼,但为了谨慎起见,我会举例说明。

装饰器类:

class decorator(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print 'something'
        self.func(*args, **kwargs)

装饰器函数:

def decorator(func):
    def wrapper(*args, **kwargs):
        print 'something'
        return func(*args, **kwargs)
    return wrapper

使用其中一种与另一种是否仅仅是个人口味问题?有没有实际的区别?


3
有一个区别。当你想要使用普通函数和使用普通类时,区别正好相同。 - Falmarri
3个回答

14
如果您可以编写一个函数来实现您的装饰器,那么您应该选择这种方式。但并非所有的装饰器都能轻松地编写成函数形式 - 例如当您想要存储一些内部状态时。
class counted(object):
    """ counts how often a function is called """
    def __init__(self, func):
        self.func = func
        self.counter = 0

    def __call__(self, *args, **kwargs):
        self.counter += 1
        return self.func(*args, **kwargs)


@counted
def something():
    pass

something()
print something.counter

我见过人们(包括我自己)为了只使用函数编写装饰器而做出荒谬的努力。我仍然不知道为什么,因为类的开销通常是可以忽略不计的。


4
当然,这可以写成一个函数。但在Python中,通常更喜欢使用类来隐藏状态,而不是使用闭包。 - Sven Marnach
2
我其实很喜欢这个Python(3.x)版本的代码(使用大括号*哭泣*因为注释会吃掉换行符):def counted(f) { x = 0; def wrapper(*args, **kwargs) { nonlocal x; x += 1; f(*args, **kwargs) } return wrapper }。然而2.x版本就相当丑陋了(通过使用单例列表并使用其单个项目来解决缺少nonlocal的问题)。 - user395760
1
你也可以使用包装函数的属性来保存计数器。 - kindall
1
@delnan:是的,nonlocal非常有用,但在Py2中,您也可以在装饰器中编写def wrapped(...):wrapped.counter +=1;...(函数也是可变的)。我真的想不出一个好的例子来回答我的问题;只需想象一些更复杂的东西就可以了;-) - Jochen Ritzel

3
通常这只是一个品味问题。大多数Python程序使用鸭子类型,不关心它们调用的是函数还是其他类型的实例,只要它是可调用的。而任何具有__call__()方法的对象都是可调用的。
使用函数式装饰器有一些优点:
  • 当您的装饰器不返回包装函数(即在对其进行某些操作后返回原始函数,例如设置属性)时,更加简洁。

  • 无需显式保存对原始函数的引用,因为闭包会处理这个问题。

  • 大多数帮助您制作装饰器的工具,例如functools.wraps()或Michele Simionato的保留签名的decorator模块,都适用于函数式装饰器。

  • 可能有一些程序在某处并不使用鸭子类型,而实际上期望函数类型,因此返回函数来替换函数在理论上更“安全”。

出于这些原因,我大多数情况下都使用函数式装饰器。然而,作为反例,这里是一个最近我自己更自然地使用类式装饰器的实例。

2
functools.wraps() 可能旨在与函数式装饰器一起使用,但是在类式装饰器的 __init__ 中使用 functools.update_wrapper(self, wrapped) 也可以完美地工作。 - Peter Milley

2
提议的类装饰器实现与函数实现略有不同:它将无法在方法上运行。
class Decorator(object):
def __init__(self, func):
    self.func = func

def __call__(self, *args, **kwargs):
    print('something')
    self.func(*args, **kwargs)

class A:
    @Decorator
    def mymethod(self):
        print("method")

A().mymethod()

将会引发 TypeError: mymethod() 缺少 1 个必需的位置参数: 'self'

要添加方法支持,您需要实现 __get__

import types
class Decorator2(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('something')
        self.func(*args, **kwargs)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return types.MethodType(self, instance)

class B:

    @Decorator2
    def mymethod(self):
        print("method")

B().mymethod()

将输出

class B:...

something
method

这段代码之所以能够运行,是因为当你访问 B().mymethod 时,首先调用了 __get__ 并提供绑定的方法。然后再调用 __call__
总之,只要定义了 __get__,类和函数实现就可以以相同的方式使用。有关更多信息,请参见 Python Cookbook 中的第 9.9 节。

这个问题似乎从未得到解决。显然在Python食谱中有提到,但是所有讨论装饰器作为类的“Python教程”页面都没有考虑它在方法中的使用。看起来这是一个明显的陷阱,最终只需要一个简单的解决方案。感谢您!希望我能给您超过1个赞! - saquintes

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