一个天才写了三年Python,最后发现装饰器最有用的用法只有3个
有一个天才,大三那年第一次接触Python。
他看到装饰器的第一反应是:这玩意儿不就是函数套函数吗?有什么难的?
于是他在简历上写了"精通Python装饰器"。
面试的时候,面试官问:"你能手写一个带参数的装饰器吗?"
他愣了三秒,写了个语法错误的嵌套函数。
面试官又问:"functools.wraps是干什么的?"
他说:"可能是......包装一下?"
那天他走出写字楼,在楼下便利店买了瓶冰可乐,蹲在路边喝了很久。
第一个用法:计时
天才后来进了一家创业公司,接手了一个祖传项目。
项目里有个接口,用户反馈说"有时候很慢"。
他问有多慢,产品经理说"有时候要等好几秒"。
他说"好几秒是几秒",产品经理说"就是好几秒嘛"。
天才没再问,他写了个装饰器:
python
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} 耗时: {elapsed:.3f}s")
return result
return wrapper
然后在那个接口上加了一行@timer。
第二天日志里清清楚楚写着:get_user_info 耗时: 4.872s。
他顺着日志查下去,发现是一个循环里每次都查数据库,加了个缓存之后,耗时降到了0.03秒。
产品经理说:"你看,我说好几秒吧。"
天才没说话。但从那天起,他写的每一个可能慢的函数,都会加@timer。
你连自己的代码跑多快都不知道,你怎么优化?
第二个用法:重试
天才后来做了个爬虫项目,爬一个第三方的数据接口。
那个接口很奇怪,十次请求有三次会超时,两次返回500,五次正常。
运维说对方服务器不稳定,天才说那我重试呗。
最开始他是这么写的:
python
for i in range(3):
try:
resp = requests.get(url)
break
except:
time.sleep(1)
后来每个请求都要写一遍这个循环,代码里全是for和try,难看死了。
他想起了装饰器,于是写了个:
python
import requests
from functools import wraps
def retry(max_times=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(max_times):
try:
return func(*args, **kwargs)
except Exception as e:
if i == max_times - 1:
raise e
time.sleep(delay)
return wrapper
return decorator
然后所有请求函数上加一行@retry(max_times=3, delay=2),世界清净了。
带参数的装饰器,本质就是三层嵌套。最外层收参数,中间层收函数,最内层收参数。想通了这一点,装饰器就没什么神秘的了。
天才后来面试的时候,面试官又问带参数的装饰器,他一口气写了出来,还顺便讲了functools.wraps是为了保留原函数的元信息------比如函数名、文档字符串,不然你print(func.name)出来的是wrapper,调试的时候能把人逼疯。
面试官点点头,说:"你这个wraps讲得不错。"
天才心里想:三年前你问我的时候我要是会这个,我早进你们公司了。
第三个用法:权限校验
天才跳槽到了一家做SaaS的公司,负责后端接口开发。
公司的接口分三种权限:普通用户、管理员、超级管理员。
最开始他是这么写的:
python
def delete_user(user_id, current_user):
if current_user.role != "admin":
raise PermissionError("只有管理员才能删除用户")
# ... 删除逻辑
每个接口里都有一个if判断权限,二十个接口就是二十个if,改一次权限逻辑要改二十个地方。
有一天产品经理说"超级管理员也能删用户",天才改了二十个地方,改到第十五个的时候差点把键盘砸了。
那天晚上他用装饰器重写了一遍:
python
from functools import wraps
def require_role(role):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
current_user = kwargs.get("current_user")
if not current_user or current_user.role != role:
raise PermissionError(f"需要{role}权限")
return func(*args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(user_id, current_user=None):
# ... 删除逻辑,不用再写if了
后来产品经理又改权限,他只改装饰器里的一行,所有接口自动生效。
这就是装饰器的真正价值:把那些"每个函数都要做但又不是业务本身"的事抽出来。计时、重试、权限、日志、缓存,这些横切关注点,用装饰器包一层,业务函数就干干净净只关心业务。
写在最后
天才现在工作五年了,他说装饰器他用得最多的就是这三个:计时、重试、权限校验。
那些什么"装饰器实现单例模式""装饰器实现AOP切面编程"的高级用法,他不是不会,而是用得少。
编程这件事,不是你会多少高级语法,而是你能不能用最简单的东西解决最实际的问题。
一个timer装饰器,三十行代码,能帮你定位90%的性能问题。
一个retry装饰器,二十行代码,能让你的爬虫稳定性从70%升到99%。
一个require_role装饰器,十五行代码,能让你少写二十个if。
简单的东西重复用,比复杂的东西偶尔用,有价值得多。
天才说他现在面试新人,从来不问"你能手写多少种设计模式",他只问一个问题:
"你写过的最有用的装饰器是什么?"
能答上来的,基本都能用。
答不上来的,回去写一个timer,明天再来。