一个Python开发者写了15年装饰器,某天突然意识到:自己一直在俄罗斯套娃里写代码。五层嵌套闭包,参数归属全靠猜,每次重构都像在拆炸弹。

他决定做件"不可能的事"——把装饰器工厂压成一张扁平的煎饼。要类型安全,要支持异步,要能在类方法上自动处理self,还要能叠罗汉一样堆叠。这些需求单看都不难,凑在一起就是地狱难度。

装饰器工厂的本质困境

装饰器工厂的本质困境

Python装饰器的写法,新手看是魔法,老手看是债务。最简单的装饰器长这样:

@my_decorator def func(): pass

但真正的工程里,你需要带参数的装饰器——比如@retry(max_attempts=3)。这时候就得写"装饰器工厂":一个返回装饰器的函数。问题从这里开始失控。

传统写法是嵌套地狱。外层函数接收装饰器参数,返回一个接收目标函数的函数,那个函数再返回实际替换的包装函数。三层起步,五层常见。作者的原话是:「你最后会疑惑哪个函数做什么、哪个参数属于哪个函数」。

类型注解更是让这团乱麻雪上加霜。Pyright(微软的Python类型检查器)对嵌套闭包的支持堪称挑剔,# type: ignore往往比业务代码还多。

扁平化设计的五个硬指标

扁平化设计的五个硬指标

作者给自己列了张死亡清单,全部达成才算通关:

第一,单层函数结构。拒绝俄罗斯套娃,所有逻辑摊平在一个函数里。

第二,完整类型安全。Pyright零报错,零ignore注释。

第三,实例方法透明。装饰类方法时,self参数不该成为心智负担。

第四,原生异步支持。async def和def一视同仁。

第五,可堆叠。多个装饰器叠加时不能互相拆台。

他坦言开局时心里没底:「说实话,我不知道能不能全部实现。但这就是这类设计挑战的乐趣所在」。

关键突破:把函数签名当成数据

关键突破:把函数签名当成数据

解法的核心藏在Python 3.9+的typing模块里。作者用ParamSpec(参数规范)把被装饰函数的签名捕获为类型变量,再用TypeVar绑定返回值类型。简单说,就是让类型系统"记住"原函数的输入输出长什么样。

对于异步支持,他用了个小技巧:不判断函数是不是协程,而是让包装函数统一返回协程。同步函数被包一层async def,开销微乎其微,代码却干净得多。

处理self参数更妙。Python的descriptor协议会在类属性访问时自动绑定实例,作者利用这个机制,让装饰器在类环境下自动解包bound method,开发者完全无感知。

最终形态的代码长什么样

最终形态的代码长什么样

成品是一个叫@factory的装饰器工厂——用装饰器来简化装饰器工厂,这本身就够meta的。使用方代码简洁到可疑:

@factory(config=...) async def my_decorator(func, *args, **kwargs): # 前置逻辑 result = await func(*args, **kwargs) # 后置逻辑 return result

没有嵌套,没有闭包陷阱,类型推导全自动。堆叠时直接往上摞,每个装饰器看到的都是"干净"的原函数签名。

作者在文末留了道思考题:如果把这套模式推广到类装饰器,或者带状态的全局装饰器,边界会在哪里崩解?他还没试过,但「这就是乐趣所在」。