跳到正文
装饰器与 with:我从「黑魔法」到「原来是省这个」的转弯
装饰器与 with:我从「黑魔法」到「原来是省这个」的转弯

装饰器与 with:我从「黑魔法」到「原来是省这个」的转弯

装饰器和 with 一开始很像黑魔法。这篇不讲语法细节,讲我怎么从「看不懂这层包装」到想清楚:它们就是把横切的重复逻辑从主流程里剥离。

速读#

装饰器和 with 的黑魔法感,不是语法本身难,而是不知道它们在替你省什么。反过来问“不用它我会怎么写”,就能看见共同点:装饰器处理函数前后的重复包装,with 处理资源用完后的可靠收尾。

黑魔法感从哪来#

装饰器、with 让人觉得玄,通常不是语法难,是不知道它在解决什么——不知道替你省下什么,就读不懂为什么长成那样。想清问题,语法就从魔法降回工具。

我当时卡的不是照抄 @timed 能不能跑,是讲不清 @ 背后发生了什么。和后来用 AI「能跑不懂」是同一种状态。

反向问:不用它们,我会怎么写#

装饰器:函数前后的重复包装#

想给好几个函数加「打印执行耗时」。笨办法是每个函数里写一遍——重复,改一处要改多处。装饰器把这层包装抽出来复用:

@timed
def 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 收尾之所以是收尾,就是因为它必须在——包括抛异常时:

    @contextmanager
    def timer(label):
    start = time.perf_counter()
    try:
    yield
    finally:
    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 依赖注入:鉴权 / 校验从路由里拿出来

一次理解,多处复用。啃透一个抽象,遇见变体不慌。

之后对所有「看着玄」的东西,第一反应都是同一句:它到底在解决什么问题?

版权许可

CC BY-NC-SA 4.0 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

相关文章

s1oopX

登录 s1oopX