尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
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。把这三条守住绝大多数「装饰器一加函数就坏」的问题都不会出现。
RELATED

相关推荐

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

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

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

📅 2026/10/12 6:17:44
Flume写HDFS全流程指南:版本兼容、参数调优与小文件治理

Flume写HDFS全流程指南:版本兼容、参数调优与小文件治理

Flume连着HDFS这套组合,在我接触过的数据采集场景里出现频率非常高。很多团队一开始只是用Flume把日志推到Kafka,等需要把历史数据落到HDFS做离线分析时,才发现两者对接有一堆细节要处理:版本兼容、Sink参数调优、小文件治理、数据…

📅 2026/10/12 6:17:44
力扣14题最长公共前缀:四种解法与工程应用详解

力扣14题最长公共前缀:四种解法与工程应用详解

刷力扣刷到第14题的时候,我一开始是有点不屑的——最长公共前缀,不就是拿第一个字符串当头,然后挨个比较嘛。但真正提交了两次之后,我才发现这道题里藏着的门道比想象中多。今天就把我对这道题的完整拆解、各种解法的思路和踩过的…

📅 2026/10/12 6:17:44
MORE NEWS

更多资讯

📰

上下文管理实战:从滑动窗口到结构化记忆的对话系统设计

1. 上下文管理到底在管什么第一次听到“上下文管理”这个词,很多人会下意识觉得它是个很虚的概念,像是某种架构师画图时才用得上的术语。但如果你真正写过稍微复杂一点的对话系统、Agent 应用,或者哪怕只是一个带多轮记忆的客服机器人&#x…

📰

优惠券、满减、折扣叠加规则详解:计算顺序与分摊逻辑

1. 三种优惠同时命中时,最大的问题不是“算”,而是“先算谁”1.1 一次后台账单对不上,逼我先回答“先算哪个”早几年我做某个商城活动的价格模块,遇到一个特别典型的客诉:用户买了两件商品,系统同时给了优惠…

📰

MySQL 8.0 DBA实战:权限、锁、备份与复制核心操作指南

简介:这份资源是 Oracle University 出品的《MySQL 8.0 for Database Administrators Student Guide - Volume II》官方学习指南,面向数据库管理员、运维工程师及准备 MySQL 认证的进阶学习者,帮助其系统掌握 MySQL 8.0 的管理与优化技能。压…

📰

Spring Boot智能配餐系统毕业设计全解析:架构、推荐与部署实战

毕业设计选择恐惧症的福音来了。Spring Boot智能配餐系统,这个题目一听就是典型的Java后端全栈练手项目,热度常年居高不下,源码版本43062意味着是经过多轮迭代的成熟版本。做毕业设计最怕什么?怕题目太偏找不到参考资料&#xff0…

📰

2026降AI率工具深度实测:继续教育论文如何避开检测雷区

1. 先搞清楚降AI率工具到底在降什么——检测原理与工具分类1.1 继续教育学员为什么跑不掉AI检测2026年了,继续教育毕业论文、平时作业、开题报告和中期检查报告,几乎全部要过AI率检测这一关。很多学员的真实状态是:写初稿时觉得AI真香&#x…

📰

从极简命名到工程实践:拆解一个叫“rea”的模块设计思路

1. 从“rea”这个标题说起:一个极简词背后的项目拆解思路“rea”这三个字母,第一次看到的时候我愣了几秒。它不像一个完整的英文单词,也不是常见的技术缩写,更不像某个产品的正式命名。但恰恰是这种极简、模糊、甚至有点“残缺感”…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬