装饰器
写函数写着写着,你会发现一堆函数共享着同样的"周边动作":执行前打个日志、计时、鉴权、缓存结果、重试……要是每个函数都手抄一遍这些代码,既臃肿又难维护。装饰器就是解决这件事的语法工具:把"周边动作"抽成一个可复用的壳,往…
写函数写着写着,你会发现一堆函数共享着同样的"周边动作":执行前打个日志、计时、鉴权、缓存结果、重试……要是每个函数都手抄一遍这些代码,既臃肿又难维护。装饰器就是解决这件事的语法工具:把"周边动作"抽成一个可复用的壳,往函数身上一套,函数就自带这些能力了。
好消息是,学完前面两章,装饰器对你来说几乎没有任何新知识——它不过是函数是一等公民、闭包、__call__ 这几块积木的拼装。这一章我们把拼法讲透。
1. 装饰器的本质:接收函数,交还函数
先抛开 @ 符号,看装饰器"素颜"的样子。假设有个发布应用的函数,我们希望它执行前后自动进出维护模式:
def maintenance_window(func):
def wrapper():
print("进入维护模式")
func()
print("退出维护模式,恢复服务")
return wrapper
def deploy_app():
print("发布新版本……")
# 手动给函数"套壳"
deploy_app = maintenance_window(deploy_app)
deploy_app()
# 输出:
# 进入维护模式
# 发布新版本……
# 退出维护模式,恢复服务
看清楚发生了什么:maintenance_window 接收旧函数 deploy_app,返回新函数 wrapper;我们用新函数顶替了原名。之后调用 deploy_app(),跑的其实是"壳 + 原函数"的组合。
Python 为这个"套壳再顶替"的动作准备了专门语法——@,称为装饰器语法糖:
@maintenance_window
def deploy_app():
print("发布新版本……")
# 上面的写法完全等价于:
# def deploy_app():
# print("发布新版本……")
# deploy_app = maintenance_window(deploy_app)
也就是说,@ 后面跟的名字会被当成函数调用,底下定义的函数作为参数传进去,返回值顶替原函数名。仅此而已。
2. 用一句话给装饰器定性
结合第 08 章的知识,可以给装饰器下一个精确的定义:
装饰器是一个可调用对象,它接收一个可调用对象,可以返回任何东西。
两点展开说:
- "可调用对象"不限于函数——实现了
__call__的类实例同样能当装饰器(上一章手写的MyProp就是活例子:@MyProp接收方法,返回的是一个描述符对象,根本不是函数); - "返回任何东西"意味着语法上允许返回非可调用对象,但那样一来
被装饰的名字(...)就没法调用了。所以为了让程序照常运行,装饰器通常返回另一个可调用对象来顶替原对象。
把这条定义想通,各类框架里千奇百怪的装饰器就都不神秘了。
3. 多个装饰器叠着用
装饰器可以套好几层,规则只有四个字:就近包裹——离函数最近的装饰器先套,然后一层层往外:
@outer
@inner
def task():
pass
# 完全等价于:
# task = outer(inner(task))
执行装配时自下而上(inner 先套),实际调用时则由外向内穿过各层壳。可以想象成给礼物打包:先把礼物装进内层盒子,再套外层盒子;拆的时候自然先拆外层。
4. 动手前的两件小工具
本章习题会用到 time 模块的两个功能,先热热身:
time.time():返回当前时间戳(一个浮点数,表示从 1970 年至今的秒数);time.sleep(seconds):让程序原地暂停指定秒数。
import time
start = time.time()
time.sleep(1)
elapsed = time.time() - start
print(f"耗时: {elapsed} 秒") # 耗时: 1.0003 秒 左右
两件事一组合,就能测出任意函数跑了多久——这正是习题一要做的。
习题
一、秒表装饰器 stopwatch
编写装饰器 stopwatch,自动打印被装饰函数的执行耗时(保留 4 位小数):
import time
def stopwatch(func):
# 你的代码
pass
@stopwatch
def build_report():
time.sleep(1)
return "报表已生成"
build_report()
# 输出:build_report 执行耗时: 1.0012 秒
二、保住函数的"身份证":keep_identity
函数被套壳后,__name__ 会变成 wrapper、__doc__ 会丢失。请实现装饰器 keep_identity,把原函数的名字和注释"移植"给新函数:
def keep_identity(func):
# 你的代码
pass
def with_banner(func):
@keep_identity(func)
def wrapper():
print("====== 开始 ======")
func()
print("====== 结束 ======")
return wrapper
@with_banner
def welcome():
"""欢迎致辞"""
print("欢迎各位!")
welcome()
# 输出:
# ====== 开始 ======
# 欢迎各位!
# ====== 结束 ======
print("name:", welcome.__name__) # name: welcome
print("doc:", welcome.__doc__) # doc: 欢迎致辞
三、复读机装饰器 echo
编写一个带参数的装饰器 echo(n),让被装饰函数每次调用都自动重复执行 n 次:
def echo(n):
# 你的代码
pass
@echo(3)
def notify(msg):
print(msg)
notify("开饭了")
# 输出:
# 开饭了
# 开饭了
# 开饭了
四、缓存装饰器 memoize
编写装饰器 memoize:把函数的"参数 → 结果"映射存进字典,同样的参数再次调用时直接查表返回,不再重复计算:
def memoize(func):
# 你的代码
pass
@memoize
def climb(n):
"""爬楼梯:每次可以走 1 阶或 2 阶,返回走完 n 阶的方案总数"""
if n <= 2:
return n
return climb(n - 1) + climb(n - 2)
print(climb(60)) # 应当瞬间出结果
提示: 用字典做"参数 → 结果"的登记簿;注意 climb 内部是递归调用,装饰器接住的是同一个名字,所以递归层也会吃到缓存——这正是它飞快的秘密。
五、类装饰器 auto_dict
装饰器不仅能修饰函数,还能修饰类。编写类装饰器 auto_dict,自动给类安上一个 to_dict 方法,把实例属性打包成字典:
def auto_dict(cls):
# 你的代码
pass
@auto_dict
class Song:
def __init__(self, title, seconds):
self.title = title
self.seconds = seconds
song = Song("晴天", 269)
print(song.to_dict()) # {'title': '晴天', 'seconds': 269}
下一章:11 · 迭代协议与生成器