前言

在学习自动化测试框架unittestpytest的时候,发现有很多类或者函数头上会带一顶**@开头的帽子,尤其是pytest中,很多核心的用法(像参数化、夹具等)都是通过这个家伙来实现的,看起来很高端的样子,那么这玩意到底是什么呢?它就是python中的一个重难点:装饰器**。

说到装饰器,那么就必须先了解另一个概念,闭包

闭包

什么是闭包?简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行,这样的一个函数我们称之为闭包

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

直接上栗子:

1
2
3
4
5
6
7
def outer():
greetings = 'hello '
def inner(name):
return greetings + name
return inner

print(outer()('古一')) # 输出:hello 古一

上面代码的意思:定义了一个外层函数outer和一个内部函数inner;在outer函数内部,又定义了一个变量greetings并赋值;然后在内部函数inner中调用了这个变量;最后outer函数的返回就是inner函数本身。

我们将其分解来看,先调用函数outer:

1
2
print(outer())
# 输出: <function outer.<locals>.inner at 0x0000024AEF221E50>

可以看到outer返回了一个函数,其实就是它的内层函数innerouter()('古一')这一句其实就等同于inner('古一')

当我们用调试模式去走读这个闭包函数可以发现,进入outer函数内部后,当执行到def inner(name):这里后,并不会马上进入inner函数的内部,而是先执行return inner这句,即先走完外层函数outer的生命周期,然后才会开始进入inner内部,而且,虽然外层函数的生命周期已经结束,但是**内层函数inner仍然可以调用outer的局部变量greeting**!这就是闭包的特别之处。

由此,我们可以简单总结出闭包的特点:

  • 函数中嵌套了另一个函数
  • 外层函数的返回值是嵌套的内层函数
  • 内层函数对外部函数有访问(即对外部作用域有非全局变量的引用)

说了这么多闭包的东西,它跟装饰器有什么关系呢?

装饰器其实就是闭包在python中的一种经典引用,它能让python代码更简洁逼格更高,也能夹带更多“私货”。

实现装饰器

在我平时写自动化测试框架时,会自己定义一些装饰器来减少代码冗余,简化代码,如用装饰器实现日志输出等。

先看一个栗子:

1
2
def add(a, b):
return a + b

这个是一个加法函数,现在我有一个新需求:我想把传入的是什么参数以及函数的运行时间,用日志打印出来,那么直接改原函数也很简单能实现:

1
2
3
4
5
6
7
8
9
def add(a, b):
logger.info(f'传入的参数为:{a},{b}')
start_time = time.time()
res = a + b
time.sleep(1)
end_time = time.time()
run_time = end_time - start_time
logger.info(f'函数的运行时间为:{run_time}')
return res

嗯,这个扩展的功能看起来还不错,我想给减法、乘法、除法甚至更多函数也加上这个功能,那么问题来了,那么多函数我都要一一去加上这么一长串东西吗?显然不合适。另一方面,直接修改原函数,也是不符合面向对象编程原则之一:开放封闭原则的。

封闭:已经实现的功能代码对修改是封闭的。

开放:已经实现的功能代码对扩展是开放的。

那么我们怎么解决这个问题?

首先我们上面说到过闭包,我们用外函数和内函数来实现以下:

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
27
28
import time
from loguru import logger

# 外层函数
def extended(func):
# 内层被装饰的功能函数
def wrapper(a, b):
# 扩展的功能
logger.info(f'传入的参数为:{a},{b}')
start_time = time.time()
# 功能函数
res = func(a, b)
# 扩展的功能
time.sleep(1)
end_time = time.time()
run_time = end_time - start_time
logger.info(f'函数的运行时间为:{run_time}')
return res

return wrapper

# 真正的功能函数
def add(a, b):
res = a + b
return res

# 通过外函数、内函数和功能函数,实现了在不改变原功能函数的情况下的功能扩展
print(extended(add)(1, 2))

extended就是外层函数,wrapper就是内层函数,即被扩展的功能函数,只不过在这里内层函数对外层引用的变量是个函数,运行一下,看下效果:

1
2
3
| INFO     | __main__:wrapper:34 - 传入的参数为:1,2
3
| INFO | __main__:wrapper:40 - 函数的运行时间为:1.000391960144043

ok,有了这个装饰器,我们就可以对我们想要扩展的函数尽情装饰了。

but!这里还会有两个问题

1、如果要装饰的函数只有1个或者3个、4个甚至有关键字参数呢

2、我们的调用仍然很麻烦,extended(add)(1, 2)的调用方式,不容易让使用者理解我们这个函数是在做什么

针对第一个问题,其实很好解决,python中给我们提供了不定长参数*args**kwargs**,因此我们只需要做一个简单的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def extended(func):
# 内层被装饰的功能函数
def wrapper(*args, **kwargs):
# 扩展的功能
logger.info(f'传入的参数为:{args,kwargs}')
start_time = time.time()
# 功能函数
res = func(*args, **kwargs)
# 扩展的功能
time.sleep(1)
end_time = time.time()
run_time = end_time - start_time
logger.info(f'函数的运行时间为:{run_time}')
return res

return wrapper

来试试:

1
2
3
4
5
def add2(a, b, c, d=None):
res = a + b + c + d
return res

print(extended(add2)(1, 2, 3, d=4))

运行结果:

1
2
3
| INFO     | __main__:wrapper:34 - 传入的参数为:(1, 2, 3),{'d': 4}
10
| INFO | __main__:wrapper:40 - 函数的运行时间为:1.0007085800170898

通过不定长参数,让我们的装饰器更加通用了。

回到第二个问题,其实Python为了让大家写起来方便,给装饰器提供了一个语法糖,这就很pythonic了,用法如下:

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time
from loguru import logger

def extended(func):
def wrapper(*args, **kwargs):
logger.info(f'传入的参数为:{args},{kwargs}')
start_time = time.time()
res = func(*args, **kwargs)
time.sleep(1)
end_time = time.time()
run_time = end_time - start_time
logger.info(f'函数的运行时间为:{run_time}')
return res

return wrapper

# 直接戴一顶帽子
@extended
def add(a, b):
res = a + b
return res

print(add(1,2))

通过在功能函数上加@即可直接调用装饰器,这样有利于让我们把更多的注意力放在功能函数本身。

至此,我们就实现了一个基础且通用的装饰器。

带参数的装饰器

ok,新需求又来了,如果想通过装饰器传参给功能函数怎么办呢?比如我想通过从外部传入一个名字,打印是谁在调用这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
def extended(func):
def wrapper(*args, **kwargs):
# 这里的name该从哪里传进来呢?
print(f'{name}正在调用该函数')
func(*args, **kwargs)
return wrapper

# 给装饰器传入'kevin'
@extended('kevin')
def add(a, b):
res = a + b
return res

这样写肯定会报错,问题的关键在于如何去接收这个传入的参数,其实我们只需要在原来的装饰器外面再加一层用来接收外部参数即可,先上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 加了一层用来接收外部参数
def add_name(name):
def extended(func):
def wrapper(*args, **kwargs):
print(f'{name}正在调用该函数')
func(*args, **kwargs)
return wrapper
# 返回第二层函数
return extended

# 调用的装饰器不再是extended,而是add_name
@add_name('kevin') # 等同于:add = add_name('kevin')(add)
def add(a, b):
res = a + b
return res


print(add(1, 2))

我们把原来的装饰器extended看做一个整体,即一个内层函数,add_name就是它的外层函数,外层作用域中有一个变量name传入,上面闭包中讲过,内层是可以引用外部作用域的变量的,因此,在最里层的功能函数,就可以直接引用这个name。于是,通过再嵌套一层,实现了我们新增的需求。

原函数还是原函数吗?

在使用装饰器后,有一个值得注意的地方,还是上面的第一个例子,我们先打印出add()函数的一些元信息:

1
2
3
4
5
6
print(add.__name__)
# 输出: 'wrapper'
help(add)
# 输出:
# Help on function wrapper in module __main__:
# wrapper(*args, **kwargs)

我们发现,add()函数被装饰以后,它的元信息变了。元信息告诉我们“它不再是以前那个add()函数,而是被wrapper()取代了”。这是装饰器带来的一个副作用,会覆盖掉原函数的元信息。

为了解决这问题,可以通过内置的装饰器@functools.wrap解决,它能帮助保留原函数的元信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import functools

def extended(func):
@functools.wrap(func)
def wrapper(*args, **kwargs):
# 这里的name该从哪里传进来呢?
print(f'{name}正在调用该函数')
func(*args, **kwargs)
return wrapper

# 给装饰器传入'kevin'
@extended('kevin')
def add(a, b):
res = a + b
return res
print(add.__name__)
# 输出:'add'

叠加装饰器

我们不满足于只给函数扩展一个功能,想给它同时扩展不同的功能,我们可以直接在功能函数上直接叠加使用:

1
2
3
4
@decorator2
@decorator1
def func():
print('叠加装饰器')

那么当有多个装饰器时,程序的运行顺序是怎么样的呢?

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
27
28
29
30
31
def extended1(func1):
def wrapper1(*args, **kwargs):
print('我是装饰器1') # step3:开始执行装饰器1
res = func1(*args, **kwargs) # step4:此时func1=wrapper2()=add()
print('装饰器1的尾巴') # step6:继续装饰器1剩余的装饰
return res # step7: 返回res,装饰器1装饰完毕
return wrapper

def extended2(func2):
def wrapper2(*args, **kwargs):
print('我是装饰器2') # step1:先执行装饰器2
res = func2(*args, **kwargs) # step2:此时func2=add(),但此时还不会执行add()函数 本身,因为add()函数还有一个装饰器1,所以此时进入装饰器1
print('装饰器2的尾巴') # step8:继续装饰器2剩余的装饰
return res # step9:所有装饰完毕

return wrapper

@extended2
@extended1
def add(a, b): # step5:执行完功能函数
print('我是功能函数')
res = a + b
return res

print(add(1, 2))
"""
函数执行顺序:add()-->wrapper2()-->func2()-->wrapper1()-->func1()-->add()
装饰器装饰顺序:
1、@extend2:add()-->extend2()-->func2=add原函数-->add指向wrapper2
2、@extend1:wrapper2-->extend1()-->func1=wrapper2-->add指向wrapper1
"""

运行结果:

1
2
3
4
5
6
我是装饰器2
我是装饰器1
我是功能函数
装饰器1的尾巴
装饰器2的尾巴
3

总结:

装饰器执行顺序:从上往下

装饰器装饰顺序:从下往上

装饰类

上文中说到的装饰器,装饰对象都是函数,那么能否装饰类呢?答案是可以的。

假如我们要给不同的类增加一个属性,

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def extended(cls):
cls.name = 'kevin'
cls.age = 18
return cls

@extended
class A:
pass

@extended
class B:
pass

print(A.name) # 输出kevin
print(A.age) # 输出18
print(B.name) # 输出kevin
print(B.age) # 输出18

方法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def extended(cls):
def wrapper(*args, **kwargs):
cls.name = 'Annie'
cls.age = 18
return cls(*args, **kwargs) # 返回的是A(),即对象

return wrapper


@extended # A = extended(A)
class A:
pass

print(A()) # 类型为对象
print(A().name) # Annie
print(A().age) # 18

类装饰器

在python中有三个内置装饰器:@classmethod@staticmethod@property,查看这三个装饰器源码可以发现:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class classmethod(object):
"""
classmethod(function) -> method

Convert a function to be a class method.

A class method receives the class as implicit first argument,
just like an instance method receives the instance.
To declare a class method, use this idiom:

class C:
@classmethod
def f(cls, arg1, arg2, ...):
...

It can be called either on the class (e.g. C.f()) or on an instance
(e.g. C().f()). The instance is ignored except for its class.
If a class method is called for a derived class, the derived class
object is passed as the implied first argument.

Class methods are different than C++ or Java static methods.
If you want those, see the staticmethod builtin.
"""
def __get__(self, *args, **kwargs): # real signature unknown
""" Return an attribute of instance, which is of type owner. """
pass

def __init__(self, function): # real signature unknown; restored from __doc__
pass

@staticmethod # known case of __new__
def __new__(*args, **kwargs): # real signature unknown
""" Create and return a new object. See help(type) for accurate signature. """
pass

__func__ = property(lambda self: object(), lambda self, v: None, lambda self: None) # default

__isabstractmethod__ = property(lambda self: object(), lambda self, v: None, lambda self: None) # default


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
27
28
29
30
31
32
33
34
35
36
37
class staticmethod(object):
"""
staticmethod(function) -> method

Convert a function to be a static method.

A static method does not receive an implicit first argument.
To declare a static method, use this idiom:

class C:
@staticmethod
def f(arg1, arg2, ...):
...

It can be called either on the class (e.g. C.f()) or on an instance
(e.g. C().f()). Both the class and the instance are ignored, and
neither is passed implicitly as the first argument to the method.

Static methods in Python are similar to those found in Java or C++.
For a more advanced concept, see the classmethod builtin.
"""
def __get__(self, *args, **kwargs): # real signature unknown
""" Return an attribute of instance, which is of type owner. """
pass

def __init__(self, function): # real signature unknown; restored from __doc__
pass

@staticmethod # known case of __new__
def __new__(*args, **kwargs): # real signature unknown
""" Create and return a new object. See help(type) for accurate signature. """
pass

__func__ = property(lambda self: object(), lambda self, v: None, lambda self: None) # default

__isabstractmethod__ = property(lambda self: object(), lambda self, v: None, lambda self: None) # default

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class property(object):
"""
Property attribute.

fget
function to be used for getting an attribute value
fset
function to be used for setting an attribute value
fdel
function to be used for del'ing an attribute
doc
docstring

Typical use is to define a managed attribute x:

class C(object):
def getx(self): return self._x
def setx(self, value): self._x = value
def delx(self): del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")

Decorators make defining new properties or modifying existing ones easy:

class C(object):
@property
def x(self):
"I am the 'x' property."
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
"""
def deleter(self, *args, **kwargs): # real signature unknown
""" Descriptor to change the deleter on a property. """
pass

def getter(self, *args, **kwargs): # real signature unknown
""" Descriptor to change the getter on a property. """
pass

def setter(self, *args, **kwargs): # real signature unknown
""" Descriptor to change the setter on a property. """
pass

def __delete__(self, *args, **kwargs): # real signature unknown
""" Delete an attribute of instance. """
pass

def __getattribute__(self, *args, **kwargs): # real signature unknown
""" Return getattr(self, name). """
pass

def __get__(self, *args, **kwargs): # real signature unknown
""" Return an attribute of instance, which is of type owner. """
pass

def __init__(self, fget=None, fset=None, fdel=None, doc=None): # known special case of property.__init__
"""
Property attribute.

fget
function to be used for getting an attribute value
fset
function to be used for setting an attribute value
fdel
function to be used for del'ing an attribute
doc
docstring

Typical use is to define a managed attribute x:

class C(object):
def getx(self): return self._x
def setx(self, value): self._x = value
def delx(self): del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")

Decorators make defining new properties or modifying existing ones easy:

class C(object):
@property
def x(self):
"I am the 'x' property."
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
# (copied from class doc)
"""
pass

@staticmethod # known case of __new__
def __new__(*args, **kwargs): # real signature unknown
""" Create and return a new object. See help(type) for accurate signature. """
pass

def __set__(self, *args, **kwargs): # real signature unknown
""" Set an attribute of instance to value. """
pass

fdel = property(lambda self: object(), lambda self, v: None, lambda self: None) # default

fget = property(lambda self: object(), lambda self, v: None, lambda self: None) # default

fset = property(lambda self: object(), lambda self, v: None, lambda self: None) # default

__isabstractmethod__ = property(lambda self: object(), lambda self, v: None, lambda self: None) # default

其实这三个装饰器,都不是函数实现而是用类实现的。

如果要用类实现装饰器,会涉及到一个魔术方法__call__,先看看这个方法的作用:

1
2
3
4
5
6
class A:
def __init__(self):
print('这是实例初始化方法')

m = A() # 输出:这是实例初始化方法
m() # 报错:TypeError: 'A' object is not callable A的对象不可调用

由上可知,当给一个实例对象加上(),它是无法被调用的,如果想让它能像函数一样被调用,就必须加上__call__方法:

1
2
3
4
5
6
7
8
9
class A:
def __init__(self):
print('这是实例初始化方法')

def __call__(self, *args, **kwargs):
print('执行__call__')

m = A() # 输出:这是实例初始化方法
m() # 输出:执行__call__

所以,当实例对象加上()后,就会自动触发__call__方法。

那么有了这个魔术方法的加持,我们就可以用类去实现装饰器了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class decorator:
def __init__(self, func): # func = demo
print('开始实例化对象')
self.func = func # self.demo = demo

def __call__(self, *args, **kwargs):
print('装饰器扩展的功能1')
self.func() # self.demo()
print('装饰器扩展的功能2')

@decorator # demo = decorator(demo)
def demo():
print('执行功能函数')

demo()

首先,我们创建了一个类decorator,它的初始化方法中会传入一个参数func,并将这个参数设置为实例属性self.func,当函数demo()执行时,会先进行装饰@decorator,即执行__init__,此时demo = decorator(demo),即demo现在是一个decorator类的实例对象,又由于给demo对象加了括号,即调用实例对象,那么此时__call__方法就会触发,先执行扩展功能1,再执行原函数,最后执行扩展功能2,所以最后得到的输出是:

1
2
3
4
开始实例化对象
装饰器扩展的功能1
执行功能函数
装饰器扩展的功能2

总结

python中的装饰器无非就是以上几类,所谓装饰器,其实就是通过装饰器函数(或类),来修改原函数(或类)的一些功能,使得原函数(或类)不需要修改。

Decorators is to modify the behavior of the function through a wrapper so we don’t have to actually modify the function.