这个坑我是被项目逼着学会的。有一次产品要求给所有接口加耗时统计我打开函数开头写一行start_time time.time()return前再写一行print。改到第三个函数时我开始怀疑人生——同样的代码复制粘贴几十遍以后改格式还得逐个文件翻。于是搜索Python 不修改函数添加日志所有答案都指向同一个词装饰器。但学装饰器的过程比改代码还痛苦。教程上来就甩一个xxx看不懂为什么后面跟函数名、为什么有的装饰器带括号有的不带、为什么 wrapper 里要写*args, **kwargs、为什么还要 import 一个叫wraps的东西。我一度把它当成需要背下来的语法咒语。直到后来才明白装饰器没有任何魔法它只是一个普通的函数调用只是 Python 官方提供的一个缩写。把这层窗户纸捅破所有为什么就都有了答案。这篇笔记就是从头捅到底。一、先补两块地基函数是对象闭包是记忆装饰器踩坑的根源是很多人跳过了两个基础概念直接学语法。快速补齐。第一块函数是对象。Python 里函数和字符串、整数一样是普通值——可以赋给变量、装进列表、当参数传给另一个函数、当返回值返回defhello(name):returnfHello,{name}greethello# 函数赋给变量注意没有括号——不是调用是传递funcs[hello,greet]# 装进列表print(funcs[0](Bob))# Hello, Bobdefcall_it(func,arg):# 函数当参数returnfunc(arg)print(call_it(hello,Carol))hello和hello()的区别是理解一切后续内容的钥匙前者是函数对象本身后者是执行它并取结果。第二块闭包。函数里定义函数内层函数能访问外层函数的变量——而且即使外层函数已经 return 了这个记忆依然保留defouter(x):definner(y):returnxy# inner 记住了 xreturninner add5outer(5)# outer 执行完毕、已经返回print(add5(10))# 15 —— x5 还活着可以把闭包理解成函数对象随身带一个小背包里面装着它定义时所在作用域的变量引用。这个小背包正是装饰器能记住原函数的根本。两块地基铺好装饰器只剩一件事要做把函数传进函数、包装、返回。二、撕开它就是一行赋值语句先完全不用语法糖手写一个包装defadd_log(func):defwrapper():print(函数执行前)func()print(函数执行后)returnwrapperdefsay_hello():print(Hello!)# 手动包装传进去拿回新的替换原名say_helloadd_log(say_hello)say_hello()# 输出函数执行前 / Hello! / 函数执行后看最后那行赋值say_hello add_log(say_hello)。这个动作就是装饰器的全部——把原函数传给包装函数返回增强版用原名指回它。而 Python 觉得这行赋值写起来啰嗦就发明了缩写add_logdefsay_hello():print(Hello!)这两段代码在 Python 解释器眼里一模一样。遇到add_log解释器的翻译动作就是执行say_hello add_log(say_hello)。这是全文最重要的一句话值得单独放大不是注解或标记它是一条在函数定义完成的瞬间立刻执行的语句。带括号就立即执行——这是函数调用的基本规则不会豁免它。这个认知直接解释了新手最容易栽的坑第五节会回来收割。三、造一个万能模板*args、**kwargs和wraps上一节的wrapper()只能包装无参数无返回值的函数——真实代码几乎不存在这种函数。需要两个补丁补完就是一个可以无脑复用的模板。补丁一适配任意签名。让 wrapper 接收任意位置参数和关键字参数原封不动转发给原函数defwrapper(*args,**kwargs):resultfunc(*args,**kwargs)# 转发并接住返回值returnresult补丁二治身份失忆症。包装之后say_hello这个名字指向的其实是wrapper——于是say_hello.__name__变成了wrapper文档字符串也丢了。这会破坏调试信息更严重的是让依赖函数名做路由注册的框架Flask、FastAPI直接失效。解药是标准库的functools.wraps它把原函数的元数据__name__、__doc__等拷贝给 wrapperfromfunctoolsimportwrapsdefadd_log(func):wraps(func)# 一行身份找回defwrapper(*args,**kwargs):print(f调用{func.__name__})resultfunc(*args,**kwargs)print(f{func.__name__}返回{result!r})returnresultreturnwrapper两块补丁打完这就是通用装饰器的标准模板——计时、重试、缓存、权限校验全都长这个骨架。四、带参数的装饰器为什么要套三层下一个拦路虎是这种写法repeat(num_times3)defgreet(name):print(fHello,{name}!)repeat后面怎么带括号了之前的add_log不带啊回到那个核心认知后面跟的是一个会返回装饰器的调用。拆解执行过程repeat(num_times3)defgreet(name):...等价于greetrepeat(num_times3)(greet)从里往外读先执行repeat(num_times3)它返回一个装饰器这个装饰器再吃掉greet返回 wrapper。所以repeat的结构必须是三层嵌套defrepeat(num_times):# 第一层装饰器工厂吃配置defdecorator(func):# 第二层真正的装饰器吃函数wraps(func)defwrapper(*args,**kwargs):# 第三层干活的人for_inrange(num_times):resultfunc(*args,**kwargs)returnresultreturnwrapperreturndecorator为什么必然三层因为配置和函数是两次分开的投喂num_times在repeat(num_times3)时进来func在函数定义完成时才进来。两次投喂需要两层函数分别接住加上最里面干活的 wrapper正好三层。这不是设计炫技是逻辑推论——想通了这一点带参装饰器要套三层就从需要背的规则变成了显然的事实。顺带补一条堆叠规则多个装饰器从下往上应用等价于从上往下嵌套包裹abdeff():...# 等价于 f a(b(f))五、三大踩坑实录学完结构该收割第二节埋下的那个认知了。三个坑都源于对是语句理解不彻底。坑一装饰器在导入时就偷跑杀伤力最大我在生产环境栽过的具体场景为了发监控日志装饰器里初始化了一个数据库连接池。结果容器启动后 K8s 健康检查超时直接杀 Pod——明明一个接口都没被调用启动却卡在 import 某个模块那里两分钟。还原现场importtimedefmonitor_decorator(func):print(f正在初始化监控绑定{func.__name__})time.sleep(3)# 模拟连接池初始化print(监控就绪)defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)returnwrappermonitor_decoratordefget_user_info(user_id):returnf用户{user_id}只是 import 这个模块、一次都没调用控制台照样输出那两行、照样等 3 秒。因为monitor_decorator就是get_user_info monitor_decorator(get_user_info)——只要模块被加载这行就执行装饰器外层的代码就跑完。我当年以为它在等指令其实它第一眼看到我就动手了。正确姿势所有重操作连数据库、读文件、建连接必须挪进 wrapper 内部实现懒加载——第一次真正调用时才初始化。装饰器外层只做轻量的准备工作。坑二忘了wraps框架炸了代码本地跑得好好的接上 Flask/FastAPI 后路由全失效。排查半天发现__name__全是wrapper——框架靠函数名注册路由包装后名字丢了。习惯写 wrapper 之前先敲wraps(func)把它当成模板的一部分而不是可选项。坑三三层漏写一层报错看不懂带参数装饰器少嵌套一层repeat(3)会把3当函数传进去报错完全对不上号。定位办法就是心里默背那句等价式f repeat(3)(f)检查每一层吃的是什么。六、三个可以直接抄走的实战装饰器学习笔记的终点是工具箱。三个高频场景的成品代码均已含wraps、均已懒加载。计时器importtimefromfunctoolsimportwrapsdeftiming(func):wraps(func)defwrapper(*args,**kwargs):starttime.perf_counter()resultfunc(*args,**kwargs)elapsedtime.perf_counter()-startprint(f{func.__name__}:{elapsed:.4f}s)returnresultreturnwrapper重试器带参数版defretry(max_attempts3,delay1):defdecorator(func):wraps(func)defwrapper(*args,**kwargs):forattemptinrange(max_attempts):try:returnfunc(*args,**kwargs)exceptExceptionase:ifattemptmax_attempts-1:raisetime.sleep(delay*(2**attempt))# 指数退避returnwrapperreturndecoratorretry(max_attempts3,delay0.5)deffetch_api(url):...缓存器——其实标准库已经写好了functools.cache就是一个官方装饰器直接用fromfunctoolsimportcachecachedeffibonacci(n):returnnifn2elsefibonacci(n-1)fibonacci(n-2)看到cache时可以顺便检验一下自己的理解它是无参数装饰器两层结构在定义时执行wrapper 里带着对原函数的闭包引用。结语回头看这篇笔记走过的路其实只有一句话不是魔法是f decorator(f)这行赋值的官方缩写——它在函数定义完成的瞬间执行仅此而已。这句话往前推得到装饰器在导入时就运行于是坑一的启动卡死可以推演出来往前推得到带参数时后面是一次调用返回装饰器于是三层嵌套的必然性可以推演出来往前推得到原名被 wrapper 顶替于是wraps的必要性也可以推演出来。学语法的痛苦九成来自把可推导的东西当需要背诵的东西。这也是技术学习笔记这个方向的价值所在踩坑的意义从来不是记住坑的位置而是把坑背后的机制挖出来。机制有了下一个没见过的语法糖——property、staticmethod、dataclass——你都能自己拆开看。