你有没有经历过这样的场景:项目里已经躺了三十多个函数,产品经理突然跑过来让你给每个接口加上调用日志、耗时统计、权限校验。如果你还在一处处复制粘贴print(f"xxx called"),那这篇文章正好是为你准备的。
Python 里的装饰器,我习惯叫它代码的"优雅整容术"与"逻辑劫持"。说它整容,是因为它能在不改动原函数内部代码的前提下,给函数"塑形"——加上日志、鉴权、缓存、重试这些外壳;说它劫持,是因为它在调用函数之前和之后,悄悄插入你的逻辑,甚至可以决定原函数到底跑不跑。无论你是刚学完 Python 基础语法、想把函数玩出花样的新手,还是已经写了不少业务代码、正在为重复劳动发愁的开发者,理解装饰器的机制都能让你从"会写函数"进阶到"会包装函数",代码复用性和维护性完全上一个台阶。
这篇内容不止讲@装饰器的语法,我会把闭包的底层原理、带参装饰器的嵌套结构、实际项目里高频场景的完整代码,以及我踩过的坑全部拆开揉碎讲一遍。
1. 从重复代码说起:装饰器到底在替你做什么
1.1 一个真实的改造现场:三十个接口函数让你加日志
假设你正在做一个简单的订单系统,目前有三个核心函数:登录、查用户信息、创建订单。它们的原始实现大致长这样:
def login(username, password): # 几十行业务逻辑 return {"code": 0, "msg": "login success"} def get_user(user_id): # 查询数据库等逻辑 return {"nickname": "张三", "level": 3} def create_order(user_id, amount): # 校验库存、生成订单号 return {"order_id": "20240901-001", "amount": amount}现在问题来了,老板要求每个接口都要记录"谁在什么时候调用了它、参数是什么、耗时多久、返回了什么"。没有装饰器的时候,你最直接的想法是在每个函数里加重复代码:
import time def login(username, password): start = time.time() print(f"[LOG] 调用 login, 参数: {username}, {password}") result = {"code": 0, "msg": "login success"} print(f"[LOG] login 返回: {result}, 耗时: {time.time() - start:.4f}s") return result一个函数改起来还好,三十个函数呢?你会被迫复制粘贴大量的"埋点代码"。而且这种做法的弊端显而易见:
- 侵入式修改:业务代码和日志逻辑混在一起,读起来难受,出了问题也很难排查。
- 容易漏改:函数一多,总有几个忘记加日志,线上问题就很难追踪。
- 修改成本高:哪天想统一把日志格式换掉,又得把所有函数翻一遍。
这时候装饰器就是最自然的解法。它的核心思路是:日志逻辑不该写在业务函数身体里,而应该包在业务函数外面。
1.2 装饰器的核心转变:把逻辑从"身体里"抽到"壳子上"
用装饰器改造后,业务函数本身完全不用动,只需要在定义处加一行:
@log_decorator def login(username, password): # 原始业务逻辑,一个字符都不改 return {"code": 0, "msg": "login success"} @log_decorator def get_user(user_id): return {"nickname": "张三", "level": 3} @log_decorator def create_order(user_id, amount): return {"order_id": "20240901-001", "amount": amount}login、get_user、create_order这三个名字,现在指向的不再是"原始函数本体",而是一个"被装饰器包装过的新函数"。当代码调用login(...)时,真正执行的其实是包装壳里的逻辑:先记录参数、再调用原始函数、然后记录返回值和耗时。
打个比方,原始函数是一个旅客,装饰器是机场的安检通道。旅客不需要在自己身上装安检设备,只需要从通道里走一趟,安检流程就自动完成。你后续想加液体检查、加防爆检查,只需要修改安检通道(装饰器),不需要把每个旅客(函数)重新改造一遍。
1.3 "整容术"和"逻辑劫持"到底指什么
"优雅整容术"很好理解:不动内部器官,只改变外表。业务函数内部的逻辑是"内脏",装饰器在外面做美化或增强。加日志、加耗时统计、加权限验证,本质上都是在"表皮层"动刀。
"逻辑劫持"则更进一步:装饰器不只是"在前后加点东西",它可以完全接管函数的执行流程。原函数要不要执行、什么时候执行、执行结果怎么处理,都由装饰器说了算。一个最典型的情况就是权限校验:
def admin_required(func): def wrapper(user, *args, **kwargs): if user.role != "admin": # 到这里就返回了,原函数根本不会被调用 return {"code": 403, "msg": "no permission"} return func(user, *args, **kwargs) return wrapper用户没有管理员权限时,包装函数直接返回 403,原始函数压根没机会执行。这就是"劫持"——装饰器把原本属于原函数的执行权夺了过来,然后根据自定义规则决定是否放行。这个思想在后面的鉴权、缓存、限流、重试场景中会反复出现。
2. 撕开装饰器的"包装纸":闭包、函数对象与 @ 语法糖
2.1 Python 里函数是一等公民,先接受这个事实
要理解装饰器,第一步不是学语法,而是接受一个基本事实:在 Python 里,函数也是对象。它跟整数、字符串、列表一样,可以被赋值给变量,可以作为参数传进另一个函数,也可以作为返回值从函数里"蹦"出来。
def say_hello(): return "hello" # 函数也是对象,可以直接赋值给别的变量 greet = say_hello print(greet()) # hello # 函数可以作为参数传入另一个函数 def call_func(f): return f() + ", python" print(call_func(say_hello)) # hello, python很多刚学 Python 的同学看到这里会有点不适应:"明明我在函数名后面没加括号,它怎么就变成了一个可调用的变量?"没错,不加括号的函数名,代表的是"这个函数对象本身";加了括号,它才去执行。装饰器利用的恰恰是"不加括号"的行为——函数对象可以被传来传去。
2.2 闭包是装饰器的地基
装饰器的底层技术是闭包。闭包简单说就是:内层函数记住了外层函数的局部变量,即使外层函数已经执行完毕,这些变量依然存活,并陪伴内层函数走完一生。
看一个最小闭包示例:
def outer(message): def inner(): print(f"收到消息: {message}") return inner func = outer("hello") # 外层函数已经执行完了,但 inner 依然记得 message 的值 func() # 收到消息: hello你可以把inner想象成一个背着背包的旅行者。背包里装着外层函数运行时的局部变量message。哪怕outer函数已经返回,旅行者背着背包继续走,随时能从背包里取出message来用。
装饰器就是"外层函数负责收函数,内层函数负责干活"的组合。外层函数接收一个函数作为参数,内层函数记录、调用这个函数,并把调用结果返回出去。这个结构在装饰器里被无限复用。
2.3 @ 语法糖到底做了什么
@只是一个简写。下面两种写法是完全等价的:
# 写法一:不用 @ def login(username, password): return {"code": 0, "msg": "success"} login = log_decorator(login) # 手动把原函数交给装饰器,再用返回值覆盖原变量 # 写法二:用 @ @log_decorator def login(username, password): return {"code": 0, "msg": "success"}@log_decorator这行的意思是:定义完login函数后,立即执行log_decorator(login),然后把返回结果重新赋值给login这个名字。装饰器在函数定义的那一刻就会执行,而不是在函数被调用时才执行。这点特别容易被人忽略,后面讲坑的时候还要提。
提示:装饰器接收的是一个函数对象,返回的通常也是一个函数对象。如果在装饰器内部忘记返回包装函数,原函数名就会被"覆盖"成
None,调用时直接报TypeError: 'NoneType' object is not callable。
2.4 第一个完整装饰器的推演过程
现在手动实现一个timer_decorator,用来统计被装饰函数的执行耗时。
import time def timer_decorator(func): # 包装函数:接收任意参数 def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) # 调用原始函数 cost = time.time() - start print(f"[TIMER] {func.__name__} 耗时 {cost:.4f}s") return result # 把原函数返回值原样传出去 return wrapper为什么内层函数必须写成*args, **kwargs?因为装饰器不知道将来会装饰什么函数。有的函数接收两个位置参数,有的接收关键字参数,有的什么都不接收。用*args和**kwargs把参数"原封不动"地接住,再原封不动地传给原始函数,这样装饰器才有通用性。
还有个细节:内层函数拿到result之后必须return出去。很多人第一次写装饰器容易漏掉这个return,结果发现被装饰的函数输出变成了None。原因就是:你调用的是wrapper,而wrapper没有把原函数的结果交还给你。
实际使用:
@timer_decorator def heavy_compute(n): total = 0 for i in range(n): total += i return total print(heavy_compute(1000000))输出类似:
[TIMER] heavy_compute 耗时 0.0312s 499999500000耗时统计是先打印的,返回值是后返回的,顺序很清晰:先记录开始时间、执行原函数、算耗时打印、把原结果返回给上层调用者。
3. 实战拆解:日志、鉴权、重试三个高频装饰器
3.1 日志装饰器:参数、返回值、耗时全记录
日志装饰器是装饰器里最"万能"的应用,几乎每个项目都有。我自己写的日志装饰器,通常至少包含四个信息:函数名、调用参数、返回值、耗时。异常情况也要单独记录,不能因为日志把真实异常吞掉。
import time import traceback def log_decorator(log_func=print): def decorator(func): def wrapper(*args, **kwargs): start = time.time() args_repr = [repr(a) for a in args] kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] signature = ", ".join(args_repr + kwargs_repr) log_func(f"[LOG] 调用 {func.__name__}({signature})") try: result = func(*args, **kwargs) elapsed = time.time() - start log_func(f"[LOG] {func.__name__} 返回 {result!r}, 耗时 {elapsed:.4f}s") return result except Exception as e: log_func(f"[LOG] {func.__name__} 异常: {e}") log_func(traceback.format_exc()) raise # 异常一定要重新抛出,不能替调用方做决定 return wrapper return decorator这里用了带参数的装饰器log_decorator(log_func=print),后面会详细解释这种三层嵌套的结构,现在先知道它能灵活指定日志输出到哪里就行。注意!r这个格式化符号,它能把参数以repr形式输出,字符串会带上引号,这样日志里参数类型一目了然。
在实际项目中,你只要把log_func换成项目自己的日志对象(比如logging.getLogger("app").info),整组业务函数就能统一接入标准日志系统,比在每个函数里手写 print 可维护得多。
3.2 鉴权装饰器:逻辑劫持的典型战场
鉴权是我觉得最能体现"劫持"二字的装饰器场景。它可以拦截未登录的请求、校验用户角色、判断接口权限,并且在权限不足时不让原函数执行分毫。
假设你在写一个 Web 后端(不依赖任何框架,模拟逻辑),接口函数需要从请求上下文里取当前用户:
class User: def __init__(self, user_id, role): self.user_id = user_id self.role = role def require_login(func): def wrapper(*args, **kwargs): # 从 kwargs 里找当前用户(开发时经常这么传,方便测试) user = kwargs.get("current_user") if user is None: return {"code": 401, "msg": "请先登录"} return func(*args, **kwargs) return wrapper def require_admin(func): def wrapper(*args, **kwargs): user = kwargs.get("current_user") if user is None: return {"code": 401, "msg": "请先登录"} if user.role != "admin": return {"code": 403, "msg": "无权访问"} return func(*args, **kwargs) return wrapper然后分别用于不同接口:
@require_login def get_my_profile(current_user=None): return {"profile": f"用户 {current_user.user_id} 的资料"} @require_admin def delete_order(order_id, current_user=None): # 只有管理员能走到这里 return {"code": 0, "msg": f"订单 {order_id} 已删除"}测试一下:
bob = User(user_id=1, role="user") admin = User(user_id=2, role="admin") print(get_my_profile(current_user=bob)) # {'profile': '用户 1 的资料'} print(delete_order(order_id=100, current_user=bob)) # {'code': 403, 'msg': '无权访问'} print(delete_order(order_id=100, current_user=admin)) # {'code': 0, 'msg': '订单 100 已删除'}注意看,当普通用户尝试删除订单时,delete_order的业务逻辑完全没执行,返回的就是装饰器给的 403。这就是"逻辑劫持"的极致表现:装饰器握着通向原函数的闸门,它不放行,原函数连启动的机会都没有。
3.3 重试装饰器:失败后自动重试的那点经验
在调用第三方 API、连数据库、发网络请求这类场景里,经常遇到"偶发失败"。直接抛异常给用户不友好,全部手动重试又太繁琐。重试装饰器可以统一解决,但有几个细节我得先说清楚:
- 重试次数要能配置,不能写死。
- 重试间隔要有,否则失败时瞬间空转,CPU 白烧。
- 不是所有异常都值得重试,得支持指定捕获哪些异常。
- 重试耗尽后,最后一次异常要原样抛出,不能让调用方"以为成功了"。
import time def retry(max_times=3, delay=0.5, exceptions=(Exception,)): def decorator(func): def wrapper(*args, **kwargs): last_exc = None for attempt in range(1, max_times + 1): try: return func(*args, **kwargs) except exceptions as e: last_exc = e if attempt == max_times: break time.sleep(delay) raise last_exc return wrapper return decorator使用示例:
@retry(max_times=4, delay=0.2, exceptions=(ConnectionError, TimeoutError)) def fetch_remote_data(): # 模拟偶发失败 import random if random.random() < 0.5: raise TimeoutError("模拟超时") return {"data": "ok"}业务调用方完全无感知,失败后自动重试。这类装饰器在高并发服务里几乎是标配。再进一步,生产环境通常会给delay加一点随机抖动,避免大量请求失败后同步重试造成"重试风暴",把下游服务直接打挂。抖动就是在间隔基础上加一个随机值:
import random time.sleep(delay + random.uniform(0, 0.2))从"劫持"的角度看,重试装饰器劫持的是函数的异常出口:原本函数抛个异常就结束了,现在被装饰器半路接住,又塞回执行流程里重来一次。
3.4 三个装饰器的共同结构
把上面三个装饰器放一起看,会发现它们的长相高度一致,区别只在于"在 wrapper 里对原函数做了什么处理":
| 装饰器 | 前置钩子(调用原函数前) | 调用原函数 | 后置钩子(调用原函数后) | 可能完全绕过原函数 |
|---|---|---|---|---|
| 日志 | 记录参数、开始时间 | 正常调用 | 记录结果、耗时、异常 | 否 |
| 鉴权 | 校验身份和权限 | 校验通过才调用 | 无 | 是(权限不足直接返回) |
| 重试 | 无 | 循环尝试 | 捕获异常、决定是否重试 | 否(但会反复调用) |
看完这个表格,你应该能感受到:装饰器本质是一个模板,核心工作就是"在调用原函数这条主线上,选择性地插入额外逻辑"。你只需要把注意力放在三个位置:调用前、调用本身、调用后。
4. 进阶形态:带参装饰器与类装饰器
4.1 为什么需要带参装饰器
上面的retry装饰器已经出现了带参数的形式:
@retry(max_times=4, delay=0.2, exceptions=(TimeoutError,)) def fetch_remote_data(): ...如果retry只是普通单层装饰器,@retry后面就只能是retry(func)这种形式,无法额外配置参数。要支持@retry(参数),装饰器本身必须再包一层:最外层接收配置参数,中间层接收函数,内层才是真正的包装逻辑。
整个结构像这样:
def retry(max_times=3, delay=0.5, exceptions=(Exception,)): # 这一层接收配置参数 def decorator(func): # 这一层接收被装饰函数 def wrapper(*args, **kwargs): # 这一层接收调用时的参数 for attempt in range(1, max_times + 1): try: return func(*args, **kwargs) except exceptions: if attempt == max_times: raise time.sleep(delay) return wrapper return decorator调用@retry(max_times=3)时,Python 先把retry(max_times=3)执行掉,得到一个decorator,然后把这个decorator应用到函数上,即decorator(func)。
初学装饰器时,我经常被这"三层嵌套"绕晕。记忆方法是:最外层不碰函数,只碰配置;碰函数的是中间层;碰调用参数的只能是内层。一个参数被用到哪一层,它就该出现在哪个作用域,顺着这条线走,嵌套就不会乱。
4.2 把装饰器写成类:更直观的状态存储
函数式装饰器最大的短板是"没办法优雅地保存跨调用的状态"。如果你想知道某个函数被调用了多少次、累计耗时多少,函数式写法也能实现,但得靠外层闭包里的可变变量来记录,读起来别扭。
更直观的方式是用类实现装饰器。一个类的实例可以保存属性,天然适合存状态。实现的关键是__call__魔法方法,它让类的实例可以像函数一样被调用。
import time class CallStats: def __init__(self, func): self.func = func self.call_count = 0 self.total_time = 0.0 def __call__(self, *args, **kwargs): self.call_count += 1 start = time.time() result = self.func(*args, **kwargs) self.total_time += time.time() - start return result def get_stats(self): return { "call_count": self.call_count, "total_time": round(self.total_time, 4), "avg_time": round(self.total_time / self.call_count, 4) if self.call_count else 0 }使用:
@CallStats def process(item): return item * 2 for i in range(10): process(i) print(process.get_stats()) # {'call_count': 10, 'total_time': ..., 'avg_time': ...}这里@CallStats做的事情是:用CallStats(process)创建了一个实例,这个实例通过__call__方法变成了"可调用对象",并覆盖了process这个名字。因为实例的call_count和total_time是实打实的对象属性,所以统计状态被安全地保存在对象里,不会像函数闭包变量那样显得"藏得深"。
4.3 类装饰器与函数装饰器的选择标准
用哪一种不是谁替代谁的关系,我按几年使用经验给个选择标准:
| 情况 | 推荐 | 原因 |
|---|---|---|
| 只做横切逻辑,无跨调用状态 | 函数式装饰器 | 结构简单、可读性好 |
| 需要在多次调用间记录状态 | 类装饰器 | 属性存储更直观,自带可调用的get_stats方法 |
| 逻辑复杂、包含多个辅助方法 | 类装饰器 | 可以拆分成私有方法,避免闭包里堆满逻辑 |
| 需要动态修改装饰器参数 | 函数式带参装饰器 | 三层嵌套天然支持配置参数 |
另外提一句,类装饰器在__init__里接收函数,但如果你想让它也能"带参配置",照样可以套三层:__init__接收配置参数,__call__接收函数,__call__内部再返回一个wrapper。套路一模一样。
5. 容易踩的坑:wraps、执行顺序、性能与调试
5.1 functools.wraps 到底解决了什么
我见过太多初学者写的装饰器不带@functools.wraps,结果调试时函数名全变成了wrapper,对外接口文档也是一片混乱。看个反例:
def my_decorator(func): def wrapper(*args, **kwargs): """我是包装函数""" return func(*args, **kwargs) return wrapper @my_decorator def add_numbers(a, b): """计算两数之和""" return a + b print(add_numbers.__name__) # wrapper print(add_numbers.__doc__) # 我是包装函数add_numbers这个名字指向的其实是wrapper,所以它的__name__和__doc__都变成了包装函数的值。如果框架或工具依赖函数元信息做文档生成、路由注册、序列化,这种"身份丢失"就会出 bug。
解决办法是在装饰器内部的wrapper上加一行@functools.wraps(func):
import functools def my_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @my_decorator def add_numbers(a, b): """计算两数之和""" return a + b print(add_numbers.__name__) # add_numbers print(add_numbers.__doc__) # 计算两数之和functools.wraps做的事情是把原函数的__name__、__doc__、__module__、__dict__等元信息复制到wrapper上,并额外设置__wrapped__指向原函数。我个人的习惯是,写任何自定义装饰器第一行先写@functools.wraps(func),这行代码的成本几乎为零,但能避免后面调试时多花半小时。
5.2 多个装饰器的执行顺序:从下往上包装,从上往下执行
叠加装饰器是很多人理解错的重灾区。看这个例子:
def first(func): def wrapper(): print(">>> first 调用前") func() print(">>> first 调用后") return wrapper def second(func): def wrapper(): print(">>> second 调用前") func() print(">>> second 调用后") return wrapper @first @second def business(): print("业务函数执行")调用business()的输出是:
>>> first 调用前 >>> second 调用前 业务函数执行 >>> second 调用后 >>> first 调用后为什么会是这个顺序?因为装饰器的叠加等价与嵌套调用:
business = first(second(business))second先被应用,business先变成了"包着原函数的 second 版";然后first再应用到这个"second 版"上,从外到内形成first包second包原函数。所以执行时,最外层first的"调用前"先打印,接着进入second的"调用前",最后才到原函数本体。记忆口诀:装饰(包装)从下往上,执行(脱壳)从上往下。
这个特性在签名校验、事务控制和权限装饰器叠加时尤其重要。比如你希望"登录校验"始终在最外层,把它放在最下面被最后装饰反而会被包在最外面,需要明确规划装饰器的上下顺序。
5.3 性能与调试:装饰器不是免费午餐
每次调用被装饰的函数,都要先执行"外层包装函数"的分发逻辑,这比直接调用原始函数多出几次函数跳转和args/kwargs打包解包。在单个函数调用上,这个开销微乎其微,通常可以忽略。
但在每秒几十万次调用的热点路径上,装饰器的性能开销就会暴露出来。我实测过,一个很薄的装饰器能带来微秒级别的额外耗时,普通业务无感,可如果这个函数恰好是排序算法、数值循环的核心回调函数,加装饰器和不加装饰器的差距就能体现到基准测试结果里。
调试时也有一套技巧。当你怀疑装饰器内的逻辑影响了结果,可以用func.__wrapped__绕过所有装饰器,直接调用原始函数:
# 如果装饰器都用了 functools.wraps,这里就是原始函数 raw_result = business.__wrapped__()或者直接在装饰器内部临时打印调用栈,确认当前执行路径。结合functools.wraps保留的函数名,用调试器(比如 pdb 或 IDE 断点)定位问题时,你能在调用栈里看到真实的函数名,而不是满屏的wrapper。
5.4 三个真实翻车现场
我把自己和朋友踩过的典型坑列一下,提前避雷。
翻车 1:装饰器忘了 return,函数变成 None
def bad_decorator(func): def wrapper(): print("干活...") func() # 漏了 return wrapper @bad_decorator def hello(): print("hello") hello() # TypeError: 'NoneType' object is not callable因为装饰器函数没有返回任何值,hello这个名字被赋成了None。修复方法就是补上return wrapper。
翻车 2:带参装饰器少写一层
# 错误写法 def retry(max_times): def wrapper(func): # 这里接收的应该是被装饰函数,但少了一层 ...当你写@retry(3)时,Python 会执行retry(3),得回来一个decorator函数,然后再用这个decorator去接收被装饰函数。如果retry(3)直接返回了wrapper(内层包装函数),那实际出来的是wrapper(func),而不是decorator(func),执行就乱了。检查标准很简单:带参装饰器一定有三层 def,少一层就报错或行为诡异。
翻车 3:类里直接定义装饰器函数,self 问题
如果你在类的方法里"临时"定义一个装饰器,直接套在另一个方法上,会因为self的传入方式引发意外:
class Service: def my_decorator(self, func): def wrapper(self, *args, **kwargs): return func(self, *args, **kwargs) return wrapper @my_decorator # 这里 my_decorator 接收到的其实是方法对象,不是 self 场景 def do_thing(self): pass这个写法很容易把self搞混。一般建议装饰器独立定义在类外面,或者用staticmethod包装,不要让实例方法直接充当装饰器。
6. 装饰器的边界感:哪些逻辑适合劫持,哪些不适合
6.1 适合装饰器管的"横切逻辑"
什么样的逻辑天生适合装饰器?我的判断标准有三条:
- 横切多个函数:日志、鉴权、限流、缓存、重试、事务、耗时统计,几乎每个函数都要做,把它们从业务函数里抽出去,是最大的价值。
- 与具体业务无关:装饰器本身不知道也不该知道业务函数内部怎么算,它只关心"调用前做什么、调用后做什么"。假如一个装饰器需要判断函数返回值里的业务字段,那它已经开始过度耦合。
- 策略可以统一配置:通过带参装饰器,不同函数可以有不同的重试次数、不同的权限级别、不同的缓存有效期,但机制是同一套。
符合这三条的逻辑,放进装饰器没有副作用,还能让业务函数保持纯粹。我见过很舒服的设计:请求处理函数只要写自己的业务返回值,日志、权限、限流全部由装饰器栈完成,代码清单得像待办事项表。
6.2 不适合用装饰器的场景
装饰器也不是万能胶。下面是几个反面场景:
依赖函数内部局部状态的逻辑不能用装饰器。装饰器只能看到传入的参数和返回的结果,拿不到函数内部的局部变量。比如你想在函数执行到某个中间步骤时做特殊处理,装饰器是无能为力的,这个逻辑必须老实写在函数内部。
和返回值深度耦合的业务判断不要硬塞进装饰器。比如"如果订单金额超过 1000,走领导审批流程",这跟业务强相关,一旦放进通用装饰器,它就要去解析各种函数的返回结构,维护成本越来越高。这种逻辑更适合留在业务函数内部,或者由调用方在拿到结果后处理。
装饰器叠加过多会让代码变得难以调试。如果一个接口函数头顶上盖了七八个装饰器,调用链一层套一层,报错时你很可能需要展开很长的栈帧才能定位到真正的业务逻辑。此时应该考虑用框架层的中间件、过滤器替代部分职责,把粒度放粗。
6.3 装饰器与中间件的分工定位
Web 框架里的中间件(比如 Django 的 Middleware、Flask 的 before_request/after_request、FastAPI 的依赖注入)本质上是"更大粒度的装饰器思想"。它们的思路一致:在不修改每个视图函数的前提下,在请求进入视图之前和离开视图之后插入统一逻辑。
它们的区别在于作用粒度:
- 装饰器:精准控制到函数级,只影响被
@标记的函数。 - 中间件:全局控制到请求级,所有经过框架路由的请求都会被插入逻辑。
实际项目的分层建议是:全局性、与路由无关的逻辑优先用中间件;只针对一部分接口的个性化逻辑,用装饰器。比如全站访问日志、统一跨域、全局异常兜底用中间件;单个接口的超时重试、某个模块的权限细粒度校验、部分接口的缓存策略,用装饰器。两者配合才是完整的横切逻辑解决方案。
我在项目里最常见的实践是:装饰器负责"函数级"的横切处理,中间件负责"请求级"的横切处理,业务函数本身不放任何横切代码。这样无论是加功能还是排查问题,都只需要找对应层级,不会互相踩脚。
最后再分享一点个人体会。我刚用装饰器的时候,总想把它当成炫技工具,什么逻辑都想往装饰器里塞。后来一个老同事跟我说了一句话:"装饰器是给代码做减法的工具,不是做加法的工具。" 每当你觉得"可以用一个装饰器来管这件事"时,先检查一下:它是不是真的让业务函数变轻了、让重复代码变少了?如果答案是肯定的,尽管用;如果只是为了让代码看起来"高深",那可能反而是负担。
如果你正在项目的多个函数里复制粘贴同样的前置逻辑,或者为几十个接口的日志和权限焦头烂额,装饰器就是你需要的那个"整容师"。从手写一个带functools.wraps的日志装饰器开始,慢慢尝试鉴权、重试、限流,你会发现自己对"函数的包装"越来越有感觉。等哪一天你不再需要查语法,随手就能写出嵌套合理的装饰器时,你对 Python 的掌控力就又进了一阶。