0x00 装饰器(decorator)介绍

可以查看官方文档对其的介绍:

返回值为另一个函数的函数,通常使用 @wrapper 语法形式来进行函数变换。 装饰器的常见例子包括 classmethod()staticmethod()

装饰器语法只是一种语法糖,以下两个函数定义在语义上完全等价:

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

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

同样的概念也适用于类,但通常较少这样使用。有关装饰器的详情可参见 函数定义类定义 的文档。

python装饰器简单来说就是用一切皆对象和代码复用的思想去修改一个可调用对象的功能的函数,以达到让代码更加简洁的效果。这个功能也体现了python简洁 高效的设计哲学。

0x01 一切皆对象

python中的函数是一个函数对象.
python内存空间的存储特点:python右值对应一个内存空间,而变量名为这片内存的引用(用C++来理解的话,右值为对象,左值为这个对象的引用)。

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
In [1]: def foo():
...: "this is test function"
...: return "Hello world"
...:

In [2]: print(foo())
Hello world

In [3]: print(foo) # foo指向的是一个函数对象
<function foo at 0x7f7528a135b0>

In [4]: a = foo # 将foo指向内存中的函数对象赋值给一个变量a

In [5]: print(a()) # 这里a就指向了内存中的函数对象
Hello world

In [6]: print(a) # 和foo执行同一个地址
<function foo at 0x7f7528a135b0>

In [7]: del foo # 解除引用(引用计数器减一),这里涉及到了python垃圾回收的机制,python内部会有一个引用计数器,当这个计数器归0时才会适机回收内存中的函数对象

In [8]: print(foo) # 可以看见这里报错NameError
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-8-699c1a78ee01> in <module>
----> 1 print(foo)

NameError: name 'foo' is not defined

In [9]: print(a()) # 而a仍然引用内存中的函数对象,所以仍然可以使用
Hello world

0x02 闭包的概念

由于python语法的灵活性,可以在一个函数A中的函数体中在定义另一个函数B,其实就是在函数体内创建了一个函数对象,理所当然的可以将这个函数对象返回出去,而闭包就是在个这函数B的函数体中使用了函数A中的变量并且函数A最后把函数B返回了出去。
看看下面简单的例子就是一个闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
In [10]: def A(x):
...: def B(y):
...: return x+y
...: return B
...:
...:

In [11]: print(f"{A}\n{A(1)}") # 可以发现B是A中的函数对象
<function A at 0x7f7528136200>
<function A.<locals>.B at 0x7f75281353f0>

In [12]: print(A(1)(2))
3

In [13]: C = A(5) # 这里初始化一个x为5

In [14]: for i in range(3): # y = 0 1 2
...: print(C(i))
...:
5
6
7

0x03 装饰器的简单使用

查看一下官方文档中对于在函数定义中使用decorator的描述:

一个函数定义可以被一个或多个 decorator 表达式所包装。 当函数被定义时将在包含该函数定义的作用域中对装饰器表达式求值。 求值结果必须是一个可调用对象,它会以该函数对象作为唯一参数被发起调用。 其返回值将被绑定到函数名称而非函数对象。 多个装饰器会以嵌套方式被应用。 例如以下代码

1
2
3
@f1(arg)
@f2
def func(): pass

大致等价于

1
2
def func(): pass
func = f1(arg)(f2(func))

不同之处在于原始函数并不会被临时绑定到名称 func

总接一下就是:

  1. @后面的变量的右值必须是一个可调用对象
  2. 作为decorator的函数对象(要被@的对象)有且仅有一个参数,被修饰的函数(被@变量修饰)就无所谓了。

e.g. 简单使用一下

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
In [1]: def decorator(fn):  # 在函数定义的时候做些事
...: print("start....")
...: fn()
...: print("end....")
...: return 1 # 这是只是测试用一下,在实际开发以debug为目的时这里也可以返回fn, 这样不会影响fn功能,只是加一些debug语句
...:

In [2]: @decorator
...: def foo(): # 解释器执行完之后直接就输出了,且foo就变成了1
...: print("hello world")
...: # 相当于定义foo之后,foo = decorator(foo)
start....
hello world
end....

In [3]: print(f"{type(foo)}-[{foo}] ---value {foo}") # 可以看见这里的输出结果foo的右值已经变成了1
<class 'int'>-[1] ---value 1

In [4]: def decorator(fn): # 一个简单的用法
...: def addsomething():
...: print("start....")
...: fn()
...: print("end....")
...: return addsomething
...:

In [5]: @decorator
...: def foo(): # 解释器执行完这里之后fn就变成了修饰器内部函数对象了
...: print("hello world")
...: # 相当于定义foo之后,foo = decorator(foo)

In [7]: print(f"{type(foo)}-[{foo}]") # 看这里的输出结果
<class 'function'>-[<function decorator.<locals>.addsomething at 0x7fe074357c70>]

In [8]: foo() # 调用时才输出
start....
hello world
end....

所以说修饰器本质就是将多个要动态重用的代码封装到一个装饰器中来让python解释器给一个被修饰的函数加上这些重用的代码。

但这时可能有dio大的要问了要是被修饰的函数需要参数怎么办?
很简单,在修饰器闭包的函数定义的参数中加上不定长参数*args, **kwargs, 这样就可以传然后参数调用了,具体可以见官方文档, 这里就不展开讲了。看下面的例子

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
In [9]: def decorator(fn):
...: def addsomething(*args, **kwargs):
...: print("start....")
...: fn(*args, **kwargs) # 传参数使用
...: print("end....")
...: return addsomething
...:

In [10]: @decorator
...: def foo(name, country="CN"):
...: print(f"I'm {name}, from {country}")
...:
...:

In [11]: foo() # 尝试不传入参数
start.... # 这里可以看出python解释型语言的特点
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-11-c19b6d9633cf> in <module>
----> 1 foo()

<ipython-input-9-03a692c62082> in addsomething(*args, **kwargs)
2 def addsomething(*args, **kwargs):
3 print("start....")
----> 4 fn(*args, **kwargs) # 这里相当于调用了fn() 所以报错了
5 print("end....")
6 return addsomething

TypeError: foo() missing 1 required positional argument: 'name'

In [12]: foo("wakaka")
start....
I'm wakaka, from CN
end....

0x04 一些问题

可能有些细心的朋友发现了,在上面的使用方式中,经过修饰器修饰过后的函数已经变成了修饰器内的闭包函数,即函数名和注释文档都变成了别人的形状,这好吗?这不好,纯爱战士狂怒,那么不想被替换怎么办?

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
In [9]: def decorator(fn):
...: def addsomething(*args, **kwargs):
...: print("start....")
...: fn(*args, **kwargs) # 传参数使用
...: print("end....")
...: return addsomething
...:

In [18]: def foo(name: str, country: str = 'CN') -> None: # 加了文档字符串和参数注释
...: """
...: this is simple test demo
...: """
...: print(f"I'm {name}, from {country}")
...:

In [19]: print(f"{foo}\n\tdocument: {foo.__doc__}\n\t annotations: {foo.__annotations__}") # 见输出 都有
<function foo at 0x7fe07425e050>
document:
this is simple test demo

annotations: {'name': <class 'str'>, 'country': <class 'str'>, 'return': None}

In [20]: @decorator
...: def foo(name: str, country: str = 'CN') -> None: # 修饰过后的foo
...: """
...: this is simple test demo
...: """
...: print(f"I'm {name}, from {country}")
...:
...:

In [21]: print(f"{foo}\n\tdocument: {foo.__doc__}\n\t annotations: {foo.__annotations__}") # 见输出 注释都无了 函数名也变了
<function decorator.<locals>.addsomething at 0x7fe06d93bf40>
document: None
annotations: {}

幸好的是,python给我们提供了functools.wraps函数来解决这个问题, 查看官方文档. 修改上面例子来使用它

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
In [22]: from functools import wraps

In [23]: def decorator(fn):
...: @wraps(fn)
...: def addsomething(*args, **kwargs):
...: print("start....")
...: fn(*args, **kwargs)
...: print("end....")
...: return addsomething
...:

In [24]: @decorator
...: def foo(name: str, country: str = 'CN') -> None:
...: """
...: this is simple test demo
...: """
...: print(f"I'm {name}, from {country}")
...:
...:

In [25]: print(f"{foo}\n\twhoami: {foo.__name__}\n\tdocument: {foo.__doc__}\n\t annotations: {foo.__annotations__}")
# 可以发现这里的文档字符都输出了,达到了我们想要的效果
<function foo at 0x7fe06da4d1b0>
whoami: foo
document:
this is simple test demo

annotations: {'name': <class 'str'>, 'country': <class 'str'>, 'return': None}

这里懒得去看源码了,可以简单的推测一下functools.wraps 装饰器内部是将fn函数的一些属性(如__name__、__annotations__、__doc__、...)取出来然后修改闭包函数的属性然后在修饰函数的时候就达到效果了

0x05 带参数的修饰器

这里说的带参数的修饰器指的@后面的变量传参了,如@functools.wraps(fn), 但其@后面的变量本质还是一个可调用对象. 在我看来这种方式像有一个修饰器工厂一样,根据传入的材料不同而去制作不同的修饰器
下面来使用一下这种方式
e.g. 用修饰器来制定日志输出

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
In [2]: from functools import wraps

In [7]: def logout(logfile='test.log'):
...: def log_decorator(fn):
...: from time import ctime
...: @wraps(fn)
...: def logging(*args, **kwargs):
...: msg = f"[{ctime()}] {fn.__name__} start...."
...: res = fn(*args, **kwargs)
...: msg += f"\n[{ctime()}] {fn.__name__} end...."
...: print(msg)
...: # 打开并写入日志
...: with open(logfile, 'a') as f:
...: f.write(msg+'\n')
...: return res
...: return logging
...: return log_decorator
...:

In [8]: @logout()
...: def foo():
...: pass
...:

In [9]: @logout('foo2.log')
...: def foo2():
...: pass
...:

In [10]: foo()
[Mon Jan 17 18:52:55 2022] foo start....
[Mon Jan 17 18:52:55 2022] foo end....

In [11]: foo2()
[Mon Jan 17 18:52:59 2022] foo2 start....
[Mon Jan 17 18:52:59 2022] foo2 end....

查看一下当前目录下的文件

1
2
3
decorator.md
foo2.log
test.log

没毛病(^_^)

0x06 修饰器类

修饰器也可以修饰类,这种用法和修饰函数差不多,参考官方文档.
但本节要说的不是这个,而是利用python的魔术方法__call__实现一个修饰器的类.
__call__是函数调用其实可以代表(), 是函数调用的时候用的, 举个简单的例子

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
In [1]: def foo():
...: print("hi")
...:

In [2]: foo()
hi

In [3]: foo.__call__()
hi

In [4]: class A:
...: def __call__(self): # 用c++ 来理解的话, 就是重载类实例方法的()运算符
...: print("hello")
...:

In [5]: a = A()

In [6]: a()
hello

In [7]: A.__call__(a)
hello

In [8]: a.__call__()
hello

将0x05的例子用修饰器类的方式改写一下
e.g. 直接调用方式

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
#!/usr/bin/python
# encoding: utf-8

from time import ctime

class logging(object):
log_file = 'test.log'

def __init__(self, func):
self.func = func
self.msg_s = "[{0}] {1} start...\n"
self.msg_e = "[{0}] {1} end...\n"

def __call__(self, *args, **kwars):
msg = self.msg_s.format(ctime(), self.func.__name__)
res = self.func(*args, **kwars)

msg += self.msg_e.format(ctime(), self.func.__name__)
print(msg)

with open(self.log_file, "a") as f:
f.write(msg)

return res


@logging
def foo():
pass
# foo = logging(foo) # 返回的是logging类的一个实例对象

@logging
def foo2():
pass
# foo2 = logging(foo2) # 返回的是logging类的一个实例对象


if __name__ == '__main__':
print(f"{type(foo)} {foo}")
foo() # 相当于 logging.__call__(foo)
logging.log_file = 'foo2.log' # 用的时候修改需要记录的日志文件名
foo2() # 相当于 logging.__call__(foo2)

运行结果

1
2
3
4
5
6
<class '__main__.logging'> <__main__.logging object at 0x7fa03da8bfd0>
[Mon Jan 17 20:14:36 2022] foo start...
[Mon Jan 17 20:14:36 2022] foo end...

[Mon Jan 17 20:14:36 2022] foo2 start...
[Mon Jan 17 20:14:36 2022] foo2 end...

可以看见被修饰后的函数变成了logging类的实例方法, 且这种方式使用functools.wraps的修饰方式也差不多, 只要理解了decorator的本质, 这里的结果就不难理解.

e.g. 带参数方式的修饰器类

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
#!/usr/bin/python
# encoding: utf-8

from time import ctime
from functools import wraps

class logout(object):

def __init__(self, logfile = 'test.log'):
self.logfile = logfile
self.msg_s = "[{0}] {1} start...\n"
self.msg_e = "[{0}] {1} end...\n"

def __call__(self, func):

@wraps(func)
def logging(*args, **kwargs):
msg = self.msg_s.format(ctime(), func.__name__)
res = func(*args, **kwargs)

msg += self.msg_e.format(ctime(), func.__name__)
print(msg)

with open(self.logfile, "a") as f:
f.write(msg)

return res
return logging


@logout()
def foo():
pass
# foo = logout()(foo) # 返回的是logout.__call__中的一个闭包函数

@logout('foo2.log')
def foo2():
pass
# foo2 = logout('foo2.log')(foo2) # 返回的是logout.__call__中的一个闭包函数


if __name__ == '__main__':
print(f"{type(foo)} {foo}")
foo()
foo2()

运行结果:

1
2
3
4
5
6
<class 'function'> <function foo at 0x7fcd736c3f40>
[Mon Jan 17 20:28:23 2022] foo start...
[Mon Jan 17 20:28:23 2022] foo end...

[Mon Jan 17 20:28:23 2022] foo2 start...
[Mon Jan 17 20:28:23 2022] foo2 end...

可以发现修饰过后还是一个函数

RERERENCE

[1] https://docs.python.org/zh-cn/3/
[2] https://zhuanlan.zhihu.com/p/87353829
[3] https://eastlakeside.gitbook.io/interpy-zh/decorators