资讯动态

HoRain云--从零理解 Python 装饰器:给函数增加能力而不改原函数

发布时间:2026/10/27 18:25:08 来源:尧图企业网站定制
在多个函数周围添加计时、日志或权限检查时直接复制代码很容易造成重复。装饰器允许我们在不修改函数主体的情况下给函数增加通用行为。装饰器看起来抽象其核心却只有一句话接收一个函数返回一个新函数。一、函数也是可以传递的对象Python 中的函数可以赋值给变量defgreet(name):returnf你好{name}say_hellogreetprint(say_hello(小林))这里的say_hello指向函数对象。既然函数能放进变量也就能作为参数传给其他函数并作为返回值传出来。先看一个最简单的包装函数defannounce(func):defwrapper():print(开始执行)resultfunc()print(执行结束)returnresultreturnwrapperannounce()接收原函数返回新函数wrapper()。调用包装后的函数时额外逻辑会在原函数前后执行。二、是应用装饰器的语法announcedeftask():print(处理任务)task()可以把它理解为定义完task后再执行taskannounce(task)因此调用task()时调用的实际是包装后的函数。装饰器并不是一个与普通函数毫无关系的新机制主要让包装过程写起来更自然。不过前面的wrapper()不能接收参数。实际项目中包装器一般需要支持位置参数和关键字参数fromfunctoolsimportwrapsdefannounce(func):wraps(func)defwrapper(*args,**kwargs):print(f开始执行{func.__name__})resultfunc(*args,**kwargs)print(执行结束)returnresultreturnwrapper这里还有一个容易遗漏的细节必须把原函数的返回值传回去。否则装饰前能返回数据的函数装饰后会意外得到None。三、functools.wraps有什么用包装后的函数对象默认叫wrapper其文档字符串等信息也来自包装函数。这会影响调试、帮助文档以及部分依赖函数元信息的工具。wraps(func)会保留原函数的常用元信息。因此编写通用装饰器时通常应当使用它。四、实战统计函数运行时间importtimefromfunctoolsimportwrapsdefmeasure_time(func):wraps(func)defwrapper(*args,**kwargs):starttime.perf_counter()try:returnfunc(*args,**kwargs)finally:elapsedtime.perf_counter()-startprint(f{func.__name__}耗时{elapsed:.4f}秒)returnwrappermeasure_timedefcalculate(limit):returnsum(range(limit))print(calculate(100_000))perf_counter()适合测量经过的时间。把输出放进finally意味着原函数正常返回或抛出异常时都会记录耗时。异常不会被这个装饰器吞掉而是继续向外传播。一次运行的计时结果不能当成严格的性能结论。机器负载、输入规模以及程序预热情况都可能影响耗时。装饰器适合快速观察严谨比较还需要专门的基准测试。五、带参数的装饰器为什么多一层如果希望写成repeat(3)就需要先接收装饰器自己的参数再返回真正用于包装函数的装饰器fromfunctoolsimportwrapsdefrepeat(times):defdecorator(func):wraps(func)defwrapper(*args,**kwargs):resultNonefor_inrange(times):resultfunc(*args,**kwargs)returnresultreturnwrapperreturndecorator使用方式repeat(3)defsay(message):print(message)say(你好)理解时按顺序看repeat(3)先生成decoratordecorator接收say并返回wrapper调用say()时实际上进入wrapper()。这个示例适合说明结构但不能随意用于有副作用的函数。发送邮件、扣款或写入数据若重复三次可能造成严重问题。真正的重试逻辑还必须考虑异常类型、最大次数、等待时间和操作是否允许重复执行。六、多个装饰器的顺序firstseconddefwork():pass它近似等价于workfirst(second(work))距离函数最近的second先包装函数first再包装其结果。顺序不同行为可能不同。多个装饰器叠加时应明确每一层处理的是参数、返回值还是异常。装饰器适合多个函数都需要的通用逻辑。若一段逻辑只属于一个函数直接写在函数内部可能更清楚。掌握装饰器不意味着处处使用它能让调用关系易读才是好的抽象。七、异常与返回值要保持原有语义包装函数并不意味着可以随意改变原函数的契约。原函数返回一个值装饰后通常仍应返回这个值原函数抛出异常装饰器若没有明确的恢复策略就不应悄悄返回None。例如日志装饰器可以记录异常后重新抛出importloggingfromfunctoolsimportwraps loggerlogging.getLogger(__name__)deflog_failure(func):wraps(func)defwrapper(*args,**kwargs):try:returnfunc(*args,**kwargs)exceptException:logger.exception(%s 执行失败,func.__name__)raisereturnwrapper这里捕获较宽的Exception是为了在应用边界记录失败随后立即重新抛出未把错误当成成功。若上层也会记录相同异常则应选择一个合适层级记录避免重复日志。记录参数时还要防止敏感值进入日志。八、用具体调用理解叠加顺序假设first在调用前打印 Asecond在调用前打印 Bdeffirst(func):wraps(func)defwrapper():print(A)returnfunc()returnwrapperdefsecond(func):wraps(func)defwrapper():print(B)returnfunc()returnwrapperfirstseconddefwork():print(工作)调用work()时会依次输出 A、B、工作。虽然second在定义阶段先包装原函数但实际调用从最外层的first开始。把“包装顺序”和“调用进入顺序”区分开就不容易混淆。九、测试装饰器时检查什么装饰器是可复用代码测试应覆盖返回值、位置参数、关键字参数、异常传播以及是否保留__name__等信息。带参数装饰器还应检查边界值例如重复次数为零或负数时究竟允许、拒绝还是有明确默认行为。若装饰器维护共享状态还要考虑多线程或异步场景不能默认一个局部演示就适用于所有执行方式。装饰器的价值在于将真正重复的逻辑集中起来而不是制造额外的间接层。先确认需求在多个函数中稳定存在再抽取为装饰器通常比预先设计一个通用框架更容易维护。十、装饰器在定义阶段执行decorator应用在函数定义处意味着创建包装函数的过程通常发生在模块加载、定义函数的时候真正的wrapper主体则在调用函数时运行。这两种时机不能混为一谈。例如带参数装饰器repeat(3)会在定义时先执行repeat(3)得到装饰器每次调用目标函数才执行包装器中的循环。因此不应在装饰器工厂中意外执行昂贵操作或者假设它会在每次函数调用时重新读取配置。装饰器也可以用于方法但方法调用时的self会作为位置参数传入通用的*args, **kwargs包装器可以转发它。若装饰器需要根据实例状态作判断必须明确实例的位置及函数调用约定。对于异步函数普通同步包装器并不自动具备正确的await行为需要编写适合异步调用的包装逻辑。学习装饰器时最有效的方法是把写法还原成赋值表达式画出“原函数、包装函数、最终变量”之间的关系再逐步加入参数和返回值。理解执行时机与调用顺序后计时、日志等常见用法都会顺理成章。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑