装饰器在Python圈子里被叫“魔法”,其实它不是魔法,而是把函数当参数传来传去。工作里我见过不少同事,学Python写业务代码挺熟练,一到自己写装饰器就发怵,不是看不懂别人写的装饰器,就是要用的时候不知道从哪下手。这篇文章不绕弯子,直接从“重复代码怎么把人逼疯”讲起,撕开@语法糖,再带大家手写几个项目里真正能用的装饰器,最后聊聊我踩过的那些坑。读完你不需要背概念,照着例子改一改,就能写出自己的装饰器。
1. 先别谈语法,看看重复代码是怎么逼出装饰器的
很多人学装饰器是从“@加在函数上面”这个语法开始的,结果只记住了形式,没理解它到底为了解决什么问题。我习惯反过来讲:先制造一个痛点,再看装饰器如何把痛点消掉。
1.1 一个计时需求的三种“脏”写法
假设项目里有一批函数需要统计耗时。最直接的想法,是在每个函数内部加上计时逻辑:
import time def fetch_user(user_id): start = time.perf_counter() try: # 模拟查询数据库 time.sleep(0.1) return {"id": user_id, "name": "张三"} finally: print(f"fetch_user 耗时 {time.perf_counter() - start:.4f}s")函数少的时候还好,函数一多,问题就来了:同样的几行代码被复制到十多个函数里。一旦你想把计时结果改成写到日志文件,就得挨个函数改一遍。这违背了“单一职责”:业务函数本该只关心自己做什么,结果还得管计时、打日志这些“横切关注点”。
于是有人想到第二种写法,写一个通用的计时工具函数,手动包一下调用:
def timeit(func, *args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) print(f"{func.__name__} 耗时 {time.perf_counter() - start:.4f}s") return result result = timeit(fetch_user, user_id=1)这比复制粘贴好,但调用方必须改变调用方式。大多数业务代码已经在直接调用fetch_user(1)了,如果因为加了个计时功能就要把所有调用点改成timeit(fetch_user, 1),改动范围太大,也容易漏改。
第三种写法最接近装饰器的思路:定义一个函数,它接收一个函数,返回一个“被包装过的新函数”。
def timeit(func): def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) print(f"{func.__name__} 耗时 {time.perf_counter() - start:.4f}s") return result return wrapper fetch_user = timeit(fetch_user) result = fetch_user(1)注意最后一行,业务代码还是调用fetch_user(1),完全不用改。我们只是把fetch_user这个名字重新绑定到了wrapper函数上。原来的函数还在内存里,只不过现在外面多了一层“包装皮”。这,就是装饰器最朴素的形态。
1.2 从“高阶函数接收函数”到“装饰器模式”
上面第三种写法如果用@语法来表达,就成了:
@timeit def fetch_user(user_id): ...有人觉得@很神奇,其实它就是fetch_user = timeit(fetch_user)的语法糖。两者等价,没有额外魔法。我们写这段代码时,等于告诉Python:“麻烦你在定义fetch_user之后,立刻把它传给timeit,再把timeit返回的新函数重新绑到fetch_user这个名字上。”
这种设计本质上是装饰器模式:在不修改原有函数内部实现的前提下,给函数动态附加新职责。Java里实现装饰器往往要写一堆类、接口、抽象类,Python因为函数是一等公民,语法上天然支持,所以显得格外轻巧。理解了这一点,后面所有复杂写法都只是在这个基础上加参数、加嵌套而已。
2. 拆开“魔法”看本质:函数对象、闭包和@符号的执行顺序
要真正“祛魅”,必须把支撑装饰器的三个底层机制说透:函数是对象、闭包保存状态、@符号的绑定时机。
2.1 Python里函数也是对象:一等公民带来的自由
在Python中,函数和整数、字符串一样,都是对象。函数名只是一个变量名,绑定到某个函数对象上。你可以把一个函数作为参数传给另一个函数,也可以把函数作为返回值从另一个函数里拿出来,甚至可以把函数放进列表、字典:
def add(a, b): return a + b def apply(func, x, y): return func(x, y) print(apply(add, 2, 3)) # 5这段代码没有任何装饰器,但它已经包含了装饰器的核心能力:函数可以被传递。既然函数能被传递,那我们就可以写一个函数timeit,它接收“原函数”作为参数,再返回“新函数”。装饰器本质上就是“工厂函数”:原料是原函数,产品是包装后的新函数。
注意一个关键细节:@timeit是在函数定义阶段执行的,不是在函数调用阶段。什么意思?看这段代码:
@timeit def fetch_user(user_id): ...当Python执行到这一行时,会先创建fetch_user函数对象,然后立即调用timeit(fetch_user),把返回结果重新绑定给fetch_user。所以装饰过程只发生一次,之后每次调用fetch_user(1),实际调用的都是wrapper。这个时机理解不到位,后面排查“为什么我的装饰器代码只打印了一次”这类问题时会很困惑。
2.2 闭包:wrapper为什么能记住外层变量
再看timeit的实现:
def timeit(func): def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) print(f"{func.__name__} 耗时 {time.perf_counter() - start:.4f}s") return result return wrapperwrapper内部使用了变量func,而func是外层函数timeit的参数。Python会把这个“还在外层作用域里的变量”捕获到wrapper的闭包环境中。即使timeit已经返回,func也不会被释放,而是被wrapper继续持有。
闭包是装饰器能工作的地基。没有闭包,包装函数就记不住它该调用哪个原函数。理解闭包有个简单类比:wrapper像是一个派出去执行任务的员工,func是派工单上写着的“要找的人”,虽然发任务的人已经走了,但员工只要拿着派工单,就能找到那个“要找的人”。
调试装饰器时可以打印一下wrapper.__closure__,能看到闭包里捕获了哪些变量。这是排查“为什么装饰器里的变量值一直是旧值”时很有用的手段。
2.3 @符号只是“把函数传进去再传回来”的简写
把所有神秘感剥掉,装饰器的执行链路完全可以展开成普通调用:
# 写法A:使用语法糖 @timeit def fetch_user(user_id): ... # 写法B:不使用语法糖 def fetch_user(user_id): ... fetch_user = timeit(fetch_user)两种写法完全等价。语法糖的好处是:第一,可读性更强,抬头一看就知道这个函数被加了什么能力;第二,装饰器紧贴着函数定义,不会出现在文件末尾,修改时不会遗漏。
但语法糖也带来一个认知盲区:不少人以为@timeit是在“函数内部”生效,其实它是在“函数定义完之后”生效的。如果同一个函数被多个装饰器叠加,执行顺序就容易搞混。我会在后面的避坑章节专门展开,这里先记住一个公式:
@decorator_a @decorator_b def func(): ...等价于:
func = decorator_a(decorator_b(func))最底层的decorator_b先执行,它包装原函数;然后decorator_a再包装decorator_b返回的新函数。这个顺序问题,后文我会专门细说。
3. 手写装饰器:从无参版本到带参版本的三层嵌套
理解了原理,接下来动手。我建议按这个顺序练习:先写无参装饰器,再加functools.wraps,最后写带参装饰器。
3.1 最基础的无参装饰器:四行代码理解全部核心
一个最朴素的装饰器只需要四行核心逻辑:
import time from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) print(f"{func.__name__} 执行耗时 {time.perf_counter() - start:.4f}s") return result return wrapper @timer def load_data(): time.sleep(0.2) return [1, 2, 3] data = load_data() # 输出: load_data 执行耗时 0.2001s把这段代码拆开看:
timer(func):外部工厂函数,接收原函数作为参数;wrapper(*args, **kwargs):内部包装函数,接收任意参数,这样它能代理原函数的各种调用方式;func(*args, **kwargs):真正调用原函数,并把返回值原样返回;return wrapper:把包装函数交还出去,完成“偷梁换柱”。
注意wrapper里的*args, **kwargs不能省。很多人第一次写装饰器只针对一个具体函数,写成def wrapper()或def wrapper(x),结果原函数换个参数个数就崩。统一用*args, **kwargs接收,再透传,是最稳妥的写法。
3.2 functools.wraps:别让函数“丢身份”
只看上面的实现,功能已经没问题,但有一个隐患:装饰之后,load_data.__name__变成了wrapper。
print(load_data.__name__) # wrapper这会带来一堆连锁问题:日志里函数名不对、框架根据函数名做路由匹配时出错、用help()看文档时看到的是包装函数的文档而不是原函数的。更麻烦的是,很多调试工具靠__doc__和__name__识别函数,一旦被装饰,原函数身份信息就丢了。
解决办法是在内部包装函数上再加一个装饰器:@wraps(func)。wraps是functools模块提供的工具,它会从func上把__name__、__doc__、__module__、__qualname__等属性复制到wrapper上,同时还会设置一个特殊的__wrapped__属性,指向原来的函数。
from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): ... return wrapper__wrapped__这个属性很值钱,它在后面讲“处理装饰器带来的签名问题”时是关键。现在记住一条铁律:凡是手写装饰器,内部包装函数一定要加@wraps(func)。不加也能跑,但坑会在后面某个深夜等着你。
3.3 带参数装饰器:为什么非得套三层
无参装饰器只能写死行为。假如我想写一个“自动重试”的装饰器,它需要支持配置重试次数和重试间隔,那就要写成“返回装饰器的函数”。
import time from functools import wraps def retry(max_retries=3, delay=0.1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_retries + 1): try: return func(*args, **kwargs) except Exception: if attempt == max_retries: raise time.sleep(delay) return wrapper return decorator @retry(max_retries=5, delay=0.2) def fetch_data(): ...使用方式也要跟着变:@retry(...)后面带了一对括号,所以它等价于:
fetch_data = retry(max_retries=5, delay=0.2)(fetch_data)拆解执行过程:
- 先调用
retry(max_retries=5, delay=0.2),得到decorator; - 再把
fetch_data作为参数传给decorator,得到wrapper; - 最后把
wrapper重新绑定到fetch_data名字上。
所以这里一共有三层:
| 层级 | 函数 | 作用 |
|---|---|---|
| 第一层 | retry | 接收装饰器参数,并返回真正的装饰器 |
| 第二层 | decorator | 接收原函数,并返回包装函数 |
| 第三层 | wrapper | 接收业务参数,处理逻辑并调用原函数 |
为什么不能把参数直接写在retry上?因为装饰器语法要求@retry后面的东西必须是一个“能接收函数并返回函数”的调用。写成@retry时,Python需要retry这个对象本身可调用且接收函数;写成@retry(...)时,Python需要retry(...)的返回值是可调用且接收函数。想同时支持@retry和@retry(3)两种写法,需要一个小技巧:判断第一次收到的参数到底是不是函数。
def smart_retry(*args, **kwargs): if len(args) == 1 and callable(args[0]) and not kwargs: func = args[0] return retry()(func) return retry(*args, **kwargs)这个写法在实际项目中偶尔能派上用场,让装饰器对调用方更友好。不过它牺牲了一点明确性,我一般只在内部工具里这么用。
4. 五个常用装饰器设计:可以直接抄进项目里
下面这些装饰器我都实际用过,代码不复杂,但每段都藏着一些生产环境才想得到的细节。
4.1 耗时统计:性能分析的探针
计时装饰器最常用,但有个细节容易被忽略:用time.time()还是time.perf_counter()。time.time()是墙上时钟,可能受到系统时间调整的影响;time.perf_counter()专门用来测量短时间间隔,精度更高。生产代码里我默认用perf_counter。
import time from functools import wraps def timer(unit="s", digit=4): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() try: return func(*args, **kwargs) finally: cost = time.perf_counter() - start if unit == "ms": cost *= 1000 print(f"{func.__name__} elapsed: {cost:.{digit}f}{unit}") return wrapper return decorator @timer(unit="ms") def parse_config(): time.sleep(0.01)我把打印放在了finally里,这样即使原函数抛异常,耗时也能被记录,不会漏掉慢请求。这个细节不贵,但排查线上慢接口时非常有用。
4.2 自动重试:对付不稳定依赖的成熟模板
重试装饰器的关键不是重试本身,而是要不要退避、退避多久。最简单的固定间隔已经能解决很多偶发网络问题,但更稳妥的做法是指数退避:第一次等0.1秒,第二次等0.2秒,第三次等0.4秒,这样既避免一上来就打爆服务,又能在持续故障时快速放弃。
import random import time from functools import wraps def retry_with_backoff(max_retries=3, base_delay=0.1, max_delay=2.0): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): delay = base_delay for attempt in range(1, max_retries + 1): try: return func(*args, **kwargs) except Exception as exc: if attempt == max_retries: raise sleep_time = min(delay, max_delay) + random.uniform(0, 0.05) print(f"{func.__name__} 第 {attempt} 次失败: {exc}, {sleep_time:.2f}s 后重试") time.sleep(sleep_time) delay *= 2 return None return wrapper return decorator加随机抖动(jitter)是为了避免多个实例同时重试产生“惊群效应”,这在分布式调用里很重要。另外注意:没有特别需求时,重试装饰器不要捕获KeyboardInterrupt或SystemExit,它们不是Exception的子类,所以不会被这里捕获,这是合理的。
4.3 结果缓存:用计算量换速度之前先想清楚
缓存装饰器的核心是用参数作为键,把结果存下来。自己写一版能加深理解:
from functools import wraps def cached(func): cache = {} @wraps(func) def wrapper(*args, **kwargs): key = repr((args, sorted(kwargs.items()))) if key not in cache: cache[key] = func(*args, **kwargs) return cache[key] return wrapper真正生产环境我建议优先用functools.lru_cache,它自带的maxsize可以限制缓存大小,避免内存无限涨,还能通过cache_info()查看命中率。
但自己写这版会暴露一个隐患:如果原函数返回的是可变对象,比如列表、字典,调用方拿到的是同一个对象,修改这个对象会污染缓存。解决方法是存值时拷贝一份:cache[key] = copy.deepcopy(func(...)),但深度拷贝本身也有开销。所以使用缓存装饰器之前要想清楚:数据是否真的安全、缓存是否会引入过期的脏数据、需不需要缓存失效机制。缓存不是万能灵药,它只是用一致性风险换性能。
4.4 权限校验:把业务里的if判断移出去
在Web框架里,权限校验几乎是装饰器最经典的应用场景。在业务函数里写一堆if 用户角色 != 'admin': raise 403会让代码到处是重复判断,而且一个函数里可能同时混着权限逻辑和业务逻辑。用装饰器能把权限判断收敛到一处。
from functools import wraps def require_role(role): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): user = kwargs.get("user") or args[0] if user.role != role: raise PermissionError(f"需要 {role} 角色,当前是 {user.role}") return func(*args, **kwargs) return wrapper return decorator @require_role("admin") def delete_user(user, target_id): ...这个例子简化了一些,但思路是对的:权限校验尽量在函数边界完成,业务函数内部只处理“已经通过校验”之后的事情。这样写的好处是,以后权限规则变了,只需要改装饰器,不用逐个业务函数排查。另一个实用细节:装饰器里不要吞掉异常,校验失败要么抛异常,要么返回统一的错误对象,看框架习惯,但一定要有明确的失败信号。
4.5 日志追踪:在函数边界做切面的最小实现
打印入参、出参、异常信息的日志装饰器,在排查问题时简直救命。它有两条注意事项:第一,参数可能很大,全量打印会把日志撑爆,最好限制长度;第二,参数里可能有关键信息,日志脱敏要在装饰器层面考虑。
import json from functools import wraps def log_call(max_arg_len=200): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): arg_preview = str(args)[:max_arg_len] kwargs_preview = str(kwargs)[:max_arg_len] print(f"调用 {func.__name__} args={arg_preview} kwargs={kwargs_preview}") try: result = func(*args, **kwargs) print(f"{func.__name__} 返回 {str(result)[:max_arg_len]}") return result except Exception: print(f"{func.__name__} 抛出异常", exc_info=True) raise return wrapper return decorator我把exc_info=True放在print里而不是用logging.exception,是为了避免有些环境里logging配置不完整导致异常堆栈丢失。这个装饰器我一般只在内网工具或开发环境里开,生产环境会接更严谨的日志链路,不会直接print。
5. 进阶使用的避坑指南:这些坑我基本都踩过一遍
装饰器写起来短,坑却不少。这里挑五个最常见的,每个我都吃过亏。
5.1 @wraps没加导致的签名问题:排查起来非常迷惑
没加@wraps时,最直接的后果是__name__变了。但更深的问题藏在inspect.signature里。很多框架会用签名判断参数是否符合要求,比如Flask的路由视图函数,如果函数被装饰后签名变成(*args, **kwargs),一些自动化测试工具或文档生成器就会拿到错误的信息。
加上@wraps之后,wrapper.__wrapped__会指向原函数,inspect.signature默认会沿着__wrapped__找到原函数签名,所以大多数情况下能恢复正常。但要注意:如果装饰器本身需要修改函数签名,比如想往参数里注入一个cache对象,那就不能依赖@wraps自动拷贝,必须显式设置__signature__属性。这类需求很高级,普通业务里基本碰不到,知道有这回事就行。
5.2 多个装饰器的执行顺序:从下往上套,从外往里执行
这是新手最容易搞反的。看这段代码:
@make_bold @make_italic def say_hi(): return "hello"等价于say_hi = make_bold(make_italic(say_hi))。离函数最近的make_italic先执行,它把原函数包装成新函数;然后make_bold再包装这个新函数。调用时最外层包装先执行:make_bold的wrapper先跑,在其中调用make_italic的wrapper,最后才进入原函数。
如果你想让@auth在@logging外面,让日志能记录到未授权访问,就把@auth写在上面:
@auth # 外层,先校验身份 @logging func # 内层,再记录日志 def protected(): ...执行时流程是:校验身份 -> 记录日志 -> 执行业务。想清楚这个顺序,比死记“从下往上装饰,从外往里执行”更有用。
5.3 方法上的装饰器:self有时候会不翼而飞
给类的方法加装饰器,wrapper写成def wrapper(*args, **kwargs)就没问题,因为self会被当作第一个普通参数传进来。但有两种情况会翻车。
第一种,装饰器里调用原函数时漏掉了self。比如你写了一个“记录方法名”的装饰器,没有透传*args, **kwargs,而是框架性地写死了只接受一个参数,方法一调用就报“missing 1 required positional argument: 'self'”。
第二种,在类内部定义装饰器函数,没加@staticmethod。比如:
class Service: def log_method(func): @wraps(func) def wrapper(self, *args, **kwargs): print(f"before {func.__name__}") return func(self, *args, **kwargs) return wrapper @log_method def run(self): ...这个写法可能能跑,也可能不能跑,取决于你访问log_method的方式。因为类体内部引用log_method时,它还是一个普通函数,还没绑定成方法;但如果你把装饰器用在类外部、通过实例方法引用,就会出问题。为避免这种不确定性,我的经验是:装饰器尽量定义在模块顶层,不要放在类内部。如果一定要放类里,就加上@staticmethod,明确告诉Python这不需要绑定。
5.4 async函数上的普通装饰器:小心协程永远不被await
普通装饰器用在async def函数上,问题非常隐蔽。看这段:
def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) print(f"{func.__name__} 耗时 {time.perf_counter() - start:.4f}s") return result return wrapper @timer async def fetch(): ...这里wrapper是普通函数,它调用了func(*args, **kwargs),而func是协程函数,调用它不会真正执行,只会返回一个coroutine对象。任务循环不会去await这个对象,于是你会在日志里看到“coroutine was never awaited”的警告,并且函数里的逻辑根本没有执行。
解决办法是让装饰器意识到被装饰的函数是不是协程函数:
import asyncio from functools import wraps from inspect import iscoroutinefunction def timer(func): @wraps(func) async def async_wrapper(*args, **kwargs): start = time.perf_counter() result = await func(*args, **kwargs) print(f"{func.__name__} 耗时 {time.perf_counter() - start:.4f}s") return result @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) print(f"{func.__name__} 耗时 {time.perf_counter() - start:.4f}s") return result if iscoroutinefunction(func): return async_wrapper return wrapper注意这里不能只用@wraps(func)修饰两个wrapper,因为装饰器最后返回哪一个,那个函数的__name__才会最终生效。async_wrapper和wrapper都加上@wraps没问题,但返回前最好再把__signature__校验一下。生产环境里我还会额外检查inspect.isasyncgenfunction,处理异步生成器的场景,但那属于比较偏门的用法,就不在这里展开了。
5.5 在装饰器里做重活:每次都inspect会把性能拖垮
有些装饰器为了追求“智能”,会在每次调用时执行inspect.signature、inspect.getfullargspec这类反射操作。反射操作的开销比普通函数调用大得多,放在高频请求路径上会带来肉眼可见的性能下降。
我调试过一个接口,响应时间从40毫秒涨到300毫秒,最后定位到某个装饰器每次调用都做了一次完整的参数签名解析。修复方法很简单:把“只需要做一次”的工作放到装饰器工厂阶段执行,不要在wrapper里执行。
def validate_schema(schema): # 在这里解析schema,只做一次 compiled = compile_schema(schema) def decorator(func): @wraps(func) def wrapper(*args, **kwargs): # 这里只用预先编译好的结果 check(compiled, *args, **kwargs) return func(*args, **kwargs) return wrapper return decorator这个经验的核心是:装饰器定义阶段和函数调用阶段要分清。能提前算的不要拖到调用时,否则每次调用都在重复付出成本。
6. 装饰器的优雅边界:什么时候该用,什么时候该绕开
装饰器非常顺手,但用多了之后我开始反思:什么情况下装饰器是好的抽象,什么情况下它只是把代码变得“玄乎”?
6.1 适合装饰器的四类场景
根据我的实践经验,四类场景最适合用装饰器。
第一类是横切关注点,比如日志、计时、鉴权、重试、缓存,这些逻辑与业务无关,适合统一收敛。第二类是协议适配,比如把一个普通函数包装成回调接口要求的格式。第三类是状态清理,比如确保函数结束后释放资源。第四类是注册与路由,比如把某个函数注册到路由表或任务中心,框架里常见的@app.route就是这种。
这些场景有几个共同点:逻辑高度统一、与业务函数本身无关、需要在多个函数间复用。满足这些条件,装饰器是明确的正收益。
6.2 过度装饰的迹象:代码“魔法”过了头
装饰器最大的缺点是“隐式”。别人读代码时,如果不去看装饰器定义,根本不知道函数外面套了什么逻辑。这就产生了一种理解成本:一个函数的行为从“读函数体”变成了“读函数体+所有装饰器”。
我见过一些项目,把业务判断塞进装饰器,结果一个装饰器里又包裹另一个装饰器,每个装饰器都在修改函数行为。排查问题时,调用链绕来绕去,大脑很难跟踪。这通常已经过度设计了。判断标准可以很简单:如果你需要画图才能解释清这个函数上有多少个装饰器、每个装饰器改了什么,那就该简化了。
另外,装饰器会让错误堆栈变长。虽然@wraps能优化函数名显示,但调用栈里还是会多出几层wrapper,对不熟悉装饰器的人来说排错体验并不友好。
6.3 我的几条工程经验
写了几年装饰器,踩过坑也重构过一些糟糕代码,我沉淀下来几条个人经验。
第一,装饰器定义放在独立的模块里,不要散落在业务文件里。这样别人看到@cache、@timer时能快速找到定义。
第二,装饰器的职责保持单一。一个装饰器只做一件事,两个需求就写两个装饰器组合,不要在一个装饰器里又计时又缓存又重试。
第三,装饰器参数不要太复杂。如果装饰器的配置已经超过三四个,我会重新考虑是否应该拆成普通函数或策略对象。
第四,给装饰器写清晰的名字和文档。@check和@validate_user_permission,后者显然更利于维护。
第五,关键路径上的装饰器要刻意关注性能。装饰器虽然优雅,但它毕竟多包了一层函数调用。对每秒调用几万次的核心函数,我会直接评估是否需要改成更轻量的实现。
装饰器不是越华丽越好,它更像一把锋利的小刀,用对地方能干净利落地切出结构,用错地方就会在代码里留下很多需要后人来猜的“隐藏层”。我自己的体会是:先写普通函数,等到重复出现的横切逻辑达到两三个位置时,再考虑抽装饰器。过早抽象和过度抽象一样,都是在给未来的维护者制造负担。
最后说一个小技巧:如果你要调试某个被装饰的函数,又不想被装饰器干扰,可以直接通过func.__wrapped__拿到原始函数,绕过所有包装层。这个属性在关键时候能救命,也是我前面反复强调@wraps的原因之一。