python decorator(装饰器)

Python的装饰器语法是一种语法糖. 虽然没有它也能写代码, 但这东西既有助于书写时候减少代码量, 也有助于代码阅读. 有时候甚至让我觉得Python比其他动态语言更接近于自然语言的主要原因之一是因为有装饰器的存在. 装饰器在程序语言中的作用类似于自然语言的定语和状语, 不影响句子主干, 但能使原来简单的句子富于变化, 更凝练的表达更多的信息.

这篇文章目标是要把所有跟装饰器相关的知识一次性讲清楚, 目标是即便是python初学者通过本文就可以完全理解装饰器. 本文语法基于python3, 代码在python3.9上测试通过.

预备知识

在学习装饰器之前也需要理解一些Python的基础知识.

  1. 要知道并且理解python里万物皆对象. 也了解对象可以具有自己的属性(attribute).
  2. 了解表达式(expression)和语句(statement)的区别.
  3. 可调用对象(callable objects)就是能做调用(call)这个动作的对象. 函数, 实例方法, 类, 具有__call__方法的类实例都属于可调用对象. 调用(call)这个动作在python里意味着以某些对象作为输入(input)参数执行一段代码, 并返回其他一个或多个对象.
  4. 名字(name)在python里仅仅是识别对象的一个标签(或者叫标识符identifier), 一个对象可以有多个标签, 一个标签在不同时间也可以被贴在不同对象上.
  5. python里的函数定义语法
    1
    2
    def myfun():
    pass
    相当于让解释器生成一个函数对象, 然后给他起了个myfun的名字. 如果再对myfun赋值:
    1
    myfun = other_object
    相当于让myfun这个名字(标签)指向(贴到)别的对象上.
  6. 函数体内部也可以定义别的函数对象.
  7. 函数对象可以作为参数传入别的可调用对象的调用, 或者作为返回值从别的调用传出.

装饰器是如何工作的

先看看decorator(装饰器)在官方文档中的定义:

A function returning another function, usually applied as a function transformation using the @wrapper syntax. Common examples for decorators are classmethod() and staticmethod().

The decorator syntax is merely syntactic sugar, the following two function definitions are semantically equivalent:

1
2
3
4
5
6
7
def f(...):
...
f = staticmethod(f)

@staticmethod
def f(...):
...

The same concept exists for classes, but is less commonly used there.

这个定义有点粗糙, 不精确, 不完整, 也不好理解. 这不是一个好的定义. 下面我自己尝试解释一下.

首先装饰器的句法就是在函数定义语句之前或者类定义语句之前的相邻的行上书写@expression(@符号+表达式+换行符).

为了强调@后是个表达式(expression), 我将其称作装饰器表达式, 对这个表达式的要求有如下几条规则, 在本文中我称其为装饰器协议(非官方名称, 我自己取的名字):

  1. 该表达式的解析(evaluation)必须是一个可调用对象(callable object).
  2. 这个可调用对象能够接受被修饰的对象(紧挨着装饰器后面的函数,类,方法)作为唯一的输入参数.
  3. 这个可调用对象应用调用运算后应该返回一个跟被修饰对象相同类型的对象.(非必须, 但是如果不这样, 装饰器行为难以理解和阅读.)

在本文中, 根据被修饰的对象, 我把装饰器分为函数装饰器(用来装饰函数和方法)和类装饰器(用来装饰类). 这种说法其实有些歧义, 很多文章把装饰器表达式是函数还是类来作装饰器分类标准. 但实际上装饰器表达式可以是任意表达式, 只要满足上面装饰器协议. 官方PEP-3129明确给出"Class Decorator的说法". 但事实上, 分类不重要, 重要的是明白其如何工作.

以函数装饰器为例, 看起来是这样

1
2
3
@deco_exp
def myfun(...)
...

这个被翻译器解释为如下的赋值语句:

1
2
3
def myfun(...)
...
myfun = deco_exp(myfun)

我们分析一下解释后的语句干了什么, 这其实就是

  1. 首先由函数定义语句, 生成了一个函数对象, 并且用名字myfun指向这个对象,
  2. 然后对装饰器表达式deco_exp进行解析(evaluate), 其返回值是一个可调用对象.
  3. 紧接着对这个可调用对象使用调用表达式, 调用的返回值与被修饰的函数同一类型, 也是个函数.
  4. 根据赋值语句, 让原来的名字myfun指向了前面返回的函数对象(可以是新对象也可以是原来的对象).

从这个分析可以看出前面两种等价的表达里, 装饰器方式, 比后面的复制方式精炼得多.

通过这种解释, 我们也能知道对表达式的解析这个动作是跟函数定义同层进行的, 所以如果在这一个时刻, 表达式依赖得某些变量并没有定义或者没有获得正确的赋值, 装饰器不会正常工作.

最后我们大体上能理解为什么这种语法叫装饰器, 通常来讲装饰器不会改变被修饰对象的行为, 而是做一些辅助工作, 最后一步使myfun还指向一个同样功能, 但是已经添加了辅助工作的同样类型的对象. 如果你没有这么做, 调用后返回了完全不同的对象, 表面上看起来就是装饰器使被装饰的对象的行为完全变化, 这不是装饰器这种语法被设计的本意.

装饰器表达式

常用的装饰器表达式一般有如下几种可能:

  1. 函数名
  2. 函数调用
  3. 类名
  4. 实例名
  5. 类方法

下面我们举一些例子来分别对各种方式的装饰器进行展示.

函数名做装饰器

用函数名做装饰器表达式, 是最简单也是最常见的用法. 比如这个函数:

1
2
3
def add_attrib(f):
f.__tmp_attrib__ = True
return f

这个函数给对象添加了一个属性后将其返回. 如果用函数名字做装饰器. 比如:

1
2
3
4
5
6
7
8
9
10
11
>>> @add_attrib
... def myfun():
... pass
...
>>> myfun
<function myfun at 0x0000000002A551F0>
>>> type(myfun)
<class 'function'>
>>> myfun.__tmp_attrib__
True
>>>
我们可以看到, myfun被添加了一个属性. 这个装饰器等价于:
1
2
3
def myfun():
pass
myfun = add_attrib(myfun)
之所以说以函数名做装饰器表达式简单, 是因为这里跳过了表达式解析(evaluation)的阶段, 直接进行了函数调用. 理解起来更简单.

前面这个例子比较特殊的地方还在于其返回的是输入对象, 所以也可以用来做类装饰器. 比如:

1
2
3
4
5
6
7
8
9
10
11
12
>>> @add_attrib
... class MyClass:
... pass
...
>>> MyClass
<class '__main__.MyClass'>
>>> MyClass.__tmp_attrib__
True
>>> myobj = MyClass()
>>> myobj.__tmp_attrib__
True
>>>

这个装饰器等价于:

1
2
3
class MyClass():
pass
MyClass = add_attrib(MyClass)

因为类装饰器非常少见, 后面也为了缩短篇幅, 就不再举类装饰器的例子.

我们看一个返回的不是原对象的例子:

1
2
3
4
5
def twice(f):
def inner(*args, **kwargs):
return f(*args, **kwargs) + f(*args, **kwargs)
return inner

以twice这个函数名作为装饰器表达式, 其作用是把被装饰器修饰的函数执行两遍, 然后结果相加. 比如:

1
2
3
4
5
6
7
8
9
10
11
12
>>> @twice
... def concat(*arg):
... return ''.join(arg)
...
>>> @twice
... def sum(n):
... return n*(1+n)//2
...
>>> concat('a','b','c')
'abcabc'
>>> sum(10)
110

仔细分析一下这个twice函数, 其返回一个函数对象inner. 所以这个等价的赋值语句:

1
concat = twice(concat)
使得concat指向名为inner的一个函数对象. 这个内部的函数对象将"被修饰的函数"调用了两遍并相加. 在代码阅读上修饰器的方式明显比赋值的方式可读性更好. 但这里也有一个不和谐的地方. 我们被修饰的对象的属性因为这个赋值发生了一些变化.
1
2
3
4
5
6
7
8
>>> concat.__name__
'inner'
>>> sum.__name__
'inner'
>>> concat
<function twice.<locals>.inner at 0x0000000002A55670>
>>> sum
<function twice.<locals>.inner at 0x0000000002A554C0>
这种不和谐就好比是自然语言中我们给句子加个状语, 结果动词也还要加个词缀一样难受. python给我们提供了一个解决方案来克服这个问题. 给inner前面加一个装饰器:

1
2
3
4
5
6
import functools
def twice(f):
@functools.wraps(f)
def inner(*args, **kwargs):
return f(*args, **kwargs) + f(*args, **kwargs)
return inner

这个时候再测试一下concat:

1
2
3
4
5
6
7
8
9
10
11
 >>> @twice
... def concat(*arg):
... return ''.join(arg)
...
>>> concat
<function concat at 0x00000000006C2D30>
>>> concat.__name__
'concat'
>>> concat('a', 'b', 'c')
'abcabc'
>>>
这样装饰器看起来就像是真的在"装饰"后面的函数, 而不是修改了被装饰的函数.

有两个python内置(built-in)的装饰器, classmethodstaticmethod, 用来帮助用户生命类方法和静态方法.

函数调用做装饰器

前面看到的@functools.wraps(f)装饰器实际上就是以函数调用作为装饰器. 我们看一个例子:

我们改写一下add_attrib这个函数, 让其输入参数可以作为属性的值.

1
2
3
4
5
def add_attrib(value):
def inner(f):
f.__tmp_attrib__ = value
return f
return inner
这时, 下面这个装饰器
1
2
3
@add_attrib(False)
def concat(*arg):
return ''.join(arg)
等价于
1
2
3
def concat(*arg):
return ''.join(arg)
concat = add_attrib(False)(concat)
这里先对add_attrib(False)解析, 得到一个inner的函数对象. 再对这个函数对象实施调用操作, 类似于inner(concat), 返回一个加了__tmp_attrib__属性的concat函数对象.

从被修饰的对象的角度看, 这就像是装饰器带了参数, 所以很多教程也把这个叫做带参数的装饰器.

类名做装饰器

如果以类名做装饰器, 那么下面的等效语句中

1
myfun = deco_exp(myfun)
deco_exp是类名, 复制后myfun将指向类的一个类实例. 如果被修饰的对象(myfun原来指向的对象)是函数, 类应该实现一个__call__方法来使这个类实例成为可调用对象, 这样用户在对myfun进行调用操作时, 就不会感觉到有什么不同.

举个例子, 下面这个Timer类, 帮助用户记录从函数声明到函数调用的时间和函数的执行时间.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time
class Timer:
def __init__(self, func):
self.func = func
self.decl_start = time.time()
def __call__(self, *args, **kwargs):
call_start = time.time()
r = self.func(*args, **kwargs)
call_end= time.time()
print("Decl: {} secs".format(round(call_start - self.decl_start,2)))
print("Exec: {} secs".format(round(call_end - call_start,2)))
return r

@Timer
def myfun(delay):
time.sleep(delay)
结果看起来是这样
1
2
3
4
5
6
>>> myfun(2)
Decl: 4.1 secs
Exec: 2.0 secs
>>> myfun(3)
Decl: 9.3 secs
Exec: 3.01 secs

类实例做装饰器

改造一下前面的Timer类, 使其可以根据不同的参数输出不同的时间.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time
import functools
class Timer:
def __init__(self, timer_type):
self.timer_type = timer_type
self.decl_start = time.time()
def __call__(self, func):
@functools.wraps(func)
def inner(*args, **kwargs):
call_start = time.time()
r = func(*args, **kwargs)
call_end= time.time()
if self.timer_type == 'decl':
print("Decl: {} secs".format(round(call_start - self.decl_start,2)))
else:
print("Exec: {} secs".format(round(call_end - call_start,2)))
return r
return inner

@Timer('decl')
def myfun(delay):
time.sleep(delay)

@Timer('exec')
def myfun2(delay):
time.sleep(delay)

效果如下:

1
2
3
4
5
6
7
8
>>> myfun(2)
Decl: 5.63 secs
>>> myfun2(3)
Exec: 3.01 secs
>>> myfun
<function myfun at 0x00000000006F1E50>
>>> myfun2
<function myfun2 at 0x00000000006F1F70>

分析一下其等效的赋值表达式.

1
myfun = Timer('decl')(myfun)
Timer('decl')先被解析, 返回一个类实例对象, 然后使该实例做调用行为, obj.__call__被调用, 返回一个内部的临时函数, 这个函数由于有@functools.wraps装饰器加持, 使myfun名字看起来还是原来定义的原来定义的原来定义的原来定义的原来定义的原来定义的原来定义的原来定义的myfun函数. 但实际上新的myfun函数对象会根据类实例的timer_type属性打印不通的时间.

类方法做装饰器

类方法也可以做装饰器, 时函数名做装饰器的一个特例. 通常装饰的对象也是类方法(不绝对). 例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A :
def bang(func) :
def inner(self, *args, **kwargs) :
return func(self, *args, **kwargs) + '!'
return inner

@bang
def bar(self, word) :
return word

class B() :
@A.bang
def foo(self, word) :
return word + word

a = A()
b = B()
print(a.bar('Python'))
print(b.foo('Python'))
会打印出结果
1
2
Python!
PythonPython!

当被装饰对象是类方法时, 最后方法依然会被bind到类上, 所以装饰器表达式被调用之后的返回值(也就是装饰器协议里第三步)是一个可调用对象, 并且第一个参数期望得到类实例(self).

这个装饰器在类定义语法以外使用的话会很有可能有不一样的结果. 需要特别注意.

多个装饰器

python允许给函数或者类前面添加多个装饰器, 原理就是嵌套使用等价赋值表达式. 比如我们把两个装饰器连起来用.

1
2
3
4
@add_attrib
@twice
def concat(*arg):
return ''.join(arg)

这个等价于如下赋值语句,

1
concat = add_attrib(twice(concat))
其结果如下
1
2
3
4
5
6
>>> concat
<function concat at 0x0000000002A55670>
>>> concat.__tmp_attrib__
True
>>> concat('a', 'b', 'c')
'abcabc'

再举例一个有变量的

1
2
3
4
5
@Timer('exec')
@add_attrib(False)
@twice
def concat(*arg):
return ''.join(arg)
等价于
1
2
3
def concat(*arg):
return ''.join(arg)
concat = Timer('exec')(add_attrib(False)(twice(concat)))

等价于下面的解析顺序

1
2
3
4
5
6
timer_obj = Timer('exec')
add_attrib_inner = add_attrib(False)
twice_inner = twice(concat)
twice_inner = add_attrib_inner(twice_inner)
timer_inner = timer_obj(twice_inner)
concat = timer_inner

这里就更能一目了然的看出来装饰器在可读性上给python提供了多大的帮助.

总结

有关装饰器的知识知道这么多就基本足够了. 虽然装饰器@后面的表达式还可以是任意复杂的表达式. 但过于复杂了也就不符合装饰器语法产生的初衷了.

如果想知道的更多, 可以参考

有一个很好的练习就是看看functools.wraps这个装饰器为什么可以使被装饰的对象看起来跟原来一样.