Python装饰器原理与实战全解
前言装饰器decorator是 Python 里语法糖用得最成功的一处deco看上去像给函数加了个标签实际上它做的只是把下面的函数对象传进deco再用返回值覆盖原来的名字。整个过程就是一次普通的函数调用加一次普通赋值。最常见的误解是以为是某种「修改原函数」的机制。事实相反原函数没有被改动只是名字被指向了别处。deco(f)里的f还是那个原函数对象装饰后绑定到原名字上的是deco返回的新对象。这个区别在你想在装饰器外再调用原函数、或者想用inspect看签名时会立刻体现出来。本文先讲清的等价展开形式再讲不带参数、带参数、类实现这三类装饰器的写法然后给出可复用的实战例子最后说清装饰器叠加时的执行顺序和最容易踩的元信息丢失问题。文中示例以 Python 3 为基准functools.wraps自 3.2 起会复制__annotations__、自 3.12 起会复制__type_params__functools.cache需要 3.9。Python 2.7 已于 2020 年 1 月 1 日停止维护本文不使用旧版语法。一、到底做了什么# 适用于 Python 3.0def deco(func):def wrapper():print(before)func()print(after)return wrapperdecodef hello():print(hello)hello()上面这段和下面这段完全等价# 适用于 Python 3.0def hello():print(hello)hello deco(hello)hello()deco唯一的语义就是「等价于hello deco(hello)」。由此可以推出三条结论装饰器必须是一个可调用对象接收一个函数作为参数。它的返回值会绑回原来的名字所以通常返回一个函数或可调用对象。装饰发生在模块导入时不是调用时。所以装饰器顶层的代码只跑一次。输出顺序是beforehelloafter二、不带参数的装饰器要能装饰任意签名的函数wrapper必须能转交所有参数。标准写法是*args, **kwargs同时用functools.wraps保留元信息。# 适用于 Python 3.0from functools import wrapsdef logged(func):wraps(func) # 复制 __name__ / __doc__ / __qualname__ 等def wrapper(*args, **kwargs):print(f调用 {func.__name__}参数 {args} {kwargs})result func(*args, **kwargs)print(f{func.__name__} 返回 {result!r})return resultreturn wrapperloggeddef add(a, b0):把两个数相加。return a bprint(add(1, 2)) # 调用 add参数 (1, 2) {} / add 返回 3 / 3print(add.__name__) # addprint(add.__doc__) # 把两个数相加。functools.wraps(wrapped, assignedWRAPPER_ASSIGNMENTS, updatedWRAPPER_UPDATES)的默认行为是把原函数的__module__、__name__、__qualname__、__annotations__、__doc__以及 3.12 起的__type_params__复制到 wrapper 上并更新 wrapper 的__dict__同时给 wrapper 加上__wrapped__属性指向原函数。少了它被装饰的函数名会变成wrapper文档字符串也会丢。三、带参数的装饰器要写成retry(3)这种形式装饰器本身就得再套一层最外层收配置参数返回一个真正的装饰器。# 适用于 Python 3.0from functools import wrapsdef repeat(times):def deco(func):wraps(func)def wrapper(*args, **kwargs):result Nonefor _ in range(times):result func(*args, **kwargs)return resultreturn wrapperreturn decorepeat(3)def say(msg):print(msg)return msgsay(hi) # 打印三次 hi展开过程是say repeat(3)(say)两次调用两层闭包。判断一个装饰器带不带参数只要看它接收的第一个参数是配置值还是函数。四、类实现的装饰器任何定义了__call__的实例都是可调用对象因此类也能当装饰器。带状态的装饰器用类写会比闭包清楚。# 适用于 Python 3.0from functools import update_wrapperclass CountCalls:def __init__(self, func):self.func funcself.count 0update_wrapper(self, func) # 类和函数都能被 update_wrapper 处理def __call__(self, *args, **kwargs):self.count 1return self.func(*args, **kwargs)CountCallsdef ping():return pongprint(ping(), ping()) # pong pongprint(ping.count) # 2print(ping.__name__) # ping注意这里用的是functools.update_wrapper而不是functools.wraps官方文档明确wraps只是partial(update_wrapper, wrappedwrapped, assignedassigned, updatedupdated)这个便捷包装设计用途是「函数定义内的 wrapper 上打一行wraps(func)」。在类装饰器的__init__里没有 wrapper 函数可装饰直接调用底层的update_wrapper(self, func)才是文档推荐的写法。五、叠加顺序与内置装饰器多个装饰器叠加时靠近函数的先被应用# 适用于 Python 3.0def a(func):def w(*args, **kwargs):print(a 进入)r func(*args, **kwargs)print(a 退出)return rreturn wdef b(func):def w(*args, **kwargs):print(b 进入)r func(*args, **kwargs)print(b 退出)return rreturn wabdef target():print(target)target()输出是a 进入b 进入targetb 退出a 退出因为a在b上面展开后是target a(b(target))最外层是a的 wrapper所以它最先进入、最后退出。Python 内置的常用装饰器装饰器作用备注property把方法变成属性访问配合x.setterstaticmethod静态方法不接收 self3.10 起本身也是可调用的classmethod类方法首参是类常用于备用构造器functools.wraps复制元信息装饰 wrapper 用functools.lru_cache记忆化并保留最近若干次调用3.2参数必须可哈希functools.cache无上限记忆化3.9等价lru_cache(maxsizeNone)functools.singledispatch按第一个参数的类型分派3.4contextlib.contextmanager把生成器变成上下文管理器需要 3.2实战重试与计时# 适用于 Python 3.0import timefrom functools import wrapsdef retry(times3, delay0.0):def deco(func):wraps(func)def wrapper(*args, **kwargs):last Nonefor attempt in range(1, times 1):try:return func(*args, **kwargs)except Exception as exc: # 生产代码里应捕获具体异常last excif attempt times and delay:time.sleep(delay)raise lastreturn wrapperreturn decodef 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:.6f} 秒)return wrappertimedretry(times2)def flaky(n):return n * 2print(flaky(21)) # 42并在 finally 里打印耗时这里用try/finally而不是try/except来计时是为了保证函数抛异常时也能输出耗时。注意retry的raise last在循环外——如果每次尝试都失败最终抛出最后一次的异常times0时last为None会抛TypeError所以参数默认值和校验要按实际需求补上。functools.lru_cache是另一个高频装饰器但它有明确限制# 适用于 Python 3.2from functools import lru_cachelru_cache(maxsize128)def fib(n):return n if n 2 else fib(n - 1) fib(n - 2)print(fib(40)) # 102334155print(fib.cache_info()) # CacheInfo(hits..., misses..., ...)cache_info()返回一个CacheInfo命名元组包含hits、misses、maxsize、currsizecache_clear()可以清空缓存。常见坑点1. 忘了functools.wraps元信息全丢❌ 错误写法def deco(func):def wrapper(*a, **kw):return func(*a, **kw)return wrapperdecodef my_func():文档print(my_func.__name__) # wrapperprint(my_func.__doc__) # None✅ 正确写法from functools import wrapsdef deco(func):wraps(func)def wrapper(*a, **kw):return func(*a, **kw)return wrapper2. 带参装饰器少写一层❌ 错误写法def repeat(times, func): # 这样只能 repeat不能 repeat(3)...✅ 正确写法三层结构——外层收参数、中层收函数、内层是 wrapperdef repeat(times):def deco(func):wraps(func)def wrapper(*a, **kw):for _ in range(times):result func(*a, **kw)return resultreturn wrapperreturn deco3. wrapper 没有转交参数被装饰函数一加参数就崩❌ 错误写法def deco(func):wraps(func)def wrapper(): # 只接受零参数return func()return wrapperdecodef add(a, b):return a badd(1, 2) # TypeError: wrapper() takes 0 positional arguments✅ 正确写法def deco(func):wraps(func)def wrapper(*args, **kwargs):return func(*args, **kwargs)return wrapper4. 类装饰器没有同步元信息函数名直接消失❌ 错误写法class CountCalls:def __init__(self, func):self.func func # 只是存起来没有复制元信息def __call__(self, *args, **kwargs):return self.func(*args, **kwargs)CountCallsdef ping():return pongprint(ping.__name__) # AttributeError: CountCalls object has no attribute __name__✅ 正确写法用functools.update_wrapper把原函数的元信息搬到实例上from functools import update_wrapperclass CountCalls:def __init__(self, func):self.func funcupdate_wrapper(self, func)def __call__(self, *args, **kwargs):return self.func(*args, **kwargs)5. 装饰器叠放顺序理解反了❌ 错误想法以为a在b上面就是b先执行。✅ 正确认识展开成target a(b(target))离函数近的b先被应用运行时最外层的a最先进入、最后退出。6. 忘掉了装饰在导入时执行❌ 错误写法def deco(func):print(初始化) # 导入模块时就打印不是调用时return func✅ 正确认识装饰器函数体不含 wrapper 内部在模块导入阶段运行一次。需要延迟到调用时做的事统统写进 wrapper 里。7. 用lru_cache装饰参数不可哈希的函数❌ 错误写法from functools import lru_cachelru_cache(maxsizeNone)def f(items): # items 是 list不可哈希return sum(items)f([1, 2, 3]) # TypeError: unhashable type: list✅ 正确写法改成传元组或者在函数内转换from functools import lru_cachelru_cache(maxsizeNone)def f(items):return sum(items)print(f((1, 2, 3))) # 68. 装饰器吞掉异常或改变返回值类型❌ 错误写法def deco(func):wraps(func)def wrapper(*a, **kw):try:func(*a, **kw) # 忘了 return调用方永远拿到 Noneexcept Exception:pass # 还顺手吞掉了所有异常return wrapper✅ 正确写法def deco(func):wraps(func)def wrapper(*a, **kw):return func(*a, **kw) # 原样转交返回值return wrapper总结形式结构展开无参装饰器一个函数收函数、返回 wrapperf deco(f)带参装饰器三层配置 → 函数 → wrapperf deco(cfg)(f)类装饰器__init__存函数__call__调用f CountCalls(f)叠加由下往上应用由上往下进入a(b(f))元信息函数用wraps类实例用update_wrapper复制__name__等装饰器不难难点全在细节记牢deco就是名字 deco(名字)写 wrapper 一律用*args, **kwargs转交参数和返回值函数用functools.wraps、类实例用functools.update_wrapper。把这三条守住绝大多数「装饰器一加函数就坏」的问题都不会出现。

相关新闻

C++解释器模式实战:从经典GoF到std::variant与现代变体

C++解释器模式实战:从经典GoF到std::variant与现代变体

C里的解释器模式,说实话有点像“教科书常驻嘉宾、实战里没人爱用”的设计模式。很多人一看到GoF那套词——AbstractExpression、TerminalExpression、NonterminalExpression——就觉得这是给编译器课准备的玩具,跟日常工作没关系。但你一旦开始写规则引擎…

2026/10/12 6:17:40 阅读更多 →
特价股票与全球数字创新网络效应:识别被错杀的成长股

特价股票与全球数字创新网络效应:识别被错杀的成长股

拿“特价股票”和“全球数字创新网络效应”放到同一个标题里,乍一看像是把商超折扣海报和某场科技峰会的主题硬凑在一起。我最初也觉得这两件事隔着十万八千里,直到拿真实标的做过几轮交叉验证,才发现这恰恰是一个被大多数人忽视的研投视角。…

2026/10/12 6:16:39 阅读更多 →
知识工作插件:面向异构数据流的可编排处理单元

知识工作插件:面向异构数据流的可编排处理单元

1. 这不是插件市场,而是一套知识工作者的“操作系统补丁”“knowledge-work-plugins”——光看这个名字,很多人第一反应是:又一个浏览器插件合集?或者某个Notion模板库的副标题?其实完全不是。我在过去三年里深度参与过…

2026/10/12 6:16:39 阅读更多 →

最新新闻

P802.11be D3.0全解析:MLO、320MHz与4K-QAM

P802.11be D3.0全解析:MLO、320MHz与4K-QAM

简介:无线局域网标准演进中,IEEE 802.11系列始终引领速率与体验的突破。面向下一代Wi-Fi 7,草案P802.11be D3.0作为正式标准发布前的关键版本,冻结了多数核心参数。其中MLO多链路操作让设备可同时利用多个频段收发,320…

2026/10/12 6:57:02 阅读更多 →
classmethod、getattr、hasattr等6个Python内置方法详解

classmethod、getattr、hasattr等6个Python内置方法详解

最近在整理一份Python学习笔记,正好写到类和对象这块。前面几篇把类的基本定义、继承、魔法方法都过了一遍,这一篇我想单独谈谈类对象和属性的几个内置方法——classmethod、delattr、dir、hasattr、getattr、callable。这几个方法单独拆开看都不难&…

2026/10/12 6:57:02 阅读更多 →
基于MySQL的平行志愿模拟录取系统数据库设计与实现

基于MySQL的平行志愿模拟录取系统数据库设计与实现

简介:这份数据库课程设计资源以「平行志愿模拟录取系统」为主题,面向高校计算机相关专业学生及需要完成数据库课程设计的自学者,帮助解决志愿填报、分数排序、投档录取等业务场景下的数据库建模与实现问题。资源包共969个文件,整体…

2026/10/12 6:57:02 阅读更多 →
SSM+JSP网上购物商城项目从零搭建全流程详解

SSM+JSP网上购物商城项目从零搭建全流程详解

如果你正在做Java课程设计或者毕业设计,大概率绕不开“网上购物商城”这个题目。用SSM加JSP这套老组合来做商城,放在今天看依然是一个非常典型、非常能锻炼人的练手项目。这里记录一下我做这类项目时的完整思路,包括模块怎么拆、表怎么建、代…

2026/10/12 6:57:02 阅读更多 →
断网3年、营收反创54亿:老干妈的“离线运营“与“在线制造“实验

断网3年、营收反创54亿:老干妈的“离线运营“与“在线制造“实验

【摘要】社媒停更近3年的老干妈,2025年营收冲上54亿元历史峰值,关键工序产品不良率降至0.02%。拆解其“离线运营在线制造”双轨实践:AI视觉质检系统将拣椒岗位从600个压缩至48个,电商渠道占比不足5%成为收缩线上运营的核心决策依据…

2026/10/12 6:57:02 阅读更多 →
中文科研利器:从PDF拆解到PPT生成的三工具自动化流水线

中文科研利器:从PDF拆解到PPT生成的三工具自动化流水线

最近在整理自己课题组的工作流时,我发现了一个特别扎心的现象:市面上几乎所有的科研辅助工具,都是先为英文场景设计的。中文论文的PDF解析、中文术语的实体识别、中文实验记录的语义检索,甚至组会PPT里的中文排版,每一…

2026/10/12 6:56:02 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →