写给自己的 Python 装饰器教程
装饰器这个概念,我前后看了三遍教程,每次看完都觉得懂了,过两周又忘光。后来换了个学法:不从定义出发,从一个真实需求出发。这篇就是按那个思路写的,先给未来的自己看。
需求:给函数计时
假设有个数据处理函数,我想知道它每次跑了多久。最直白的写法:
import time
def process_data():
start = time.time()
# ... 真正的逻辑 ...
time.sleep(1)
print(f"耗时:{time.time() - start:.2f}秒")
process_data()
能用,但如果有十个函数都要计时,每个里面都塞三行计时代码,又丑又容易漏。能不能把"计时"这件事抽出来?
第一步:函数也是对象
Python 里函数是一等公民,可以当参数传、可以当返回值。利用这点,我们写一个"加工函数"的函数:
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时:{time.time() - start:.2f}秒")
return result
return wrapper
timer 接收一个函数,返回一个"包了计时逻辑"的新函数。@wraps(func) 别漏,它能保留原函数的函数名和文档字符串,否则调试时看到的都是 wrapper,很折磨。
第二步:@ 语法糖
上面写完之后,用法是:
@timer
def process_data():
time.sleep(1)
process_data()
# 输出:process_data 耗时:1.00秒
@timer 等价于 process_data = timer(process_data),只是写法更优雅。理解到这里,装饰器已经会用了。
第三步:带参数的装饰器
如果想让计时器支持"超过 N 秒才打印",装饰器本身要先接收参数,那就再包一层:
def timer(threshold=0):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
if elapsed > threshold:
print(f"{func.__name__} 耗时 {elapsed:.2f}秒,超过阈值!")
return result
return wrapper
return decorator
@timer(threshold=2)
def slow_task():
time.sleep(3)
记忆口诀:不带参数的装饰器是"一层包",带参数的是"两层包"。@ 后面有没有括号,决定了你要写几层。
实际工作中我用装饰器干过的事
- 重试:调第三方 API 失败自动重试 3 次,指数退避
- 缓存:函数结果按参数缓存,避免重复算(functools.lru_cache 就是现成的)
- 权限检查:Web 接口里先验登录态,不通过直接返回 401
写完这篇,我总算把装饰器焊进脑子里了。核心就一句话:装饰器是一个接收函数、返回新函数的函数,@ 只是语法糖。其他的都是在这个基础上的变形。