完整讲解 · 4 段教学·配 3 道练习题·预计 30 分钟
本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。
上一章学了闭包,这一章我们一步步演变一个需求,来认识 Python 中非常有特色的语法——装饰器(Decorator)。
先看一个最简单的打卡函数:
def punch():
print('昵称:两点水 部门:研发部 上班打卡成功')
punch()输出:
昵称:两点水 部门:研发部 上班打卡成功
很朴素。
产品经理跑过来说:「不行啊,怎么没有日志?打卡前后都给我打印一下提示!」
直接改函数:
def punch():
print('[LOG] 开始执行')
print('昵称:两点水 部门:研发部 上班打卡成功')
print('[LOG] 执行结束')
punch()可以是可以——但这样改变了函数本身的功能结构。本来 punch 只是打卡,现在硬塞进了日志。
而且想象一下:如果还有 holiday()、apply_leave()、overtime() 等等很多函数都要加日志——你打算每个函数都写一遍 [LOG] 开始执行 吗?
代码重复 = 坏味道。
我们知道 Python 函数有两个特点:
那就把日志抽出来:
def punch():
print('昵称:两点水 部门:研发部 上班打卡成功')
def add_log(func):
print('[LOG] 开始执行')
func()
print('[LOG] 执行结束')
add_log(punch)输出:
[LOG] 开始执行
昵称:两点水 部门:研发部 上班打卡成功
[LOG] 执行结束
这下 punch 没动过,任何想加日志的函数都可以扔进 add_log。
但调用方式变了——以前是 punch(),现在变成 add_log(punch)。如果项目里 punch() 已经被调用了 100 处,难道全要改成 add_log(punch)?
我们想要的是:不改函数本身,也不改调用方式,就给函数加上日志。
这就是装饰器要解决的事。
装饰器的写法和闭包很像——只不过它接收的是一个函数:
def decorator(func):
def wrapper():
print('[LOG] 开始执行')
func()
print('[LOG] 执行结束')
return wrapper
def punch():
print('昵称:两点水 部门:研发部 上班打卡成功')
f = decorator(punch)
f()输出:
[LOG] 开始执行
昵称:两点水 部门:研发部 上班打卡成功
[LOG] 执行结束
装饰器函数一般做这三件事:
可是认真一看:这写法怎么看都比直接传函数还麻烦啊——还要 f = decorator(punch) 拿到包装后的函数再调用。
这就是为什么 Python 引入了 @ 语法糖——它让定义装饰器、把装饰器调用原函数再把结果赋值给原函数对象名的过程变得非常简洁。
用法:在原函数定义上方加 @装饰器名:
def decorator(func):
def wrapper():
print('[LOG] 开始执行')
func()
print('[LOG] 执行结束')
return wrapper
@decorator
def punch():
print('昵称:两点水 部门:研发部 上班打卡成功')
punch()输出:
[LOG] 开始执行
昵称:两点水 部门:研发部 上班打卡成功
[LOG] 执行结束
@decorator 等价于在 def punch() 之后偷偷帮你执行了一句:
punch = decorator(punch)所以从此以后,调用 punch() 实际上调的是 wrapper(),外层加了日志,里面再调原 punch。
Python 在引入装饰器时,没有引入任何新的语法特性——它都是基于函数是对象、函数可嵌套这些已有的能力。装饰器不是 Python 特有的概念,每种语言都可以有这种思想;只不过 Python 给了它
@这个糖,写起来格外漂亮。
一句话:装饰器 = 不修改原函数和调用方式,给函数额外加一层功能。
上一节的 punch 是无参数的。可现实中函数往往是有参数的——昵称、部门都不能写死,得通过参数传:
@decorator
def punch(name, department):
print(f'昵称:{name} 部门:{department} 上班打卡成功')
punch('两点水', '研发部')如果还是用之前那个 wrapper()(没参数),punch('两点水', '研发部') 就会报「wrapper() takes 0 positional arguments」——因为真正被调用的是 wrapper,而 wrapper 没接参数。
装饰器中的 wrapper 函数可以使用
*args接收所有位置参数。但仅*args不能涵盖关键字参数,所以还要加上**kwargs。
完整写法:
def decorator(func):
def wrapper(*args, **kwargs):
print('[LOG] 开始执行')
func(*args, **kwargs)
print('[LOG] 执行结束')
return wrapper
@decorator
def punch(name, department):
print(f'昵称:{name} 部门:{department} 上班打卡成功')
@decorator
def print_info(reason, **kwargs):
print(reason)
print(kwargs)
punch('两点水', '研发部')
print_info('请假申请', sex='男', age=99)输出:
[LOG] 开始执行
昵称:两点水 部门:研发部 上班打卡成功
[LOG] 执行结束
[LOG] 开始执行
请假申请
{'sex': '男', 'age': 99}
[LOG] 执行结束
这样一来,任何参数形态的函数都可以套上这个装饰器了。
如果原函数有返回值,wrapper 要把它接住并 return 出去,否则装饰之后返回值就丢了:
def decorator(func):
def wrapper(*args, **kwargs):
print('[LOG] 开始执行')
result = func(*args, **kwargs)
print('[LOG] 执行结束')
return result
return wrapper
@decorator
def add(a, b):
return a + b
print(add(3, 5))输出:
[LOG] 开始执行
[LOG] 执行结束
8
这就是装饰器最常见、最完整的模板——记下来,以后写各种通用装饰器都按这个套路。
有时我们希望装饰器本身也接受参数——比如「日志带级别」:
@log(level='INFO')
def hello():
print('hi')这就需要三层嵌套:
代码模板如下:
def log(level):
def decorator(func):
def wrapper(*args, **kwargs):
print(f'[{level}] 开始执行')
result = func(*args, **kwargs)
print(f'[{level}] 执行结束')
return result
return wrapper
return decorator
@log(level='INFO')
def hello():
print('hi')
hello()输出:
[INFO] 开始执行
hi
[INFO] 执行结束
理解三层结构的关键:@log(level='INFO') 先调用 log('INFO'),得到中间那个 decorator,再用它去装饰 hello。所以本质上还是「decorator(hello)」——只不过 decorator 是临时生成的、带着 level 这个闭包变量。
装饰器有一个副作用:装饰之后,原函数的 __name__、__doc__ 都会被替换成 wrapper 的:
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def hello():
"""打个招呼"""
print('hi')
print(hello.__name__) # wrapper —— 不是 hello!
print(hello.__doc__) # None —— 文档丢了!调试时这很烦人。解决办法是用 functools.wraps 把原函数的元信息抄过来:
import functools
def decorator(func):
@functools.wraps(func) # 关键这一行
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def hello():
"""打个招呼"""
print('hi')
print(hello.__name__) # hello
print(hello.__doc__) # 打个招呼写装饰器时,给 wrapper 函数加
@functools.wraps(func)是一个好习惯——它把原函数的名字、文档、签名等信息保留下来,调试和文档生成时都不会出问题。
回顾这一章学到的:
*args, **kwargs 接所有参数;有返回值要 return@functools.wraps接下来在练习里动手写几个常用装饰器吧。
读完了?动手练一遍才算真会。
去闯关模式练习 →