解析Python中的装饰器

发布 : 2019-10-05 分类 : basics 浏览 : --

装饰器的作用一般可以概括为复用代码片段。

即不像 import 复用整个模块,也不像类的继承一样进行系统性的复用和演变,装饰器可以更加灵活的面向代码切片编程。

原理

如一开始所说,装饰器的本质其实就是闭包,或者说就是函数

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
from functools import wraps

def decorator(func):
"""不带参数的装饰器"""
@wraps(func)
def args_wrap(*args, **kwargs):
print("wrap")
return func(*args, **kwargs)
return args_wrap

def decorator_with_args(name):
"""带参数的装饰器"""
def func_wrap(func):
@wraps(func)
def args_wrap(*args, **kwargs):
print("another wrap by {}".format(name))
return func(*args, **kwargs)
return args_wrap
return func_wrap

@decorator
def x(name):
print("hello {}".format(name))

@decorator_with_args("hellflame")
def y(name):
print("hello from {}".format(name))

装饰器中最为神秘的可能并不是装饰器本身,而是这个语法糖 @ ,这个在装饰过程中起作用的语法(可以说是灵魂存在)。不过它的作用其实很简单,就是 执行函数 + 赋值。大概效果如下:

1
2
3
4
5
6
7
8
9
10
@decorator
def x(name):
print("hello {}".format(name))

# ====== equals =======

def x(name):
print("hello {}".format(name))

x = decorator(x)

对于带参数的装饰器:

1
2
3
4
5
6
7
8
9
10
@decorator_with_args("hellflame")
def y(name):
print("hello from {}".format(name))

# ======= equals =======

def y(name):
print("hello from {}".format(name))

y = decorator_with_args("hellflame")(y)

就这个语法糖 @ 符号的使用要求其实很简单,即使不是一个装饰器,依然可以使用该符号,比如:

1
2
3
4
5
6
def say(func):
print "{}, you are trapped".format(func.__name__)

@say
def well():
pass

在装饰阶段,well, you are trapped 便会跃然纸上,如果进一步执行 well ,便会报错

1
well()  # 'NoneType' object is not callable

well 之所以变成了 None ,其实是因为 say 默认返回值为 None。如果换一种写法,可能更能明白现状:

1
2
3
4
5
6
7
8
9
def say(func):
print "Hi, {}".format(func.__name__)
return lambda: 1

@say
def well():
pass

well() # 1

此时 well 其实是 lambda: 1。也就是说,其实装饰器的写法并不重要,重要的是返回一个 可执行对象,甚至是说一个可以加一对 () 然后运行的东西,所以还有可能出现下面神奇的东西

1
2
3
4
5
6
7
8
9
def say(func):
return type("OK", (object,), {})

@say
def well():
pass

well() # 没错,这是一个 `OK` 类的实例
print(well().__class__.__name__) # 甚至可以确认一下名字

不知道这算不算python中 duck typing

如果知道了装饰过程是如何进行的,讲真就不用那么在意装饰器本身的书写模式了,而且也就可以更加灵活的使用语法糖了。不过说了装饰过程,装饰器本身其实还是有很多需要注意的地方:

1. 元信息丢失

由于装饰过程其实是一个赋值过程,那么最后得到的被装饰对象其实是装饰器返回出来的可执行对象。这一套狸猫换太子,一般会损失 __module__, __name__, __doc__ 这些元信息,大多数情况下其实都不是很重要,在调试环节和纠错时会比较有用。functools 中的 wraps 装饰器恰好是用来恢复这些元信息的。不过需要注意的是,class 也可以被 wraps 恢复元信息,即:

1
2
3
4
5
@decorator
class A(object):
pass

issubclass(A(), A) # issubclass() arg 1 must be a class

语法和 A 的实例都不存在问题,问题在于 A 本身已经不再是 class ,而是一个方法,也就是说不管再怎么恢复,A类 已经不是 A类 了,而是 A 方法

2. 返回函数

如上所述,不管原来是什么,被装饰之后的对象变成了一个函数,即使装饰器和被装饰对象都是类也不行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Wrap(object):
def __call__(self, func):
def wrapper(*args, **kwargs):
print("Wrap")
return func(*args, **kwargs)
return wrapper

@Wrap()
class Gift(object):
Impossible = "really?"
def __init__(self):
print("surprise")
self.content = "nothing"

print(Gift().content) # nothing
type(Gift) # function
Gift.Impossible # 'function' object has no attribute 'Impossible'
Gift().Impossible # really?

3. 执行阶段

在装饰器中,由于语法糖 @ 会执行装饰器,所以第一层函数一定会被执行。可以说 参数修饰函数 (涉及到被装饰对象执行时上下文处理的函数,也就是 args_wrap 类似的函数) 才是在被装饰对象执行时真正执行的对象,在其外层的代码都会在装饰阶段就执行完毕。

这里的区别在于

  1. 装饰阶段在全局作用域执行,一般不会有竞争条件,而 参数修饰函数 却是在每次执行被装饰对象时都会运行,竞争条件与被装饰对象一致。也就是说如果在多线程环境下使用被装饰对象,那么可以通过 参数装饰函数 控制并发条件
  2. 参数修饰函数 相对装饰器中的其他部分而言,使用的是内部作用域,不能对外部作用域直接进行赋值操作。这和闭包的使用是一致的

用途

装饰器的用途可以说是很广了。

从控制被装饰对象的 输入执行输出 来分的话,有如下例子:

输入

比如通过装饰器传入额外参数:

1
2
3
4
5
6
7
8
9
10
11
def mysql_context(connection_config=DEFAULT_MYSQL_CONFIG):
def func_wrap(func):
def args_wrap(*args, **kwargs):
with closing(mysql.connection(**connection_config)) as cursor:
return func(cursor=cursor, *args, **kwargs)
return args_wrap
return func_wrap

@mysql_context()
def fetch_data(cursor):
# do things with cursor

以上是一个通过装饰器将额外的 cursor 传入被装饰函数的例子,并且能够进行自动上下文管理 (通过上下文管理器)。当然一般不推荐修改被装饰函数的函数签名

执行

干预被装饰对象的执行过程有很多种例子,记忆中比较多的用法包括:

  1. 单例模式的实例管理
  2. 权限验证 (根据权限信息选择执行被装饰函数还是执行无权限响应)
  3. 函数计时器 (记录函数运行时间)

例子比较多,其中单例模式的应用应该有另外的文章记录,其它应用应该比较直白

输出

这种用法比较少见,但是在少数需要进行消息分发的场景下,用装饰器可以很方便

1
2
3
4
5
6
7
8
9
10
11
12
13
def dispatch(targets):
def func_wrap(func):
def args_wrap(*args, **kwargs):
result = func(*args, **kwargs)
for target in targets:
send_message(target, result)
return result
return args_wrap
return func_wrap

@dispatch(["root", "apt", "docker"])
def warning():
return "warning"

以及一种对于上述三者都不怎么重视的应用

钩子注册

即对于被装饰对象的返回值不再重视,而是需要被程序其他部分知道被注册的对象即可

比如个人比较常用的一个例子就是路由注册

1
2
3
4
5
6
7
8
9
10
11
_ROUTER = []

def router(path_patterns):
def register(handler):
_ROUTER.append((path_patterns, handler))
return register

@router("/(index|error)")
class EntryHandler(RequestHandler):
def get(self, page):
self.write(page)

这里大概是在 tornado 中利用 router 函数进行路由的注册,代码思路应该在 这个项目 里有具体体现。这里对于类的返回值(实例)已经不再需要,所以直接在装饰器中舍弃了返回值

另一个知道的例子如 click ,一个终端命令行解析工具

总结

装饰器的出现,尤其是语法糖的使用,让python中的非侵入式开发变得十分方便,也让代码的复用级别进一步降低,也是 函数式编程 发展过程中出现的必然产物。

装饰器如果使用得当将会事半功倍,过度滥用也会导致头重脚轻。从整体项目来看,装饰器将python作为胶水语言的特点进一步突出,但是谁都知道,不可能整个项目都是胶水,在大型项目中,胶水更不应该出现太频繁,否则将会导致项目各模型之间穿孔严重,耦合加剧,甚者都会变成一锅浆糊。

重要的是,在合适的地方使用合适的工具

本文作者 : hellflame
原文链接 : https://hellflame.github.io/2019/10/05/decorators-in-python/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
留下足迹
点击通过issue留言