速读#
装饰器和 with 的黑魔法感,不是语法本身难,而是不知道它们在替你省什么。反过来问“不用它我会怎么写”,就能看见共同点:装饰器处理函数前后的重复包装,with 处理资源用完后的可靠收尾。
黑魔法感从哪来#
装饰器、with 让人觉得玄,通常不是语法难,是不知道它在解决什么——不知道替你省下什么,就读不懂为什么长成那样。想清问题,语法就从魔法降回工具。
我当时卡的不是照抄 @timed 能不能跑,是讲不清 @ 背后发生了什么。和后来用 AI「能跑不懂」是同一种状态。
反向问:不用它们,我会怎么写#
装饰器:函数前后的重复包装#
想给好几个函数加「打印执行耗时」。笨办法是每个函数里写一遍——重复,改一处要改多处。装饰器把这层包装抽出来复用:
@timeddef slow_add(a, b): ...
# 等价于# slow_add = timed(slow_add)这行等价改写还说明了一件事:装饰发生在定义时,不是调用时。模块一加载,slow_add 这个名字就已经指向包装后的函数;之后每次调用,跑的都是包装层。想通这点,「@ 是黑魔法」就降级成「@ 是一次普通的函数调用加赋值」。
with:用完必须收尾的资源#
文件、连接、锁,靠人记得 close,中间一抛异常就漏。with 保证收尾一定执行:
with timer("loading"): data = load_big_file()共性:横切逻辑从主流程剥离#
| 剥离什么 | |
|---|---|
| 装饰器 | 函数前后要做的事(计时、鉴权、日志) |
with | 资源用完的收尾 |
主流程只剩业务本身;包装层独立、可复用、可单独改。想清楚这点,它们就不玄了——因为知道在解决什么,而不是只记住怎么写。
两个容易漏的细节#
-
@wraps(func)别忘 保留原函数名字和文档,否则slow_add.__name__变成wrapper,排查会绕路。 -
contextmanager+yield时,退出逻辑放finally收尾之所以是收尾,就是因为它必须在——包括抛异常时:@contextmanagerdef timer(label):start = time.perf_counter()try:yieldfinally:print(f"{label}: {time.perf_counter() - start:.3f}s")执行到
yield,控制权交给with块的主体;主体抛了异常,这个异常就从yield那一行冒出来。收尾写在finally里,正常走完和异常穿透两条路它都会执行;不写finally,异常一穿透,yield后面的代码整段被跳过——收尾没了,with就白用了。顺带把两个语法钩在一起:
with背后是__enter__/__exit__协议,@contextmanager只是让你用一个生成器同时写这两个钩子——yield之前是进场,之后是收尾。知道协议在哪,遇到没用contextmanager的类式写法也认得出来。
这条思路到处都是#
- Java AOP、Spring 拦截器:同一件事在强类型世界的版本
- FastAPI 依赖注入:鉴权 / 校验从路由里拿出来
一次理解,多处复用。啃透一个抽象,遇见变体不慌。
之后对所有「看着玄」的东西,第一反应都是同一句:它到底在解决什么问题?
