前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python 3 之 装饰器详解

Python 3 之 装饰器详解

作者头像
py3study
发布2020-01-03 17:17:59
1K0
发布2020-01-03 17:17:59
举报
文章被收录于专栏:python3python3python3

------------ 装饰器 -----------------------------------------------------

什么是装饰器

装饰器是为函数和类指定管理代码的一种方式。装饰器本身的形式是处理其他的可调用对象的可调用对象(如函数)。正如我们在本书前面所见到过的,Python装饰器以两种相关形式呈现:

  • 函数装饰器在函数定义的时候进行名称重绑定,提供一个逻辑层来管理函数和方法或随后对它们调用。
  • 类装饰器在类定义的时候进行名称重绑定,提供给一个逻辑层来管理类,或管理随后调用它们说创建的实例。

简而言之,装饰器提供了一种方法,在函数和类定义语句的末尾插入自动运行代码——对于函数装饰器,在def的末尾;对于类装饰器,在class的末尾。这样的代码可以扮演不同的角色,参见后面小节介绍。

管理调用和实例

例如,通常的用法中,这种自动运行的代码可能用来增强对函数和类的调用,它通过针对随后的调用安装包装器对象来实现这一点:

  • 函数装饰器安装包装器对象,以在需要的时候拦截随后的函数调用并处理它们。
  • 类装饰器安装包装器对象,以在需要的时候拦截随后的实例创建调用并处理它们。

装饰器通过自动把函数和类名重绑定到其他的可调用对象来实现这些效果,在def和class语句的末尾做到这点当随后调用的时候,这些可调用对象可以执行诸如对函数调用跟踪和计时、管理对类实例属性的访问等任务

管理函数和类

尽管本篇大多数实例都是用包装器来拦截随后对函数和类的调用,但这并非是使用装饰器的唯一方法:

  • 函数装饰器也可以用来管理函数对象,而不是随后对它们的调用……例如,把一个函数注册到一个API。然而,我们在这里主要关注更为常见的用法,即调用包装器应用程序。
  • 类装饰器也可以用来直接管理类对象,而不是实例创建调用——例如。用新的方法扩展类。因为这些用法和元类有很大的重合(实际上,都是在类创建过程的最后运行),我们将在随后看到更多的例子。

换句话说,函数装饰器可以用来管理函数调用和函数对象,类装饰器可以用来管理类实例和类自身。通过返回装饰的对象自身而不是一个包装器,装饰器变成了针对函数和类的一种简单的后创建步骤。

不管扮演什么样的角色,装饰器都提供了一种方便而明确的方法,来编写在程序开发阶段和现实产品系统中都有用的工具。

使用和定义装饰器

根据你的工作形式,你可能成为装饰器的用户或提供者。正如我们所看到的,Python本身带有具有特定角色的内置装饰器……静态方法装饰器、属性装饰器以及更多。此外,很多流行的Python工具包括了执行管理数据库或用户接口逻辑等任务的装饰器。在这样的情况中,我们不需要知道装饰器如何编码就可以完成任务。

对于更为通用的任务,程序员可以编写自己的任意装饰器。例如,函数装饰器可能通过添加跟踪调用、在调试时执行参数验证测试、自动获取和释放线程锁、统计调用函数的次数以进行优化等的代码来扩展函数。你可以想象添加到函数调用中的任何行为,都可以作为定制函数装饰器的备选。

另外一方面,函数装饰器设计用来只增强一个特定函数或方法调用,而不是一个完整的对象接口类装饰器更好地充当后一种角色----因为它们可以拦截实例创建调用,它们可以用来实现任意的对象接口扩展或管理任务。例如,定制的类装饰器可以跟踪或验证对一个对象的每个属性引用。它们也可以用来实现代理对象、单体类 以及其他常用的变成模式。 实际上,我们将会发现很多类装饰器与前面见到的委托编程模式有很大的相似之处。

为什么使用装饰器

像很多高级Python工具一样,从纯技术的视角来看,并不是严格需要装饰器:它们的功能往往可以使用简单的辅助函数调用或其他的技术来实现(并且从基本的层面触发,我们总是可以手动地编写装饰器说自动执行的名称重绑定)。

也就是说,装饰器为这样的任务提供了一种显式的方法,它使得意图明确,可以最小化扩展代码的冗余,并且有助于确保正确的API使用:

  • 装饰器有一种非常明确的语法,这使得它们比那些可能任意地远离主体函数或类的辅助函数调用更容易为人们发现。
  • 当主体函数或类定义的时候,装饰器应用一次;在对类或函数的每次调用的时候,不必添加额外的代码(在未来可能必须改变)。
  • 由于前面两点,装饰器使得一个API的用户不太可能忘记根据API需求扩展一个函数或类。

换句话说,除了其技术模型之外,装饰器提供了一些和代码维护性和审美相关的优点。此外,作为结构化工具,装饰器自然地促进了代码的封装,这减少了冗余性并使得未来变得更容易。

装饰器确实也有一些潜在的缺点……当它们插入包装类的逻辑,它们可以修改装饰的对象的类型,并且它们可能引发额外的调用。 另外一方面,同样的考虑也适用于任何为对象包装逻辑的技术。

我们将在本篇随后的真实代码中说明这些权衡。 尽管选择使用装饰器仍然多少有些主观性,但它们的优点引人注目,足以使其快速成为Python世界中的最佳实践。为了帮助你做出决定,让我们来看一些细节。

基础知识

让我们首先从一个符号的角度来第一次看一看装饰行为。我们很快将编写真正的代码,但是,由于装饰器的很多神奇之处可归结为自动重绑定操作,所以首先理解这一映射是很重要的。

函数装饰器

函数装饰器已经从Python 2.5开始可用。正如我们在本书前面所听见到的,它们主要是一种语法糖:通过在一个函数的def语句的末尾来运行另一个函数,把最初的函数名重新绑定到新结果

用法

函数装饰器是一种关于函数的运行时声明,函数的定义需要遵守此声明。装饰器在紧挨着定义一个函数或方法的def语句之前的一行编写,并且它由@符号以及紧随其后的对于元函数的一个引用组成……这是管理另一个函数的一个函数(或其他的可调用对象)。

在编码方面,函数装饰器自动将如下的语法

@decorator        # 装饰函数
def F(arg):
    ...
    
F(99)

映射为如下对等的形式,其中装饰器是一个单参数的可调用对象,它返回与F具有相同数目的参数的一个可调用对象:

def F(arg):
    ...
F = decorator(F)        # 等价语法
F(99)

这一自动名称重绑定在def语句上有效,不管它针对一个简单的函数或是类中的一个方法。当随后调用F函数的时候,它自动调用装饰器所返回的对象,该对象可能是实现了所需的包装逻辑的另一个对象,或者是最初的函数本身。

换句话说,装饰实际把如下的第一行映射为第二行(尽管装饰器实际上只运行一次,在装饰的时候):

func(6, 7)
decorator(func)(6, 7)

这一自动名称重绑定说明了我们在前面遇到的静态方法和正确的装饰语法的原因:

class C:
    @staticmethod
    def meth(...): ...
    
class C:
    @property
    def name(self): ...

在这两个例子中,在def语句的末尾,方法名重新绑定到一个内置函数装饰器的结果。随后再调用最初的名称,将会调用装饰器所返回的对象。

实现

装饰器自身是一个返回可调用对象的可调用对象。 也就是说,它返回了一个对象,当随后装饰的函数通过其最初的名称调用的时候,将会调用这个对象----不管是拦截了随后调用的包装器对象,还是最初的函数以某种方式扩展。实际上,装饰器可以是任意类型的可调用对象,并且返回任意类型的可调用对象:函数和类的任何组合都可以使用,尽管一些组合更适合于特定的背景。

例如,要在一个函数创建之后接入装饰协议以管理函数,我们需要编写如下形式的装饰器:

def decorator(F):
    # Process function F
    return F
    
@decorator
def func(): ...        # 等价在函数后添加 func = decorator(func)

由于最初的装饰函数分配回给其名称,这么做将直接向函数的定义添加创建之后的步骤。这样的一个结构可能会用来把一个函数注册到一个API、赋值函数属性,等等。

更典型的用法,是插入逻辑以拦截对函数的随后调用,我们可以编写一个装饰器来返回和最初函数不同的一个对象:

def decorator(F):
    # Save or use function F
    # Return a different callable: nested def, class with __call__,etc.
    
@decorator
def func(): ...        # func = decorator(func)

这个装饰器在装饰的时候调用,并且在随后调用最初的函数名的时候,它所返回的调用对象将被调用。装饰器自身接受被装饰的函数,返回的调用对象会接受随后传递给被装饰函数的名称的任何参数。这和类方法的工作方式相同:隐含的实例对象只是在返回可调用对象的第一个参数中出现。

更概括的说,有一种常用的编码模式可以包含这一思想----装饰器返回了一个包装器,包装器把最初的函数保持到一个封闭的作用域中:

def decorator(F):              # On @ decoration
    def wrapper(*args):        # On wrapped function call
        # Use F and args
        # F(*args) calls original function 
    return wrapper
    
@decorator                    # func = decorator(func)
def func(x, y):                # func对象传递到了decorator函数的F参数中
    ...
    
func(6, 7)                    # 参数(6, 7)传递给了wrapper函数

当随后调用名称func的时候,它却是调用装饰器所返回的包装器函数;随后包装器函数可能会运行最初的func,因为它在一个封闭的作用域中仍然可以使用(有些也称之为“闭包”)。当以这种方式编码的时候,每个装饰的函数都会产生一个新的作用域来保持状态。

为了对类做同样的事情,我们可以重载调用操作,并且使用实例属性而不是封闭的作用域:

class decorator:
    def __init__(self, func):            # On @ decoration
        self.func = func
    def __call__(self, *args):            # On wrapped function call
        # Use self.func and args
        # self.func(*args) call original function
        
@decorator
def func(x, y):                   # func = decorator(func)
    ...                      # func对象已经被传递到了__init__
    
func(6, 7)                      #(6, 7)参数被传递到了__call__(*args)当中

现在,随后再调用func的时候,它确实会调用装饰器所创建的实例__call__运算符重载方法; 然后,__call__方法可能运行最初的func,因为它在一个实例属性中仍然可用。 当按照这种方式编写代码的时候,每个装饰的函数都会产生一个新的实例来保持状态。

支持方法装饰

关于前面的基于类的代码的细微的一点是,尽管它对于拦截简单函数调用有效,但当它应用于类方法函数的时候,并不是很有效:

class decorator:
    def __init__(self, func):        # func is method whithout instance
        self.func = func
    def __call__(self, *args):        # self is decorator instance
        # self.func(*args) fails!    # C instance not in args!
        
class C:
    @decorator
    def method(self, x, y):            # method = decorator(method)
        ...                    # 绑定到了装饰器类的一个实例上

当按照这种方式编码的时候,装饰的方法重绑定到装饰器类的一个实例,而不是一个简单的函数

这一点带来的问题是,当装饰器的__call__方法随后运行的时候,其中的self接收装饰器类实例,并且类C的实例不会包含有一个*args。这使得有可能吧调用分派给最初的方法……即保持了最初的方法的装饰器对象,但是没有传递给它。

为了支持函数和方法,嵌套函数的替代方法工作得更好:

def decorator(F):                # F is func or method without instance
    def wrapper(*args):            # class instance in args[0] for method
        # F(*args) runs func or method
    return wrapper
    
@decorator
def func(x, y):                    # func = decorator(func)
    ...
func(6, 7)                       # 相当于wrapper(6, 7)

class C:
    @decorator
    def method(self, x, y)        # method = decorator(method)
        ...                   # 重新绑定函数
        
X = C()
x.method(6, 7)                 # 相当于wrapper(X, 6, 7)

当按照这种方法编写的包装类在其第一个参数里接收了C类实例的时候,它可以分派到最初的方法和访问状态信息。

从技术上讲,这种嵌套函数版本是有效的,因为Python创建了一个绑定的方法对象,并且由此只有当一个方法属性引用一个简单的函数的时候,才把主体类实例传递给self参数; 相反,当它引用可调用的类的一个实例的时候,可调用的类的实例传递给self,以允许可调用的类访问自己的状态信息。

还要注意,嵌套函数可能是支持函数和方法的装饰器的最直接方式,但是不一定是唯一的方式。例如,上面的描述符,调用的时候接收了描述符和主体类实例。

类装饰器

函数装饰器已经证明了是如此 有用,以至于这一模式在Python2.6 和 Python3.x中扩展为允许类装饰器。类装饰器与函数装饰器密切相关,实际上,它们使用相同的语法和非常相似的编码模式。 然而,不是包装单个的函数或方法,类装饰器是管理类的一种方式,或者用管理或扩展类所创建的实例的额外逻辑,来包装实例构建调用。

用法

从语法上讲,类装饰器就像前面的class语句一样(就像前面函数定义中出现的函数装饰器)。在语法上,假设装饰器是返回一个可调用对象的一个单参数的函数,类装饰器语法:

@decorator                    # Decorate class
class C:
    ...
    
x = C(99)                     # Make an instance

等同于下面的语法……类自动地传递给装饰器函数,并且装饰器的结果返回来分配给类名:

class C:
    ...
C = decorator(C)                # Rebind class name to decorator result

x = C(99)                        # 本质上相当于 decorator(C)(99)

直接的效果就是,随后调用类名会创建一个实例,该实例会触发装饰器所返回的可调用对象,而不是调用最初的类自身

实现

新的类装饰器使用函数装饰器所使用的众多相同的技术来编码。由于类装饰器也是返回一个可调用对象的一个可调用对象,因此大多数函数和类的组合已经足够了。

尽管先编码,但装饰器的结果是当随后创建一个实例的时候才运行的。例如,要在一个类创建之后直接管理它,返回最初的类自身:

def decorator(C):
    # Process class C
    return C
    
@decorator
class C: ...                # C = decorator(C)

不是插入一个包装器层来拦截随后的实例创建调用,而是返回一个不同的可调用对象:

def decorator(C):
    # Save or use class C
    # Return a different callable: nested def, class with __call__, etc.

@decorator
class C: ...                # C = decorator(C)

这样一个类装饰器返回的可调用对象,通常创建并返回最初的类的一个新的实例,以某种方式来扩展对其接口的管理。例如,下面的实例插入一个对象来拦截一个类实例的未定义的属性:

def decorator(cls):               # On @ decoration
    class Wrapper:
        def __init__(self, *args):    # On instance creation
            self.wrapped = cls(*args)    # 这里的*args和__init__方法参数的*args值一样
        def __getattr__(self, name):    # On attribute fetch
            return getattr(self.wrapped, name)
    return Wrapper

@decorator
class C:                        # C = decorator(C)
    def __init__(self, x, y):            # Run by Wrapper.__init__
        self.attr = "spam"
        
x = C(6, 7)                # Really calls Wrapper(6, 7)
print(x.attr)               # Runs Wrapper.__getattr__, prints "spam"

此例理解逻辑:
    1,执行到 x = C(6, 7)的时候,装饰器返回了一个内部类Wrapper,实际上x = C(6, 7)可以看做是 x = Wrapper(6, 7)
    2,cls = 原始类C
    3,Wrapper(6, 7)创建了一个内部实例(第一个实例),并走__init__方法初始化该实例对象
    4,第二个实例有了一个对象wrapped = C(6, 7),这里又会创建一个实例(第二个实例),所以第一个实例的wrapped对象 = 第二个实例,第二个实例有三个属性,分别为self.x、self.y、self.attr
    5,当打印print(x.attr)的时候,就相当于打印的是第一个实例的attr属性,但是Wrapper类中__init__方法并没有初始化attr属性,所以找到了__getattr__方法,该方法返回了第二个实例的attr属性。 所以结果为spam

在这个例子中,装饰器吧类的名称重新绑定到另一个类,这个类在一个封闭的作用域中保持了最初的类,并且当调用它的时候,创建并嵌入了最初的类的一个实例。当随后从该实例获取一个属性的时候,包装器的__getattr__拦截了它,并且将其委托给最终的类的嵌入的实例。 此外,每个被装饰的类都创建一个新的作用域,它记住了最初的类。 在本文后面,我们将用一些更有用的代码来充实这个例子。

就像函数装饰器一样,类装饰器通常可以编写为一个创建并返回可调用的对象的“工厂”函数,或者使用__init__ 或 __call__ 方法来拦截所有调用操作的类,或者是由此产生的一些组合。工厂函数通常在封闭的作用域引用中保持状态,类通常在属性中保持状态。

支持多个实例

和函数装饰器一样,使用类装饰器的时候,一些可调用对象组合比另一些工作得更好。考虑前面例子的类装饰器的一个如下的无效替代方式:

class Decorator:
    def __init__(self, C):                # On @ decorator
        self.C = C
    def __call__(self, *args):            # On instance creation
        self.wrapped = self.C(*args)
        return self
    def __getattr__(self, attrname):        # On atrribute fetch
        return getattr(self.wrapped, attrname)
        
@Decorator
class C: ...                                # C = Decorator(C)

x = C()
y = C()                                    # Overwrites x!

这段代码处理多个被装饰的类(每个产生一个新的 Decorator实例),并且会拦截实例创建调用(每运行__call__方法)。然而,和前面的版本不同,这个版本没有能够处理给定的类的多个实例……每个实例创建调用都覆盖了前面保存的实例。最初的版本确实支持多个实例,因为每个实例创建调用产生了一个新的独立的包装器对象。 更通俗的说,如下模式的每一个都支持多个包装的实例:

def decorator(C):                    # On @ decoration
    class Wrapper:
    def __init__(self, *args):        # On instance creation
        self.wrapped = C(*args)
    return Wrapper
    
class Wrapper: ...
def decorator(C):                    # On @ decoration
    def onCall(*args):                # On instance creation
        return Wrapper(C(*args))    # Embed instance in instance
    return onCall

我们将在本文随后一个更为实用的环境中研究这一现象,然而,在实际中个,我们必须小心地正确组合可调用类型以支持自己的意图。

装饰器嵌套

有的时候,一个装饰器不够。为了支持多个步骤的扩展,装饰器语法允许我们向一个装饰的函数或方法添加包装器逻辑的多个层。当使用这一功能的时候,每个装饰器必须出现在自己的一行中。这种形式的装饰器语法:

@A
@B
@C
def f(...):
    ...

如下这样运行:

def f(...):
    ...
f = A(B(C(f)))            # first C, seconds B, third A

这里,最初的函数通过3个不同的装饰器传递,并且最终的可调用对象返回来分配给最初的名称。每个装饰器处理前一个的结果,这可能是最初的函数或一个插入的包装器。

如果所有的装饰器都插入包装器,直接的效果就是,当调用最初的函数名时,将会调用包装对象逻辑的3个不同的层,从而以3种不同的方式扩展最初的函数。列出的最后的装饰器是第一次应用的并且最深层次的嵌套。

就像对函数一样,多个类装饰器导致了多个嵌套的函数调用,并且可能导致围绕实例创建调用的包装器逻辑的多个层。 例如,如下的代码:

@spam
@eggs
class C:
    ...
    
X = C()

等同于如下的代码:

class C:
    ...
C = spam(eggs(C))

X = C()

再次,每个装饰器都自由地返回最初的类或者一个插入的包装器对象。有了包装器,当最终请求最初C类的一个实例的时候,这一调用会重定向到spam和eggs装饰器提供的包装层对象,二者可能有任意的不同角色。

例如,如下的什么也不做的装饰器只是返回被装饰的函数:

def d1(F): return F
def d2(F): return F
def d3(F): return F

@d1
@d2
@d3
def func():                  # func = d1(d2(d3(func)))
    print('spam')
    
func()                    # prints "spam"

同样的语法在类上也有效,就想着你什么也不做的装饰器一样。

然而,当装饰器插入包装器函数对象,调用的时候它们可能扩展最初的函数……如下的代码将其结果连接到一个装饰器层中,随着它从内向外地运行层:

def d1(F): return lambda: "X" + F()
def d2(F): return lambda: "Y" + F()
def d3(F): return lambda: "Z" + F()

@d1
@d2
@d3
def func():             # func = d1(d2(d3(func)))
    return "spam"
    
print(func())            # prints "XYZspam"

我们这里使用了lambda函数来实现包装器层(每个层在一个封闭的作用域里保持了包装的函数)。实际上,包装器可以采取函数、可调用的类以及更多央视。当设计良好的时候,装饰器嵌套允许我们以种类多样的方式来组合扩展步骤。

装饰器参数

函数装饰器 和 类装饰器似乎都能接受参数,尽管实际上这些参数传递给了真正返回装饰器的一个可调用对象,而装饰器反过来又返回一个可调用对象。例如,下面的代码:

@decorator(A, B)
def F(arg):
    ...
F(99)

自动地映射到其对等的形式,其中装饰器是一个可调用对象,它返回实际的装饰器。返回的装饰器反过来返回可调用的对象,这个对象随后运行以调用最初的函数名:

def F(arg):
    ...
F = decorator(A, B)(F)        # 重新绑定到装饰器对象所返回的对象上

F(99)                  # 本质上是 decorator(A, B)(F)(99)

装饰器参数在装饰发生之前就解析了,并且它们通常用来保持状态信息供随后的调用使用。 例如,这个例子中的装饰器函数,可能采用如下的形式:

def decorator(A, B):
    # Save or use A, B
    def actualDecorator(F):
        # Save or use function F
        # Return a callable: nested def, class with __call__, etc.
        return callable
    return actualDecorator

这个结构中的外围函数通常会吧装饰器参数与状态信息分开保存,以便在实际的装饰器中使用,或者在它所返回的可调用对象中使用,或者在二者中都使用。这段代码在封闭的函数作用域引用中保存了状态信息参数,但是通常也可以使用类属性。

换句话说,装饰器参数往往意味着可调用对象的3个层级接受装饰器参数的一个可调用对象它返回一个可调用对象以作为装饰器该装饰器返回一个可调用对象来处理对最初的函数或类的调用。 这3个层级的每一个都可能是一个函数或类,并且可能以作用域或类属性的形式保存了状态。我们将在本文后面看到引用装饰器参数的实际例子。

装饰器管理函数和类

尽管本文剩下的很大篇幅集中在包装对函数和类的随后调用,但我应该强调装饰器机制比这更加通用……它是在函数和类创建之后通过一个可调用对象传递它们的一种协议。 因此,它可以用来调用任意的创建后处理:

def decorator(o):
    # Save or augment function or class O
    return o
    
@decorator
def F(): ...                # F = decorator(F)

@decorator
class C: ...                # C = decorator(C)

只要以这种方式返回最初装饰器的对象,而不是返回一个包装器,我们就可以管理函数和类自身,而不是管理随后对它们的调用。 在本文稍后,我们将看到使用这一思想的更为实际的例子,它们用装饰器吧可调用对象注册到一个API,并且在创建函数的时候为它们赋值属性。


编写函数装饰器

现在回到代码层面。本文剩下的内容里,我们将学习实际的例子来展示刚刚介绍的装饰器概念。本小节展示几个函数装饰器的实际例子,下一小节展示实际的类装饰器的例子。此后,我们将通过使用类和函数装饰器的一些较大的例子来结束本文。

跟踪调用

首先,让我们回顾下之前讲的跟踪器的例子。如下的代码定义并应用一个函数装饰器,来统计对装饰器的函数的调用次数,并且针对每一次调用打印跟踪信息:

class tracer:
    def __init__(self, func):                # On @ decoration: save original func
        self.calls = 0
        self.func = func
    def __call__(self, *args):                # On later calls: run original func
        self.calls += 1
        print('call %s to %s' % (self.calls, self.func.__name__))
        self.func(*args)
        
@tracer
def spam(a, b, c):                    # spam = tracer(spam)
    print(a + b + c)                    # Wraps spam in a decorator object

注意,用这个类装饰器的每个函数将如何创建一个新的实例,带有自己保存的函数对象和调用计数器。还要注意观察,*args参数语法如何用来打包和解压任意的多个传入参数。这一通用性使得这个装饰器可以用来包装带有任意多个参数的任何函数(这个版本还不能在类方法上工作,但是,我们将在后面修改这一点)。

现在,如果导入这个模块的函数并交互地测试它,将会得到如下的一种行为……每次调用都初始地产生一条跟踪信息,因为装饰器拦截了调用。这段代码在Python 2.6 和 Python 3.x下都能运行,就像本文中所有其他的代码一样,除非特别提示:

>>> from decorator1 import spam

>>> spam(1, 2, 3)                # Really calls the tracer wrapper object
call 1 to spam
6

>>> spam('a', 'b', 'c')         # 调用__call__方法的返回
call 2 to spam
abc

>>> spam.calls                    # 打印计数器
2

>>> spam
<__main__.tracer object at 0x0000014BB8D032E8>

运行的时候,tracer类和装饰的函数分开保存,并且拦截对装饰的函数随后的调用,以便添加一个逻辑层来统计和打印每次调用。注意,此例中调用的总数如何作为装饰的函数的一个属性显示……装饰的时候,spam实际上是tracer类的一个实例(对于进行类型检查的程序,可能还会衍生一次查找,但是通常是有益的)。

对于函数调用。@装饰语法可能比修改每次调用来说明额外的逻辑层要更加方便,并且它避免了意外地直接调用最初的函数。考虑如下所示的非装饰器的对等代码:

calls = 0
def tracer(func, *args):
    global calls
    calls += 1
    print('call %s to %s' % (calls, func.__name__))
    func(*args)
    
def spam(a, b, c):
    print(a, b, c)
    
>>> spam(1, 2, 3)            # Normal non-traced call: accidental?
1 2 3

>>> tracer(spam, 1, 2, 3)    # Special traced call without decorators
call 1 to spam
1 2 3

这一替代方法可以用在任何函数上,且不需要特殊的@语法,但是和装饰器版本不同,它在代码中调用函数的每个地方需要额外的语法。此外,它的意图可能不够明显,并且它不能确保额外的层将会针对常规调用而调用。尽管装饰器不是必须的(我们总是可以手动地重新绑定名称),它们通常是最为方便的。

状态信息保持选项

前面小节的最后一个例子引发了一个重要的问题。函数装饰器有各种选项来保持装饰的时候说提供的状态信息,以便在实际函数调用过程中使用。它们通常需要指出多个装饰的对象以及多个调用,但是,有多种方法来实现这些目标:实例属性、全局变量、非局部变量 和 函数属性,都可以用于保持状态。

类实例属性

例如,这里是前面的例子的一个扩展版本,其中添加了对关键字参数的支持,并且返回包装函数的结果,以支持更多的用例:

class tracer:                                # State via instance attributes
    def __init__(self, func):                # On @ decorator
        self.calls = 0                        # Save func for later call
        self.func = func
    def __call__(self, *args, *kwargs):        # On call to original function
        self.calls += 1
        print('call %s to %s' % (self.calls, self.func.__name__))
        return self.func(*args, **kwargs)
        
@tracer
def spam(a, b, c)                    # Same as: spam = tracer(spam)
    print(a + b + c)                    # Triggers trace.__init__
    
@tracer
def eggs(x, y):                        # Same as: eggs = tracer(eggs)
    print(x ** y)                        # Wraps eggs in a tracer object
    
spam(1, 2, 3)                            # Really calls tracer instance: runs tracer.__call__
spam(a=4, b=5, c=6)                        # spam is an instance attribute

eggs(2, 16)                                # Really calls tracer instance, self.func is eggs
eggs(4, y=4)                                # self.calls is per-function here (need 3.x nonlocal)

就像最初的版本一样,这里的代码使用类实例属性来显示的保存状态。包装的函数和调用计数器都是针对每个实例的信息……每个装饰都有自己的拷贝。当在Python 2.6和Python 3.x下运行一段脚本的时候,这个版本的输出如下所示。注意spam和eggs函数的每一个是如何有自己的调用计数器的,因为每个装饰都创建一个新的类实例:

call 1 to spam
6
call 2 to spam
15
call 1 to eggs
65536
call 2 to eggs
256

尽管对于装饰函数有用,但是当应用于方法的时候,这种编码方案也有问题(随后更为详细的介绍)。

封闭作用域和全局作用域

封闭def作用域引用和嵌套的def常常可以实现相同的效果,特别是对于装饰的最初函数这样的静态数据。然而,在这个例子中,我们也需要封闭的作用域中的一个计数器,它随着每次调用而更改,并且,这在Python 2.6中是不可能的。在Python 2.6中,我们可以使用类和属性,正如我们前面所做的那样,或者使用全局声明状态变量移出到全局作用域

calls = 0
def tracer(func):
    def wrapper(*args, **kwargs):
        global calls
        calls += 1
        print('call %s to %s' % (calls, func.__name__))
        return func(*args, **kwargs)
    return wrapper
    
@tracer
def spam(a, b, c):
    print(a + b + c)

@tracer
def eggs(x, y):
    print(x ** y)
    
spam(1, 2, 3)
spam(a=4, b=5, c=6)

eggs(2, 16)
eggs(4, y=4)

遗憾的是,把计数器移出到共同的全局作用域允许像这样修改它们,也意味着它们将为每个包装的函数所共有。和类实例属性不同,全局计数器是跨程序的,而不是针对每个函数的……对于任何跟踪的函数调用,计数器都会递增。如果你比较这个版本与前一个版本的输出,就可以看出其中的区别……单个的、共享的全局调用计数器根据每次装饰函数的调用不正确的更新:

call 1 to spam
6
call 2 to spam
15
call 3 to eggs
65536
call 4 to eggs
256

封闭作用域和nonlocal

共享全局状态坑内是我们在某些情况下想要的。如果我们真的想要一个针对每个函数的计数器,那么像前面那样使用类,要么使用Python 3.x中新的nonlocal语句。由于这一新的语句允许修改封闭的函数作用域变量,所以它们可以充当针对每次装饰的、可修改的数据:

def tracer(func):                                # State via enclosing scope and nonlocal
    calls = 0                                    # Instead of class attrs or global
    def wrapper(*args, **kwargs):                # calls is per-function, not global
        nonlocal calls
        calls += 1
        print('call %s to %s' % (calls, func.__name__))
        return func(*args, **kwargs)
    return wrapper
    
@tracer
def spam(a, b, c):                    # Same as: spam = tracer(spam)
    print(a + b +c)
 
@tracer
def eggs(x, y):                        # Same as: eggs = tracer(eggs)
    print(x ** y)
    
spam(1, 2, 3)                            # Really calls wrapper, bound to func
spam(a=4, b=5, c=6)                      # wrapper calls spam

eggs(2, 16)                            # Really calls wrapper, bound to eggs
eggs(4, y=4)                            # Nonlocal calls _is_ not per-function here

现在,由于封装的作用域变量不能跨程序而成为全局的,所以每个包装的函数再次有了自己的计数器,就像是针对类的属性一样。这里是在Python 3.x下运行时新的输出:

call 1 to spam
6
call 2 to spam
15
call 1 to eggs
65536
call 2 to eggs
256

函数属性

最后,如果你没有使用Python 3.x 并且没有一条nonlocal语句,可能仍然能够针对某些可改变的状态使用函数属性来避免全局和类。在最新的Python中,我们可以把任意属性分配给函数以附加它们,使用func.attr=value就可以了。在我们的例子中,可以直接对状态使用wrapper.calls。如下的代码与前面的nonlocal版本一样地工作,因为计数器再一次是针对每个装饰的函数的,但是,它也可以在Python 2.6下运行:

def tracer(func):                # State via enclosing scope and func attr
    def wrapper(*args, **kwargs):    # calls is per-function, not global
        wrapper.calls += 1
        print('call %s to %s' % (wrapper.calls, func.__name__))
        return func(*args, **args)
    wrapper.calls = 0
    return wrapper

注意,这种方法有效,只是因为名称wrapper保持在封闭的tracer函数的作用域中。当我们随后增加wrapper.calls时,并不是在修改名称wrapper本身,因此,不需要nonlocal声明。

这种方案几乎作为一个脚注来介绍,因为它比Python 3.x中的nonlocal要隐晦得多,并且可能留待其他方案无济于事的情况下使用更好。然而,我们将解答本文末尾一个问题的时候使用它,那里,我们需要从装饰器代码的外部访问保存的状态;nonlocal只能从嵌套函数自身的内部看到,但是函数属性有更广泛的可见性。

由于装饰器往往意味着可调用对象的多个层级,所以我们可以用封闭的作用域和带有属性的类来组合函数,以实现各种各样的编码结构。正如我们随后将见到的,这有时候可能比我们所期待的要细微……每个装饰的函数应该有自己的状态,并且每个装饰的类都应该需要针对自己的状态和针对每个产生实例的状态。

实际上,正如下一小节说介绍的,如果我们也想要对一个类方法应用函数装饰器,必须小心Python在作为可调用类实例对象的装饰器编码和作为函数的装饰器编码之间的区分。

类错误之一:装饰类方法

当我编写上面的第一个tracer函数的时候,我幼稚地假设它也应该适用于任何方法……装饰的方法应该同样地工作,但是,自动的self实例参数应该直接包含在*args的前面。 遗憾的是,我错了:当应用于类方法的时候,tracer的第一个版本失效了,因为self是装饰器类的实例,并且装饰的主体类的实例没有包含在*args中。 在Python 3.x和Python 2.6中都是如此。

我们在前面介绍了这一现象,但是现在,我们可以在真实的工作代码环境中看到它。 假设基于类的跟踪装饰器如下:

class tracer:
    def __init__(self, func):                # On @ decorator
        self.calls = 0                       # Save func for later call
        self.func = func
    def __call__(self, *args, **kwargs):     # On call to original function
        self.calls += 1
        print('call %s to %s' % (self.calls, self.func.__name__))
        return self.func(*args, **kwargs)

简单函数的装饰与前面介绍的一样:

@tracer
def spam(a, b, c):                        # spam = tracer(spam)
    print(a + b + c)                        # Trigger tracer.__init__
    
spam(1, 2, 3)                            # Runs tracer.__call__
spam(a=4, b=5, c=6)                        # spam is an instance attribute

然而,类方法的装饰失效了。

class Person:
    def __init__(self, name, pay):
        self.name = name
        self.pay = pay
        
    @tracer
    def giveRaise(self, percent):            # giveRaise = tracer(giveRaise)
    self.pay *= (1.0 + percent)
    
    @tracer
    def lastName(slef):                        # lastName = tracer(lastName)
    return self.name.split()[-1]
    
bob = Person('Bob Smith', 50000)                # tracer remembers method funcs
bob.giveRaise(.25)                              # Runs tracer.__call__(???, .25)
print(bob.lastName())                            # Runs tracer.__call__(???)

这里问题的根源在于,tracer类的__call__方法的self……它是一个tracer实例,还是一个Person实例? 我们真的需要将其编写为两者都是:tracer用于装饰器状态,Person用于指向最初的方法。实际上,self必须是tracer对象,以提供对tracer的状态信息的访问; 不管装饰一个简单的函数还是一个方法,都是如此。

遗憾的是,当我们用__call__把装饰方法名重绑定到一个类实例对象的时候,Python只向self传递了tracer实例; 它根本没有在参数列表中传递Person主体。 此外,由于tracer不知道我们要用方法调用处理的Person实例的任何信息,没有办法创建一个带有一个实例的绑定的方法,因此,没有办法正确的分配调用。

实际上,前面的列表最终传递了太少的参数给装饰的方法,并且导致了一个错误。在装饰器的__call__方法添加一行,以打印所有的参数来验证这一点。正如你所看到的,self是一个tracer,并且Person实例完全缺失:

wKioL1bX3h3zpOV0AADS6BMVGvs141.png
wKioL1bX3h3zpOV0AADS6BMVGvs141.png

正如前面提到的,出现这种情况是因为:当一个方法名绑定只是绑定到一个简单的函数,Python向self传递了硬汉的主体实例;当它是一个可调用类的实例的时候,就传递这个类的实例。从技术上讲,当方法时一个简单函数的时候,Python只是创建了一个绑定的方法对象,其中包含了主体实例。

使用嵌套函数来装饰方法

如果想要函数装饰器在简单函数和类方法上都能工作,最直接的解决方法在于使用前面介绍的状态保持方法之一……把自己的函数装饰器编写为嵌套的def,以便对于包装器类实例和主体类实例都不需要依赖于单个self实例参数。

如下的替代方案使用Python 3.x的nonlocal。由于装饰的方法从重新绑定到简单的函数而不是实例对象,所以Python正确地传递了Person对象作为第一个参数,并且装饰器将其从*args中的第一项传递给真正的、装饰的方法的self参数:

# A decorator for both functions and methods
def tracer(func):                    # Use function, not class with __call__
    calls = 0                        # Else "self" is decorator instance only!
    def onCall(*args, **kwargs):
        nonlocal calls
        calls += 1
        print('call %s to %s' % (calls, func__name__))
        return func(*args, **kwargs)
    return onCall
    
# Applies to simple functions

@tracer
def spam(a, b, c):                    # spam = trace(spam)
    print(a + b + c)                    # onCall remenbers spam
    
spam(1, 2, 3)                        # Runs onCall(1, 2, 3)
spam(a=4, b=5, c=6)

# Applies to class method functions too!

class Person:
    def __init__(self, name, pay):
        self.name = name
        self.pay = pay
        
    @tracer
    def giveRaise(self, percent):        # giveRaise = tracer(giveRaise)
        self.pay *= (1.0 + percent)        # onCall remembers giveRaise
        
    @tracer
    def lastName(self):                    # lastName = tracer(lastName)
        return self.name.split()[-1]
        
print('methods...')
bob = Person('Bob Smith', 50000)
sue = giveRaise(.10)                        # Runs onCall(sue, .10)
print(sue.pay)
print(bob.lastName(), sue.lastName())        # Runs onCall(bob), lastName in scopes

这个版本在函数和方法上都有效:

call 1 to spam
6
call 2 to spam
15
method...
Bob Smith Sue Jones
call 1 to giveRaise
110000.0
call 1 to lastName
call 2 to lastName
Smith Jones

使用描述符装饰方法

尽管前一小节介绍的嵌套函数的解决方案是支持应用于函数和类方法的装饰器的最直接方法,其他的方法也是可能的。例如,我们之前介绍的描述符功能,在这里也能排上用场。

还记得我们在前面的讨论中,描述符可能是分配给对象的一个类属性,该对象带有一个__get__方法,当引用或获取该属性的时候自动运行该方法(在Python 2.6中需要对象派生,但在Python 3.x中不需要):

class Descriptor(object):
    def __get__(self, instance, owner): ...
    
class Subject:
    attr = Descriptor()
    
X = Subject()
X.attr                # Roughly runs Descriptor.__get__(Subject.attr, X, Subject)

描述符也能够拥有__set__ 和 __del__访问方法,但是,我们在这里不需要它们。现在,由于描述符的__get__方法在调用的时候接收描述符类和主体类实例,因此当我们需要装饰器的状态以及最初的类实例来分派调用的时候,它很适合于装饰方法。考虑如下的替代的跟踪装饰器,它也是一个描述符:

class tracer(object):
    def __init__(self, func):                # On @ decorator
        self.calls = 0
        self.func = func
    def __call__(self, *args, **kwargs):      # On call to original func
        self.calls += 1
        print('call %s to %s' % (self.calls, self.func.__name__))
        return self.func(*args, **kwargs)
    def __get__(self, instance, owner):     # On method attribute fetch
        return wrapper(self, instance)
        
class wrapper:
    def __init__(self, desc, subj):            # Save both instances
        self.desc = desc                        # Rute calls back to decr
        self.subj = subj
    def __call__(self, *args, **kwargs):
        return self.desc(self.subj, *args, **kwargs)    # Runs tracer.__call__
        
@tracer
def spam(a, b, c):        
    ... same as prior ...
    
class Person:
    @tracer
    def giveRaise(self, percent):
        ... same as prior ...

这和前面的嵌套的函数代码一样有效。装饰的函数只调用其__call__,而装饰的方法首先调用其__get__来解析方法名获取(在instance.method上); __get__返回的对象保持主体类实例并且随后调用以完成调用表达式,由此触发__call__。例如,要测试代码的调用:

sue.giveRaise(.10)                # Runs __get__ then __call__

首先运行tracer.__get__,因为Person类的giveRaise属性已经通过函数装饰器重新绑定到了一个描述符。然后,调用表达式触发返回的包装器对象的__call__方法,它返回来调用tracer.__call__。

包装器对象同时保持描述符和主体实例,因此,它可以将控制指回到最初的装饰器/描述符实例。实际上,在方法属性获取过程中,包装的对象保持了主体类实例可用,并且将其添加到了随后调用的参数列表,该参数列表会传递给__call__。在这个应用程序中,用这种方法把调用路由到描述符类实例是需要的,因此对包装方法的所有调用都使用描述符实例对象中的同样的调用计数器状态信息。

此外,我们也可以使用一个嵌套的函数和封闭的作用域引用来实现同样的效果……如下的版本和前面的版本一样的有效,通过为一个嵌套和作用域引用交换类和对象属性,但是,它所需的代码显著减少:

class tracer(object):
    def __init__(self, func):
        self.calls = 0
        self.func = func
    def __call__(self, *args, **kwargs):
        self.call += 1
        print('call %s to %s' % (self.calls, self.func.__name__))
        return self.func(*args, **kwargs)
    def __get__(self, instance, owner):
        def wrapper(*args, **kwargs):
        return self(instance, *args, **kwargs)
    return wrapper

为这些替代方法添加print语句是为了自己跟踪/调用过程的两个步骤,用前面嵌套函数替代方法中同样的测试代码来运行它们。在两种编码中,基于描述符的方法也比嵌套函数的选项要细致得多,因此,它可能是这里的又一种选择。在其他的环境中,它也可能是一种有用的编码模式。

在本文剩余的内容中,我们将相当随意地使用类或函数来编写函数装饰器,只要它们都只适用于函数。一些装饰器可能并不需要最初的类的实例,并且如果编写为一个类,它将在函数和方法上都有效……例如Python自己的静态方法装饰器,就不需要主体类的一个实例(实际上,它主要从调用中删除实例)。

然而,这里的叙述的教训是,如果你想要装饰器在简单函数和类方法上都有效,最好使用基于嵌套函数的编码模式,而不是带有调用拦截的类。

计时调用

为了展示函数装饰器的各种各样能力的一个特殊样例,让我们来看一种不同的应用场景。下一个装饰器将对一个装饰的函数的调用进行计时……既有针对一次调用的时间,也有所有迪欧用固定的总的时间。该装饰器应用于两个函数,以便比较列表解析和map内置调用所需的时间:

import time

class timer:
    def __init__(self, func):
        self.func = func
        self.alltime = 0
    def __call__(self, *args, **kwargs):
        start = time.clock()
        result = self.func(*args, **kwargs)
        elapsed = time.clock() - start
        self.alltime += elapsed
        print('%s: %.5f, %.5f' % (self.func.__name__, elapsed, self.alltime))
        return result
    
@timer
def listcomp(N):
    return [x * 2 for x in range(N)]
    
@timer
def mapcall(N):
    return map((lambda x: x * 2), range(N))
    
result = listcomp(5)
listcomp(50000)
listcomp(500000)
listcomp(1000000)
print(result)
print('allTime = %s' % listcomp.alltime)

print('')
result = mapcall(5)
mapcall(50000)
mapcall(500000)
mapcall(1000000)
print(result)
print('allTime = %s' % mapcall.alltime)

print('map/comp = %s' % round(mapcall.alltime / listcomp.alltime, 3))

在这个例子中,一种非装饰器的方法允许主体函数用于计时或不用于计时,但是,当需要计时的时候,它也会使调用签名变得复杂(我们需要在每个调用的时候添加代码,而不是在def中添加一次代码),并且可能没有直接的方法来保证一个程序中的所有列表生成器调用可以通过计时器逻辑路由,在找到所有签名并潜在地修改它们方面有所不足。

在Python 2.6中运行的时候,这个文件的self测试代码输出如下:

listcomp: 0.00000, 0.00000
listcomp: 0.01000, 0.01000
listcomp: 0.18000, 0.19000
listcomp: 0.35000, 0.54000
[0, 2, 4, 6, 8]
allTime = 0.54

mapcall: 0.00000, 0.00000
mapcall: 0.03000, 0.03000
mapcall: 0.20000, 0.23000
mapcall: 0.42000, 0.65000
[0, 2, 4, 6, 8]
allTime = 0.65
map/comp = 1.204

测试细微差别:我没有在Python 3.x下运行这段代码,因为正如我们知道的,map内置函数在Python 3.x中返回一个迭代器,而不是像在Python 2.6总那样返回一个实际的列表。由此,Python 3.x的map不能和一个列表解析的工作直接对应(即,map测试实际上在Python 3.x中没有花时间)。

如果你想要在Python 3.x下运行这段代码,那么就使用list(map())来迫使它像列表解析那样构建一个列表,否则,就不是真正地进行同类比较。然而,不要再Python 2.6中这么做,如果这么做了,map测试将会负责构建两个列表,而不是一个。

添加装饰器参数

前面小节介绍的计时器装饰器有效,但是如果它更加可配置的话,那就更好……例如,提供一个输出标签并且可以打开或关闭跟踪消息,这些在一个通用目的的工具中可能很有用。装饰器参数在这里派上了用场:对它们适当编码后,我们可以使用它们来指定配置选项,这些选项可以根据每个装饰的函数而编码。例如,可以像下面这样添加标签:

def timer(label=''):
    def decorator(func):
        def onCall(*args):                    # args passed to function
            ...                                # func retained in enclosing scope
            print(label, ...)                    # label retained in enclosing scope
        return onCall
    return decorator                            # Returns that actual decorator
    
@timer('==>')                                    # Like listcomp = timer('==>')(listcomp)
def listcomp(N): ...                            # listcomp is rebound to decorator

listcomp(...)                                   # Really calls decorator

这段代码添加了一个封闭的作用域来保持一个装饰器参数,以便随后真正调用的时候使用。当定义了listcomp函数的时候,它真的调用decorator(timer的结果,在真正装饰发生之前运行),带有其封闭的作用域内可用的label值。也就是说,timer返回decorator,后者记住了装饰器参数和最初的函数,并且返回一个可调用的对象,这个可调用对象在随后的调用时调用最初的函数。

我们可以把这种结构用于定时器之中,来允许在装饰器的时候传入一个标签和一个跟踪控制标志。下面是这么做的一个例子,编码在一个名为mytools.py的模块文件中,以便它可以作为一个通用工具导入:

import time

def timer(label='', trace=True):                # On decorator args: retain args
    class Timer:
        def __init__(self, func):                # On @: retain decorated func
            self.func = func
            self.alltime = 0
        def __call__(self, *args, **kwargs):        # On calls: call original
            start = time.clock()
            result = self.func(*args, **kwargs)
            elapsed = time.clock() - start
            self.alltime += elapsed
            if trace:
                format = '%s %s: %.5f, %.5f'
                values = (label, self.func.__name__, elapsed, self.alltime)
                print(format % values)
            return result
    return Timer

我们在这里做的主要是吧最初的Timer类嵌入一个封闭的函数中,以便创建一个作用域以保持装饰器参数。外围的timer函数在装饰器发生前调用,并且它只是返回Timer类作为实际的装饰器。在装饰时,创建了Timer的一个实例来记住装饰函数自身,而且访问了位于封闭的函数作用域中的装饰器参数。

这一次,不是吧self测试代码嵌入这个文件,我们将在一个不同的文件中运行装饰器。下面是时间装饰器的一个客户,模块文件testseqs.py,再次将其应用于迭代器替代方案:

from mytools import timer

@timer(label='[CCC]==>')
def listcomp(N):                        # Like listcomp = timer(...)(listcomp)
    return [x * 2 for x in range(N)]    # listcomp(...) triggers Timer.__call__
    
@timer(trace=True, label='[MMM]==>')
def mapcall(N):
    return map((lambda x: x * 2), range(N))
    
for func in (listcomp, mapcall):
    print('')
    result = func(5)                    # Time for this call, all calls, return value
    func(50000)
    func(500000)
    func(1000000)
    print(result)
    print('allTime = %s' % func.alltime)        # Total time for all calls
    
print('map/comp = %s' % round(mapcall.alltime / listcomp.alltime, 3))

再一次说明,如果想要在Python 3.x中正常地运行这段代码,把map函数包装到一个list调用中。当子啊Python 2.6中运行的时候,这文件打印如下的输出……每个装饰的函数 现在都有了一个子集的标签,该标签由装饰器参数定义:

[CCC]==> listcomp: 0.00000, 0.00000
[CCC]==> listcomp: 0.01000, 0.01000
[CCC]==> listcomp: 0.16000, 0.17000
[CCC]==> listcomp: 0.42000, 0.59000
[0, 2, 4, 6, 8]
allTime = 0.59

[MMM]==> mapcall: 0.00000, 0.00000
[MMM]==> mapcall: 0.02000, 0.02000
[MMM]==> mapcall: 0.21000, 0.23000
[MMM]==> mapcall: 0.39000, 0.62000
[0, 2, 4, 6, 8]
allTime = 0.62
map/comp = 1.051

与通常一样,我们可以可以交互地测试它,看看配置参数是如何应用的:

>>> from mytools import timer
>>> @timer(trace=False)                 # No tracing, collect total time
... def listcomp(N):
... return [x * 2 for x in range(N)]
...
>>> x = listcomp(5000)
>>> x = listcomp(5000)
>>> x = listcomp(5000)
>>> listcomp
<mytools.Timer instance at 0x025C77B0>
>>> listcomp.alltime
0.0051938863738243413

>>> @timer(trace=True, label='\t=>')     # Turn on tracing
... def listcomp(N):
... return [x * 2 for x in range(N)]
...
>>> x = listcomp(5000)
    => listcomp: 0.00155, 0.00155
>>> x = listcomp(5000)
    => listcomp: 0.00156, 0.00311
>>> x = listcomp(5000)
    => listcomp: 0.00174, 0.00486
>>> listcomp.alltime
0.0048562736325408196

这个计时函数装饰器可以用于任何函数,在模块中和交互模式下都可以。换句话,它自动获得作为脚本中计时代码的资格。


编写类装饰器

到目前为止,我们已经编写了函数装饰器来管理函数调用,但是,正如我们已经见到的,Python 2.6和Python 3.x扩展了装饰器使其也能在上有效。如同前面所提到的,尽管类似于函数装饰器的概念,但类装饰器应用于类……它们可以用于管理类自身,或者用来拦截实例创建调用以管理实例。和函数装饰器一样,类装饰器其实只是可选的语法糖,尽管很多人相信,它们使得程序员的意图更为明显并且能使不正确的调用最小化。

单体类

由于类装饰器可以连接实例创建调用,所以它们可以用来管理一个类的所有实例,或者扩展这些实例的接口。为了说明这点,这里的第一个类装饰器实例做了前面一项工作……管理一个类的所有实例。这段代码实现了传统的单体编码模式,其中最多只有一个类的一个实例存在。其单体函数以管理的属性定义并返回一个函数,并且@语法自动在这个函数中包装了一个主体类:

instances = {}
def getInstance(aClass, *args):            # Manage global table
    if aClass not in instances:            # Add **kargs for keywords
        instances[aClass] = aClass(*args)    # One dict entry per class
    return instances[aClass]
    
def singleton(aClass):                    # On @ decoration
    def onCall(*args):                    # On instance creation
        return getInstance(aClass, *args)
    return onCall

为了使用它,装饰器用来强化单体模型的类:

@singleton                        # Person = singleton(Person)
class Person:                        # Rebinds Person to onCall
    def __init__(self, name, hours, rete):        # onCall remembers Person
        self.name = name
        self.hours = hours
        self.rate = rate
    def pay(self):
        return self.hours * self.rate
        
@singleton                                # Spam = singleton(Spam)
class Spam:                                # Rebinds Spam to onCall
    def __init__(self, val):                # onCall remembers Spam
        self.attr = val
        
bob = Person('Bob', 40, 10)                # Really calls onCall
print(bob.name, bob.pay())

sue = Person('Sue', 50, 20)                # Same, single object
print(sue.name, sue.pay())

X = Spam(42)                                # One Person, one Spam
Y = Spam(99)
print(X.attr, Y.attr)

现在,当Person或Spam类稍后用来创建一个实例的时候,装饰器提供的包装逻辑层把实例构建调用指向了onCall,它反过来调用getInstance,以针对每个类管理并分享一个单个实例,而不管进行了多少次构建调用。这段代码的输出如下:

Bob 400
Bob 400
42 42

有趣的是,这里如果能像前面介绍的那样,使用nonlocal语句来改变封闭的作用域名称,我们这里可以编写一个更为自包含的解决方案……后面的替代方案实现了同样的效果,它为每个类使用了一个封闭作用域,而不是为每个类使用一个全局表入口:

def singleton(aClass):                        # On @ decoration
    instance = None
    def onCall(*args):                        # On instance creation
        nonlocal instance                        # 3.x and later nonlocal
        if instance == None:
            instance = aClass(*args)            # One scope per class
        return instance
    return onCall

这个版本同样地工作,但是,它不依赖于装饰器之外的全局作用域中的名称。在Python2.6或Python 3.x版本中,我们也可以用类编写一个自包含的解决方案……如下代码对每个类使用一个实例,而不是使用一个封闭作用域或全局表,并且它和其他的两个版本一样地工作(实际上,依赖于我们随后会见到的同样的编码模式时一个公用的装饰器类错误,这里我们只想要一个实例,但并不总是这样的情况):

class singleton:
    def __init__(self, aClass):                # On @ decoration
        self.aClass = aClass
        self.instance = None
    def __call__(self, *args):                # On instance creation
        if self.instance == None:
            self.instance = self.aClass(*args)    # One instance per class
        return self.instance

要让这个装饰器成为一个完全通用的工具,可将其存储在一个可导入的模块文件中,在一个__name__检查下缩进self测试代码,并且在构建调用中使用**kargs语法支持关键字参数。

跟踪对象接口

前面小节的单体示例使用类装饰器来管理一个类的所有实例。类装饰器的另一个常用场景是每个产生实例的接口。类装饰器基本上可以在实际上安装一个包装器逻辑层,来以某种方式管理对其接口的访问。

例如,__getattr__运算符重载方法作为包装嵌入的实例的整个对象接口的一种方法,以便实现委托编码模式。我们在前面介绍的管理的属性中看到过类似的例子。还记得吧,当获取为定义的属性名的时候,__getattr__会运行;我们可以使用这个钩子来拦截一个控制器类中的方法调用,并将它们传递给一个嵌入的对象。

为了便于参考,这里给出最初的非装饰器委托示例,它在两个内置类型对象上工作:

class Wrapper:
    def __init__(self, object):
        self.wrapped = object                # Save object
    def __getattr__(self, attrname):
        print('Trace:', attrname)            # Trace fetch
        return getattr(self.wrapped, attrname)    # Delegate fetch
        
>>> x = Wrapper([1, 2, 3])                # Wrap a list
>>> x.append(4)                            # Delegate to list method
Trace: append
>>> x.wrapped                            # Print my member
[1, 2, 3, 4]

>>> x = Wrapper({'a':1, 'b':2})            # Wrap a dictionary
>>> list(x.keys())                        # Delegate to dictionary method
Trace: keys                                # Use list() in 3.x
['a', 'b']

在这段代码中,Wrapper类拦截了任何包装的对象的属性访问,打印出一条跟踪信息,并且使用内置函数getattr来终止对包装对象的请求。它特别跟踪包装的对象的类之外发出的属性访问。在包装的对象内部访问其方法不会被捕获,并且会按照设计正常运行。这种整体接口模型和函数装饰器的行为不同,装饰器只包含一个特定的方法。

类装饰器为编写这种__getattr__技术来包装一个完整接口提供了一个替代的、方便的方法。例如,在Python 2.6和Python 3.x中,前面的类示例可能编写为一个类装饰器,来触发包装的实例创建,而不是把一个预产生的实例传递到包装器的构造函数中(在这里也用**kargs扩展了,以资产关键字参数,并且统计进行访问的次数):

def Tracer(aClass):                        # On @ decorator
    class Wrapper:
        def __init__(self, *args, **kargs):    # On instance creation
            self.fetches = 0
            self.wrapped = aClass(*args, **kargs)       # Use enclosing scope name
        def __getattr__(self, attrname):
            print('Trace:' + attrname)                # Catches all but own attrs
            self.fetches += 1
            return getattr(self.wrapped, attrname)    # Delegate to wrapped obj
    return Wrapper
    
@Tracer
class Spam:                                    # Spam = Tracer(Spam)
    def display(self):                        # Spam is rebound to Wrapper
        print('Spam!' * 8)
        
@Tracer
class Person:                                    # Person = Tracer(Person)
    def __init__(self, name, hours, rate):        # Wrapper remembers Person
        self.name = name
        self.hours = hours
        self.rate = rate
    def pay(self):                               # Accesses outsite class traced
        return self.hours * self.rate
        
 food = Spam()                                # Triggers Wrapper()
 food.display()                                # Triggers __getattr__
 print([food.fetches])
 
 bob = Person('Bob', 40, 50)                    # bob is really a Wrapper
 print(bob.name)                                # Wrapper embeds a Person
 print(bob.pay())
 
 print('')
 sue = Person('Sue', rate=100, hours=60)        # sue is a different Wrapper
 print(sue.name)                                # with a different Person
 print(sue.pay())
 
 print(bob.name)                                # bob has different state
 print(bob.pay())     
 print([bob.fetches, sue.fetches])                # Wrapper attr not traced

这里与我们前面在“编写函数装饰器”一节中遇到的跟踪器装饰器有很大不同,注意到这点很重要,在那里,我们看到了装饰器可以使我们跟踪和计时对一个给定函数或方法的调用。相反,通过拦截实例创建调用,这里的类装饰器允许我们跟踪整个对象接口,例如,对其任何属性的访问。

下面是这段代码子啊Python 2.6和Python 3.x下的输出:Spam和Person类的实例上的属性获取都会调用Wrapper类中的__getattr__逻辑,由于food和bob确实都是Wrapper的实例,得益于装饰器的实例创建调用重定向:

Trace:display
Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam!
[1]
Trace:name
Bob
Trace:pay
2000

Trace:name
Sue
Trace:pay
6000
Trace:name
Bob
Trace:pay
2000
[4, 2]

注意,前面的代码装饰了一个用户定义的类。就像是在前面最初的例子中一样,我们也可以使用装饰器来包装一个内置的类型,例如列表,只要我们的子类允许装饰器语法,或者手动地执行装饰……装饰器语法对于@行需要一条class语句。

在下面的代码中,由于装饰的间接作用,x实际是一个Wrapper(我吧装饰器类放倒了模块文件tracer.py中,以便以这种方式重用它):

>>> from tracer import Tracer
>>> @Tracer
... class MyList(list): pass            # MyList = Tracer(MyList)

>>> x = MyList([1, 2, 3])                # Triggers Wrapper()
>>> x.append(4)                         # Triggers __getattr__, append
Trace:append
>>> x.wrapped
[1, 2, 3, 4]

>>> WrapList = Tracer(list)                # Or perform decoration manually
>>> x = WrapList([4, 5, 6])                # Else subclass statement required
>>> x.append(7)
Trace:append
>>> x.wrapped
[4, 5, 6, 7]

这种装饰器方法允许我们把实例创建移动到装饰器自身之中,而不是要求传入一个预先生成的对象。尽管这好像是一个细小的差别,它允许我们保留常规的实例创建语法并且通常实现装饰器的所有优点。我们只需要用装饰器语法来扩展类,而不是要求所有的实例创建调用都通过一个包装器来手动地指向对象:

@Tracer                                # Decorator approach
class Person: ...
bob = Person('Bob', 40, 50)
sue = Person('Sue', rate=100, hours=60)

class Person: ...                        # Non-decorator approach
bob = Wrapper(Person('Bob', 40, 50))
sue = Wrapper(Person('Sue', rate=100, hours=60))

假设你将会产生类的多个实例,装饰器通常将会在代码大小和代码可维护性上双赢。

注意:属性版本差异:正如我们前面了解到的,在Python 2.6中,__getattr__将会拦截对__str__和__repr__这样的运算符重载方法的访问,但是在Python 3.x中不会这样。在Python 3.x中,类实例从类中继承这些方法中的一些(而不是全部)的默认形式(实际上,是从自动对象超类),因为所有的类都是“新式类”。此外,在Python 3.x中,针对打印和+这样的内置操作显示地调用属性并不会通过__getattr__(或其近亲__getattribute__)路由。 新式类在类中查找这样的方法,并且完全省略常规的实例查找。此处意味着,在Python 2.6总,基于__getattr__的跟踪包装器将会自动跟踪和传递运算符重载,但是,在Python 3.x中不会如此。要看到这一点,直接在交互式会话的前面的末尾显示“x”,在Python 2.6中,属性__repr__被跟踪并且该列表如预期的那样打印出来,但是子啊Python 3.x中,不会发生跟踪并且列表打印为Wrapper类使用一个默认显示:>>> x                       # 2.6 Trace: __repr__ [4, 5, 6, 7] >>> x                                              # 3.x <tracer.Wrapper object at 0x026c07d0>要在Python 3总同样工作,运算符重载方法通常需要在包装类中冗余的重新定义,要么手动定义,要么通过工具定义,或者通过咋超类中定义。 只有简单命名的属性会在两种版本中都同样工作。

类错误之二:保持多个实例

令人好奇的是,这个例子中的装饰器函数几乎可以编写为一个类而不是一个函数,使用正确的运算符重载协议。如下的略微简化的替代版本类似地工作,因为当@装饰器应用于类的时候,触发__init__,并且当创建了一个主体类实例的时候触发其__call__。这次,我们的对象实际是Tracer的实例,并且这里,我们实际上只是为避免使用对一个实例属性的封闭作用域引用:

class Tracer:
    def __init__(self, aClass):            # On @ decorator
        self.aClass = aClass                # Use instance atrribute
    def __call__(self, *args):            # On instance creation
        self.wrapped = self.aClass(*args)        # ONE(LAST) INSTANCE PER CLASS!
        return self
    def __getattr__(self, attrname):
        print('Trace:' + attrname)
        return getattr(slef.wrapped, attrname)
        
@Tracer
class Spam:                        # Like: Spam = Tracer(Spam)
    def display(self):
        print('Spam!' * 8)
        
...

food = Spam()                        # Triggers __call__
food.display()                        # Triggers __getattr__

正如我们在前面见到的,这个仅针对类的替代方法像前面一样处理多个类,但是,它对于一个给定的类的多个实例并不是很有效:每个实例构建调用会触发__call__,这会覆盖前面的实例。直接效果是Tracer只保存了一个实例,即最后创建的一个实例。自行体验一下看看这是如何发生的,但是,这里给出该问题的一个实例:

@Tracer
class Person:                        
    def __init__(self, name):
        self.name = name
        
bob = Person('Bob')                # bob is really a Wrapper
print(bob.name)                    # Wrapper embeds a Person
sue = Person('Sue')
print(sue.name)                    # sue overwrites bob
print(bob.name)                    # OOPS: now bob's name is "sue"!

这段代码输出如下……由于这个跟踪器只有一个共享的实例,所以第二个实例覆盖了第一个实例:

Trace: name
Bob
Trace: name
Sue
Trace: name
Sue

这里的问题是一个糟糕的状态保持……我们为每个类创建了一个装饰器实例,但是不是针对每个类实例,这样一来,只有最后一个实例保持住了。其解决方案就像我们在前面针对装饰方法的类的错误一样,在于放弃基于类的装饰器。

前面的基于函数的Tracer版本确实可用于多个实例,因为每个实例构建调用都会创建一个新的Wrapper实例,而不是覆盖一个单个的共享的Tracer实例的状态。由于同样的原因,最初的非装饰器版本正确地处理多个实例。装饰器不仅仅具有无可争辩的魔力,而且微妙的程度令人难以置信。

装饰器与管理器函数的关系

无顾这样的微妙性,Tracer类装饰器示例最终仍然是依赖于__getattr__来拦截对一个包装和嵌入实例对象的获取。正如我们在前面见到的,我们真正需要完成的是,把实例创建调用移入一个类的内部,而不是把实例传递一个管理器函数。对于最初的非装饰器跟踪实例,我们将直接有差异地编写实例创建:

class Spam:                            # Non-decorator version
    ...                                # Any class will do
food = Wrapper(Spam())                 # Special creation syntax

@Tracer
calss Spam:                            # Decorator version
    ...                                # Requires @ syntax at class
food = Spam()                          # Normal creation syntax

基本上,类装饰器把特殊语法需求从实际创建调用迁移到了类语句自身。对于本节前面的单体示例来说也是如此,我们直接将类及其构建参数传递到了一个管理器函数中,而不是装饰一个类并使用常规的实例创建调用:

instancese = {}
def getInstance(aClass, *args):
    if aClass not in instances:
        instances[aClass] = aClass(*args)
    return instances[aClass]
    
bob = getInstance(Person, 'Bob', 40, 10)            # Versus: bob = Person('Bob', 40, 10)

作为替代方案,我们可以使用Python的内省工具来从一个已准备创建好的实例来获取类(假设创建一个初始化实例是可以接受的):

instances = {}
def getInstance(object):
    aClass = object.__class__
    if aClass not in instances:
        instances[aClass] = object
    return instances[aClass]
    
bob = getInstance(Person('Bob', 40, 10))        # Versus: bob = Person('Bob', 40, 10)

这对于我们前面所编写的跟踪器那样的函数装饰器也是成立的:我们可以直接把函数及其参数传递到负责分配调用的一个管理器中,而不是用拦截随后调用的逻辑来装饰一个函数:

def func(x, y):                    # Nondecorator version
    ...                            # def tracer(func, args): ... func(*args)
result = tracer(func, (1, 2))        # Special call syntax

@tracer
def func(x, y):                    # Decorator version
    ...                            # Rebinds name: func = tracer(func)
result = func(1, 2)                # Normal call syntax

像这样的管理器函数的方法把使用特殊语法的负担放到了调用上,而不是期待在函数和类定义上使用装饰语法。

为什么使用装饰器(重访)

那么,为什么我们只是暂时不使用装饰器的方法来实现单体呢?正如我在本篇文章开始的时候提到的,装饰器暂时给我们利弊权衡。尽管语法意义重大,当面对新工具的时候,我们通常都忘了问“为什么要用”的问题。既然已经看到了装饰器实际是如何工作的,让我们花点时间在这里看看更大的问题。

就像大多数语言功能一样,装饰器也有优点和缺点。例如,从负面的角度讲,类装饰器有两个潜在的缺陷:

  • 类型修改:正如我们所见到的,当插入包装器的时候,一个装饰器函数或类不会保持其最初的类型……其名称重新绑定到一个包装器对象,在使用对象名称或测试对象类型的程序中,这可能会很重要。在单体的例子中,装饰器和管理函数的方法都为实例保持了最初的类类型;在跟踪器的代码中,没有一种方法这么做,因为需要有包装器。
  • 额外调用:通过装饰器添加一个包装层,在每次调用装饰对象的时候,会引发一次额外调用所需的额外性能成本……调用时相对耗费时间的操作,因此,装饰包装器可能会使程序变慢。在跟踪器代码中,两种方法都需要每个属性通过一个包装器层来指向;单体的示例通过保持最初的类类型而避免了额外调用。

类似的问题也适用于函数装饰器:装饰和管理器函数都会导致额外调用,并且当装饰的时候通常会发生类型变化(不装饰的时候就没有)。

也就是说,这二者都不是非常严重的问题。对于大多数程序来说,类型差异问题不可能有关系,并且额外调用对速度的影响也不显著;此外,只有当使用包装器的时候才会产生后一个问题,且这个问题常常可以忽略,因为需要优化性能的时候可以直接删除装饰器,并且添加包装逻辑的非装饰器解决方案也会导致额外调用的问题。相反,正如我们在本文开始所见到的,装饰器有3个主要优点。与前面小节的管理器(即辅助)函数解决方案相比,装饰器提供:

  • 明确的语法:装饰器使得扩展明确而显然。它们的@比可能在原文件中任何地方出现的特殊代码要容易识别,例如,在单体和跟踪器实例中,装饰器行似乎比额外代码更容易被注意到。此外,装饰器允许函数和实例创建调用使用所有的Python程序员所熟悉的常规语法。
  • 代码可维护性:装饰器避免了在每个函数或类调用中重复扩展的代码。由于它们只出现一次,在类或者函数自身的定义中,它们排除了冗余性并简化了未来的代码维护。对于我们的单体和跟踪器示例,要使用管理器函数的方法,我们需要在每次调用的时候使用特殊的代码……最初以及未来必须做出的任何修改都需要额外的工作。
  • 一致性:装饰器使得程序员忘记使用必须的包装逻辑的可能性大大减少。这主要得益于两个优点……由于装饰器是显示的并且只出现一次,出现在装饰的对象自身中,与必须包含在每次调用中的特殊代码相比较,装饰器促进了更加一致和同意的API使用。例如,在单体示例中,可能更容易忘了通过特殊代码来执行所有类创建调用,而这将会破坏单体的一致性管理。

装饰器还促进了代码的封装以减少冗余性,并使得未来的维护代价最小化。尽管其他的编码结构化工具也能做到这些,但装饰器使得这对于扩展任务来说更自然。

然而,这三个优点还不是使用装饰器语法的必须和原因,装饰器的用法最终还是一个格式选择。也就是说,大多数程序员发现了一个纯粹的好处,特别是它作为正确使用库和API的一个工具。

我还记得类中的构造函数函数的支持者和反对者也有过类似的争论……在介绍__init__方法之前,创建它的时候通过一个方法手动地运行一个实例,我那个网也能实现同样的效果(例如, X = Class().init())。然而,随着时间的流逝,尽管这基本上是一个格式的选择,但__init__语法也编程了广泛的首选,因为它更为明确、一致和维护。尽管这些该由你来决定,但装饰器似乎把很多同样的成功摆到了桌面上。


直接管理函数和类

本文中,我们的大多数示例都设计来拦截函数和实例创建调用。尽管这对于装饰器来说很典型,它们并不进行限于这一角色。因为装饰器通过装饰器代码来运行新的函数和类,从而有效地工作,它们也可以用来管理函数和类对象自身,而不只是管理对它们随后的调用。

例如,假设你需要被另一个应用程序使用的方法或类注册到一个API,以便随后处理(可能该API随后将会调用该对象,以响应事件)。尽管你可能提供一个注册函数,在对象定义之后手动地调用该函数,但装饰器使得你的意图更为明显。

这一思路如下的简单实现定义了一个装饰器,它既应用于函数应用于类,把对象添加到一个基于字典的注册中。由于它返回对象本身而不是一个包装器,所以它没有拦截随后的调用:

# Registering decorated objects to an API

registry = {}
def register(obj):                        # Both class and func decorator
    registry[obj.__name__] = obj            # Add to registry
    return obj
    
@register
def spam(x):
    return (x ** 2)                        # spam = register(spam)
    
@register
def ham(x):
    return (x ** 3)    
    
@register
class Eggs:                                # Eggs = register(Eggs)
    def __init__(self, x):
        self.data = x ** 4
    def __str__(self):
        return str(self.data)
        
print('Registry:')
for name in registry:
    print(name, '=>', registry[name], type(registry[name]))
    
print('\nManual calls:')
print(spam(2))                             # Invoke objects manually
print(ham(2))                                # Later calls not intercepted
X = Eggs(2)
print(X)

print('\nRegistry calls:')
for name in registry:
    print(name, '=>', registry[name](3))    # Invoke from registry

当运行这段代码的时候,装饰的对象按照名称添加到注册中,但当随后调用它们的时候,它们仍然按照最初的编码工作,而没有指向一个包装器层。实际上,我们的对象可以手动运行,或从注册表内部运行:

Registry:
Eggs => <class '__main__.Eggs'> <class 'type'>
ham => <function ham at 0x02CFB738> <class 'function'>
spam => <function spam at 0x02CFB6F0> <class 'function'>
Manual calls:
4
8
16
Registry calls:
Eggs => 81
ham => 27
spam => 9

例如,一个用户界面可能使用这样的技术,为用户主动注册回调处理程序。处理程序可能通过函数或类名来注册,就像这里所做的一样,或者可以使用装饰器参数来指定主体事件; 包含装饰器的一条额外的def语句可能会用来保持这样的参数以便在装饰时使用。

这个例子是仿造的,但是,其技术很通用。例如,函数装饰器也可能用来处理函数属性,并且类装饰器可能动态地插入新的类属性,或者甚至新的方法。考虑如下的函数装饰器……它们吧函数属性分配给记录信息,以便随后供一个API使用,但是,它们没有插入一个包含器层来拦截随后的调用:

# Augmenting decorated objects directly
>>> def decorate(func):
... func.marked = True # Assign function attribute for later use
... return func
...
>>> @decorate
... def spam(a, b):
... return a + b
...
>>> spam.marked
True
>>> def annotate(text):                 # Same, but value is decorator argument
... def decorate(func):
... func.label = text
... return func
... return decorate
...
>>> @annotate('spam data')
... def spam(a, b):                     # spam = annotate(...)(spam)
... return a + b
...
>>> spam(1, 2), spam.label
(3, 'spam data')

这样的装饰器直接扩展了函数和类,没有捕捉对它们的随后调用。

.

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-09-24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档