速读#
装饰器和 with 的黑魔法感,不是语法本身难,而是不知道它们在替你省什么。反过来问“不用它我会怎么写”,就能看见两类重复:装饰器复用函数调用前后的包装,context manager 把进入和退出一段作用域的协议固定下来。
黑魔法感从哪来#
装饰器、with 让人觉得玄,通常不是语法难,是不知道它在解决什么——不知道替你省下什么,就读不懂为什么长成那样。想清问题,语法就从魔法降回工具。
我当时卡的不是照抄 @timed 能不能跑,是讲不清 @ 背后发生了什么。和后来用 AI「能跑不懂」是同一种状态。
反向问:不用它们,我会怎么写#
装饰器:函数前后的重复包装#
想给好几个函数加「打印执行耗时」。笨办法是每个函数里写一遍——重复,改一处要改多处。装饰器把这层包装抽出来复用:
import timefrom functools import wraps
def timed(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() try: return func(*args, **kwargs) finally: print(f"{func.__name__}: {time.perf_counter() - start:.3f}s") return wrapper
@timeddef slow_add(a, b): return a + b
# 等价于# slow_add = timed(slow_add)这行等价改写说明装饰器表达式在函数定义完成时求值,slow_add 这个名字随后指向包装后的函数;每次真正调用才执行 wrapper。@wraps 保留名称、文档和 __wrapped__ 等元数据,finally 则保证被包装函数正常返回或抛异常时都记录耗时。这个版本只适合同步函数:装饰 async def 时必须写 async def wrapper 并 await func(...),否则测到的只是创建 coroutine 的时间。
with:用完必须收尾的资源#
文件、连接、锁,靠每个调用点记得 close 很容易漏。只要 context manager 的进入阶段成功,with 会在代码块正常结束、return 或抛出普通异常时调用退出逻辑:
with timer("loading"): data = load_big_file()共性:横切逻辑从主流程剥离#
| 剥离什么 | |
|---|---|
| 装饰器 | 函数调用前后反复出现的包装(计时、重试、日志) |
with | 一段作用域的进入、退出与资源生命周期 |
主流程只剩业务本身;包装层独立、可复用、可单独改。想清楚这点,它们就不玄了——因为知道在解决什么,而不是只记住怎么写。
两个容易漏的细节#
-
@wraps(func)别忘 它保留原函数名字、文档和解包入口,否则日志、文档生成和部分依赖签名检查的框架只会看到wrapper。 -
contextmanager+yield时,退出逻辑放finally收尾之所以是收尾,就是因为它必须在——包括抛异常时:import timefrom contextlib import contextmanager@contextmanagerdef timer(label):start = time.perf_counter()try:yieldfinally:print(f"{label}: {time.perf_counter() - start:.3f}s")执行到
yield,控制权交给with块;主体抛出的异常会在生成器的yield处重新抛出。收尾写在finally里,正常结束和异常穿透都会执行;如果在yield周围捕获异常却不重新抛出,context manager 会把异常视为已处理,这只能是明确设计,不能无意吞错。由@contextmanager包装的生成器还必须恰好yield一次,否则进入或退出时会报错。with背后是同步的__enter__/__exit__协议,@contextmanager让一个生成器表达这两个阶段:yield前进入,yield后退出。它能覆盖 Python 正常控制流与普通异常,但进程被强杀、解释器崩溃或机器断电时没有“必然收尾”;重要数据仍要依赖事务、原子写和恢复机制。异步资源对应async with、__aenter__/__aexit__与@asynccontextmanager。
这条思路到处都是#
- Java AOP、Spring 拦截器也在抽取调用前后的关注点,但依赖代理与拦截链,调用边界和失效条件不同
- FastAPI 的
yield依赖可以把资源获取与释放移出路由,语义更接近异步 context manager
一次理解,多处复用。啃透一个抽象,遇见变体不慌。
之后对所有「看着玄」的东西,第一反应都是同一句:它到底在解决什么问题?
