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

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

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

速读#

装饰器和 with 的黑魔法感,不是语法本身难,而是不知道它们在替你省什么。反过来问“不用它我会怎么写”,就能看见两类重复:装饰器复用函数调用前后的包装,context manager 把进入和退出一段作用域的协议固定下来。

黑魔法感从哪来#

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

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

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

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

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

import time
from 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
@timed
def slow_add(a, b):
return a + b
# 等价于
# slow_add = timed(slow_add)

这行等价改写说明装饰器表达式在函数定义完成时求值,slow_add 这个名字随后指向包装后的函数;每次真正调用才执行 wrapper@wraps 保留名称、文档和 __wrapped__ 等元数据,finally 则保证被包装函数正常返回或抛异常时都记录耗时。这个版本只适合同步函数:装饰 async def 时必须写 async def wrapperawait func(...),否则测到的只是创建 coroutine 的时间。

with:用完必须收尾的资源#

文件、连接、锁,靠每个调用点记得 close 很容易漏。只要 context manager 的进入阶段成功,with 会在代码块正常结束、return 或抛出普通异常时调用退出逻辑:

with timer("loading"):
data = load_big_file()

共性:横切逻辑从主流程剥离#

剥离什么
装饰器函数调用前后反复出现的包装(计时、重试、日志)
with一段作用域的进入、退出与资源生命周期

主流程只剩业务本身;包装层独立、可复用、可单独改。想清楚这点,它们就不玄了——因为知道在解决什么,而不是只记住怎么写。

两个容易漏的细节#

  • @wraps(func) 别忘 它保留原函数名字、文档和解包入口,否则日志、文档生成和部分依赖签名检查的框架只会看到 wrapper

  • contextmanager + yield 时,退出逻辑放 finally 收尾之所以是收尾,就是因为它必须在——包括抛异常时:

    import time
    from contextlib import contextmanager
    @contextmanager
    def timer(label):
    start = time.perf_counter()
    try:
    yield
    finally:
    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

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

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

版权许可

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

相关文章

s1oopX

登录 s1oopX