从零推导 Python 装饰器:不用背语法,看完这篇就懂了

发布时间:2026/8/9 2:02:42
从零推导 Python 装饰器:不用背语法,看完这篇就懂了
从零推导 Python 装饰器不用背语法看完这篇就懂了很多人学装饰器是直接背decorator语法用着用着就懵了——为什么原函数的逻辑变了为什么执行顺序和书写顺序不一样这篇文章不背语法从函数能当参数传开始一步步推导出装饰器的全部原理。前置认知函数也是个东西在 Python 里函数和数字、字符串一样是一个可以传来传去的对象。你可以把函数存进变量、当作参数传给另一个函数、甚至让一个函数返回另一个函数。这个认知是理解装饰器的地基。如果觉得抽象可以这样理解把函数想象成一道菜谱。菜谱本身不是菜它只是一张写满步骤的纸。你可以把这张纸递给别人传参也可以用一张新菜谱替换掉旧菜谱赋值。装饰器做的事情就是在不改写原菜谱的前提下给这道菜加一层摆盘。一旦接受了函数是个能传来传去的东西后面的一切都是水到渠成。第一步手动包装函数最朴素的思路把函数当作参数传进去在调用前后加上额外逻辑。deffun():print(我是原函数)defwrapper1(func):print(装饰前置逻辑)func()# 调用原函数print(装饰后置逻辑)wrapper1(fun)运行输出装饰前置逻辑 我是原函数 装饰后置逻辑wrapper1接收了fun先打印前置逻辑再替你调用fun()最后打印后置逻辑。原函数被完整地包在了中间。但这个写法有个问题fun还是原来的fun每次想用包装版都得手动写wrapper1(fun)。能不能让fun本身就升级成包装后的版本第二步函数可以重新赋值函数名本质上就是一个指向函数对象的变量名所以可以重新赋值。defwrapper2(func):print(装饰前置逻辑)returnfunc# 不加括号()不调用直接返回函数对象本身funwrapper2(fun)# 将自身作为参数传递返回并赋值给fun运行输出装饰前置逻辑注意这里没有输出我是原函数。因为wrapper2()没有调用原函数只是返回了原函数的引用。赋值之后fun变成了wrapper2()的返回值——也就是原函数本身。所以执行fun()仍然可以调用原函数没丢。但这仍然不够好前置逻辑在赋值时就跑了而且原函数没被调用。我们想要的是——赋值时不执行任何东西等真正调用fun()时才执行包装逻辑。第三步返回一个全新的函数核心技巧在 wrapper 里面再套一层函数外层函数直接返回内层函数。内层函数定义时不会执行只有被调用时才执行。defdecorator(func):defwrapper():print(装饰前置逻辑)func()# 调用原函数print(装饰后置逻辑)returnwrapper# 返回wrapper函数对象不调用它fundecorator(fun)# 赋值新函数wrapper给fun此时wrapper内容没被调用fun()# 此时fun就是全新的函数了调用fun()就是调用wrapper()运行输出装饰前置逻辑 我是原函数 装饰后置逻辑这一步要分清两个阶段赋值时只造壳不执行fun decorator(fun)调用了decorator()把原fun传进去。decorator内部定义了wrapper但没有调用它只是把wrapper这个函数对象返回出来赋给了fun。此时wrapper 里的代码一行都没跑。调用时才执行包装逻辑fun()此时调用的已经不是原函数了而是wrapper()。wrapper执行前置逻辑 → 调用原函数 → 执行后置逻辑。完整的包装效果在这一刻才显现。外层函数负责注册接收原函数、返回新函数内层函数负责干活前置 原函数 后置。这就是装饰器的标准结构。三个必须想通的关键问题问题 1为什么参数放在最外层有人可能会想能不能把func参数放到内层wrapper上试一下defdecorator():defwrapper(func):print(装饰前置逻辑)func()print(装饰后置逻辑)returnfuncreturnwrapper fundecorator()fun(fun)# 会报错fun decorator()返回了wrapper函数赋值给fun。但此时原函数fun已经被wrapper覆盖而丢失了——变量名fun现在指向的是wrapper不再是原来的那个函数。调用fun(fun)时左边的fun是wrapper需要一个参数func右边的fun也是wrapper不是原函数。类型对不上直接报错。结论必须把参数放在最外层这样原函数在赋值替换之前就被捕获进闭包了不会因为变量名被覆盖而丢失。问题 2原函数有参数怎么办前面的例子原函数都没有参数。但真实业务函数几乎都有参数比如add(a, b)。答案是用*args, **kwargs——Python 的万能参数容器可以接收任意数量、任意形式的参数deflog_decorator(func):definner(*args,**kwargs):print(f【日志】准备调用函数{func.__name__})resfunc(*args,**kwargs)# 把参数传给原函数接收返回值print(f【日志】函数执行完毕)returnres# 必须把原函数返回值返回出去很容易漏returninnerdefadd(a,b):returnab addlog_decorator(add)print(add(3,5))运行输出【日志】准备调用函数 add 【日志】函数执行完毕 8⚠️易错点return res这一行非常容易漏。如果原函数有返回值而inner没有把它返回出去调用方拿到的就是Nonebug 非常隐蔽。记住装饰器是透明管道原函数返回什么你就原样吐回去。问题 3装饰器到底解决了什么问题本质装饰器是利用闭包 函数对象赋值在不修改原函数源代码、不修改原函数调用方式的前提下给已有函数动态增加额外功能的工具。它实际解决的痛点痛点说明重复代码冗余需要给多个函数加相同附加逻辑如日志、计时、权限校验不用每个函数都抄一遍侵入业务代码不想改原有业务代码开闭原则只在外围包裹附加逻辑原函数一行不动职责混杂功能解耦职责拆分各司其职代码可读性和可维护性大幅提升无法灵活扩展可以灵活叠加多个装饰器、按需开关功能像搭积木一样组合 语法糖一行代码背后藏着什么前面我们一直手写fun decorator(fun)这行赋值。Python 觉得这行太常用了就发明了符号来替你干这件事。两种写法完全等价defouter(func):definner():print(前置)func()print(后置)returninner# 写法1原始手写赋值defhello():print(原函数)helloouter(hello)# 这一行就是 替你干的事# 写法2语法糖简写和上面完全等价outerdefhello():print(原函数)outer就是一个快捷方式它在定义完hello之后自动执行hello outer(hello)。仅此而已。但就是这个仅此而已给新手挖了不少坑。坑 1隐藏了赋值替换动作手写hello outer(hello)你能清晰看到变量hello被覆盖指向了返回的inner。但outer一行搞定看不到变量替换大脑很难反应hello已经不是原来的函数了。破解方法每次看到xxx脑子里自动翻译成函数名 xxx(函数名)。坑 2多层带参装饰器嵌套混乱deflog(level):defdeco(func):definner():passreturninnerreturndecolog(INFO)deftest():pass展开原始代码是templog(INFO)testtemp(test)log(INFO)实际上是两步先执行log(INFO)得到一个装饰器再用这个装饰器去装饰test。中间临时变量被隐藏链条断裂越看越乱。坑 3叠加装饰器执行顺序反直觉ABdeff():pass展开后是# 先执行内层 B 包裹 ffB(f)# 再用 A 包裹上一步的结果fA(f)真实代码f A(B(f))。书写从上到下执行从下到上。记忆口诀装饰从下往上包执行从外往里穿。坑 4被装饰函数元信息丢失装饰之后add.__name__不再是add变成了inner。在调试、日志、文档生成时会出问题。解决functools.wraps(func)生产代码必加。importfunctoolsdeflog_decorator(func):functools.wraps(func)# 保留原函数的名字、文档注释definner(*args,**kwargs):print(日志)returnfunc(*args,**kwargs)returninnerlog_decoratordefadd(a,b):两数相加returnabprint(add.__name__)# add而不是 innerprint(add.__doc__)# 两数相加functools.wraps 到底干了什么functools.wraps(func)本质上是把原函数的元信息复制到inner上。手写一个等价版本就清楚了defmy_wraps(func):defdecorator(wrapper):wrapper.__name__func.__name__ wrapper.__doc__func.__doc__ wrapper.__module__func.__module__ wrapper.__qualname__func.__qualname__ wrapper.__annotations__func.__annotations__returnwrapperreturndecorator展开过程temp_decorator my_wraps(func)——my_wraps的形参func 外面收到的原始函数返回里面的decorator函数inner temp_decorator(inner)——inner传递给参数wrapper执行后inner的元信息被替换成原函数的装饰器进阶带参数的装饰器如果装饰器本身需要传参比如日志级别要多套一层函数。先看错误写法defdecorator(func,level):# 两个参数functools.wraps(func)definner(*args,**kwargs):print(f[{level}] 执行函数{func.__name__})returnfunc(*args,**kwargs)returninnerdecorator(INFO)# 报错只会把函数传给第一个形参funclevel没人接收deftest():pass语法糖只能自动传一个参数——被装饰的函数test。decorator(INFO)会先把INFO传给第一个参数funclevel收不到值直接报错。正确写法在外面再包一层专门接收装饰器自身的参数importfunctoolsdeflog(level):# 外层接收装饰器参数 leveldefdecorator(func):functools.wraps(func)definner(*args,**kwargs):print(f[{level}] 执行函数{func.__name__})returnfunc(*args,**kwargs)returninnerreturndecoratorlog(INFO)deftest():print(业务逻辑)test()运行输出[INFO] 执行函数 test 业务逻辑执行流程temp_decorator log(INFO)——level INFO被闭包保存temp_decorator就是内部的decorator函数test temp_decorator(test)——decorator的形参func 原始test函数返回inner赋给test调用test()→ 执行inner——inner既能拿到外层的level又能拿到上一层的func这就是闭包的威力坑 5 只能自动传一个参数这就是上面错误写法的原因。语法糖只负责把被装饰函数传给装饰器如果装饰器还需要额外参数就必须用再套一层的方式让参数在之前就通过闭包传入。多个装饰器叠加defdec1(func):definner(*args,**kwargs):print(dec1 before)rfunc(*args,**kwargs)print(dec1 after)returnrreturninnerdefdec2(func):definner(*args,**kwargs):print(dec2 before)rfunc(*args,**kwargs)print(dec2 after)returnrreturninnerdec1dec2deff():print(f run)f()运行输出dec1 before dec2 before f run dec2 after dec1 after展开过程# 先执行内层 dec2 包裹 ffdec2(f)# 再用 dec1 包裹上一步的结果fdec1(f)调用时像剥洋葱最外层dec1先打印 before进入dec2打印 before执行原函数f然后dec2打印 after最后dec1打印 after。完整口诀装饰从下往上包执行从外往里穿返回从里往外穿。补充类装饰器除了用函数写装饰器还可以用类。原理一样只要类实现了__call__方法它的实例就能像函数一样被调用。classLogDecorator:def__init__(self,func):self.funcfunc# 接收被装饰的函数def__call__(self,*args,**kwargs):print(f【日志】准备调用{self.func.__name__})resself.func(*args,**kwargs)print(f【日志】执行完毕)returnresLogDecoratordefgreet(name):print(f你好{name})returndonegreet(小明)运行输出【日志】准备调用 greet 你好小明 【日志】执行完毕LogDecorator等价于greet LogDecorator(greet)。调用greet(小明)时Python 自动触发__call__方法。类装饰器的好处是可以保存状态比如统计调用次数在需要维护内部状态的场景下比函数装饰器更方便。实战两个真实场景场景一函数执行计时器importtimeimportfunctoolsdeftimer(func):functools.wraps(func)definner(*args,**kwargs):starttime.time()resultfunc(*args,**kwargs)endtime.time()print(f{func.__name__}耗时:{end-start:.4f}秒)returnresultreturninnertimerdefprocess_data():time.sleep(1.5)return处理完成process_data()# 输出: process_data 耗时: 1.5012秒给任何函数加上timer就能自动打印执行耗时排查性能瓶颈时非常有用。场景二登录权限校验importfunctoolsdeflogin_required(func):functools.wraps(func)definner(*args,**kwargs):ifnotcurrent_user.is_authenticated:return请先登录returnfunc(*args,**kwargs)returninnerlogin_requireddefview_profile():return这是你的个人主页login_requireddefedit_profile():return编辑你的资料多个接口需要登录校验不用每个函数里都写if not logged_in一个login_required搞定。Flask、Django 的权限装饰器就是这个原理。一句话总结把整个推导链串起来函数是对象→ 可以当参数传 → 可以被赋值替换 → 外层函数接收原函数、内层函数包裹逻辑 → 返回新函数覆盖旧名字 →语法糖替你写这行赋值。恭喜你又掌握了一个知识点记得给我点赞嘿嘿(●’◡’●)