
经常有朋友问我Python 基础学到什么程度才算真正入门我的回答里一定会有一条搞懂装饰器。这东西在 Python 里出现频率极高面试被问、源码里常见、写 Web 框架绕不开。但很多新手学了几天 Python看到app.route、staticmethod、property还是会懵不知道这些符号到底发生了什么。装饰器其实没那么玄乎。一句话概括装饰器就是一个接收函数、返回新函数的函数用来在不修改原函数代码的前提下给函数“加料”。它既能把重复代码抽出来统一处理又能让业务逻辑更干净。这篇文章我会从零开始拆装饰器的原理、手写实现、应用场景、踩坑记录争取用最“人话”的方式讲清楚适合刚学完基础语法、准备进阶的 Python 学习者也适合写过一阵子代码但没系统整理过装饰器知识的开发者。1. 装饰器到底是什么先搞懂三个前置概念1.1 函数是一等公民Python 里函数和整数、字符串、列表一样都是对象。这意味着函数可以赋值给变量、可以放在列表里、可以作为参数传给另一个函数也可以作为返回值从函数里出来。先记住这个性质装饰器才有可能实现。看个简单例子def say_hello(): return hello f say_hello # 不调用函数只把函数对象本身赋值给 f print(f) # function say_hello at 0x... print(f()) # hellof和say_hello指向的是同一个函数对象所以f()就是say_hello()。很多初学者会写f say_hello()那就只是把返回的字符串赋值过去了函数对象仍然只有一个引用这是完全不同的两码事。把函数当成对象之后自然而然地可以写出“接收函数作为参数”的函数也就是高阶函数。一个最朴素的高阶函数例子def call_twice(func): func() func() call_twice(say_hello)这种把函数传来传去的写法是函数式编程的根基也是装饰器的基础。如果这一步想不通后面所有内容都会觉得别扭。1.2 闭包内层函数带着“记忆”闭包是装饰器的另一个基石它指的是在一个嵌套函数中内层函数引用了外层函数的局部变量时即使外层函数已经执行完毕这个内层函数依然“记得”外部变量的值。def make_multiplier(x): def multiplier(y): return x * y return multiplier times_3 make_multiplier(3) print(times_3(9)) # 27times_3拿着的是multiplier函数它被返回后原来的局部变量x本来应该消失但因为有闭包机制x3被保存下来了。所以闭包的本质是“函数 它引用的外部变量的打包组合”。装饰器里的“外层函数接收一个函数、内层函数负责增强原函数”靠的正是闭包来捕获和保存传入的函数对象。可以说没有闭包就没有装饰器。1.3 装饰器的定义语法糖背后的替换逻辑理解了函数是一等公民又理解了闭包装饰器的概念就顺理成章了。装饰器本质上是一个函数它接收一个函数返回一个新函数def decorator(func): def wrapper(*args, **kwargs): # 执行原函数之前做点什么 result func(*args, **kwargs) # 执行原函数之后做点什么 return result return wrapperwrapper就是那个“加了料”的新函数。使用语法糖decorator之后下面的写法decorator def foo(): pass等价于这段手动赋值def foo(): pass foo decorator(foo)符号只是让这个过程更好看、更直观。可以理解成给手机贴膜手机还是那台手机功能没变但多了一层保护。原函数代码一个字都没改却被赋予了额外能力这就是装饰器最核心的价值。为什么要用这种方式而不是直接改原函数最直接的理由是很多时候你改不了原函数比如它是第三方库里的、它是团队里别人维护的公共方法、或者你在多个项目里复用它。用装饰器把扩展逻辑和业务逻辑拆开代码更干净而且一个装饰器能重复用在任意函数上。2. 手写装饰器从零搭建你的核心实现2.1 第一个装饰器为函数添加运行日志写一个最常用的日志装饰器把函数名、参数、耗时记录下来import time def log(func): def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) print(f调用 {func.__name__}参数 {args}耗时 {time.time() - start:.4f}s) return result return wrapper log def add(a, b): time.sleep(0.1) return a b print(add(3, 5))执行结果会有类似输出调用 add参数 (3, 5)耗时 0.1001s然后打印8。这里的*args, **kwargs几乎是所有装饰器 wrapper 的标配它保证装饰器能适配任意参数的函数。args是位置参数打包成的元组kwargs是关键字参数打包成的字典。原函数需要什么参数wrapper 就原样传进去不吞也不改。这个装饰器只做了两件事调用前记录开始时间、调用后打印日志。但它已经说明白了装饰器的工作模式——在func(...)的前后插入逻辑并且把返回值完整地交给调用方。2.2 小心原函数的名字被“偷换”了用了上面的log之后检查一下函数名print(add.__name__) # wrapper不是 add因为add log(add)变量add指向的已经是wrapper函数原来的add函数对象虽然存在但已经没有外部引用了。这会造成不少麻烦调试时看不到原始函数名help(add)显示的是 wrapper 的文档依赖函数名做序列化、路由映射的框架可能出问题单元测试用add.__name__断言会失败。解决办法是使用functools.wraps它会把原函数的__name__、__doc__、__module__、__dict__等信息复制到 wrapper 上import functools def log(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) print(f调用 {func.__name__}参数 {args}耗时 {time.time() - start:.4f}s) return result return wrapper记一个简单规则以后自己写装饰器wrapper 上面一律加上functools.wraps(func)这是 Python 标准库作者都在用的做法没有理由不加。不加属于“能用但有隐患”加了才是规范写法。2.3 带参数的装饰器再套一层函数有时候装饰器本身需要配置比如“日志级别为 ERROR”“重试次数 3 次”。这时不能直接写retry(3)因为retry(3)的求值结果是retry(3)返回的值它必须是一个接收函数、返回新函数的 decorator。也就是说需要三层嵌套def repeat(times): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(times): result func(*args, **kwargs) return result return wrapper return decorator repeat(3) def work(): print(doing work)调用work()会连续执行三次打印。这里的理解路径是repeat(3)执行返回内部函数decoratordecorator应用在work上等价于work decorator(work)decorator(work)返回wrapper而wrapper通过闭包记住了times3。三层嵌套确实容易绕晕但拆开看就是最外层接收装饰器参数中间层接收原函数最内层接收原函数的调用参数。实际项目里如果觉得三层太啰嗦可以用functools.partial或者直接写成带__call__的类装饰器后面会提到。2.4 类装饰器让代码更清晰类也能做装饰器条件是实例可以被调用也就是实现__call__方法。用类写装饰器有天然的优势状态可以用实例属性保存逻辑可以拆分成多个方法比嵌套函数更直观。class Timer: def __init__(self, func): functools.update_wrapper(self, func) self.func func def __call__(self, *args, **kwargs): start time.time() result self.func(*args, **kwargs) print(f{self.func.__name__} 耗时 {time.time() - start:.4f}s) return result Timer def process(): print(processing)Timer等价于process Timer(process)创建了一个Timer实例这个实例的__call__在被调用时执行。由于类实例天然是“可变状态容器”如果需要统计函数被调用了多少次、缓存最近的结果类装饰器写起来会比函数嵌套舒适得多。需要注意上面这种写法Timer(process)的process是普通函数如果装饰器带参数比如Timer(label)那需要Timer(label)返回一个真正的 decorator这个装饰器可以是函数也可以是类逻辑再多一层但原理不变。2.5 多个装饰器的叠加顺序一个函数可以同时被多个装饰器装饰log repeat(2) def greet(): print(hi)执行顺序和视觉顺序相反。可以等价展开来看greet log(repeat(2)(greet))。也就是先应用最靠近函数的装饰器再逐层向上。运行时请求先进入最外层装饰器依次往里最后才执行原始函数。实际项目里最典型的例子是 Flask 的app.route和login_requiredapp.route(/admin) login_required def admin(): return admin这里login_required先作用于admin再交给route登记。如果顺序写反登入逻辑可能不会包裹路由处理函数或者路由拿到的函数还带着登录校验的 wrapper需要小心测试。3. 装饰器的真正价值从语法到实战场景3.1 日志记录告别到处 print很多项目刚起步时都是“哪里有问题就在哪里加 print”时间一长全是临时输出。用装饰器可以把日志集中在一点import logging logging.basicConfig(levellogging.INFO, format%(asctime)s %(levelname)s %(message)s) def logged(levellogging.INFO): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): logging.log(level, f调用 {func.__name__}参数{args} {kwargs}) try: result func(*args, **kwargs) except Exception as e: logging.log(level, f{func.__name__} 异常{e!r}) raise logging.log(level, f{func.__name__} 返回{result!r}) return result return wrapper return decorator如果一个装饰器同时负责“进入日志”“异常日志”“退出日志”业务函数本身只关心自己的逻辑维护成本明显降低。我给团队写内部工具时最喜欢用这个装饰器去找“这个函数到底有没有被调用”“参数传得对不对”之类的问题。3.2 权限校验把业务逻辑和校验逻辑拆开Web 接口里常见的模式是每个接口开头都要判断当前用户有没有权限。如果每个接口都复制一遍校验代码改一个校验规则就要全局搜索替换非常痛苦。def require_permission(permission): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): user get_current_user() if not user.has_permission(permission): raise PermissionError(没有权限) return func(*args, **kwargs) return wrapper return decorator require_permission(admin) def delete_user(user_id): # 业务逻辑 pass校验逻辑只写一遍以后要加“审计日志”“判断用户是否被禁用”再叠一层装饰器就行。框架层面的 Flask 登录扩展、Django 的login_required本质上都是这种思路的标准化实现。3.3 缓存用空间换时间缓存装饰器是性能优化最经典的场景。写一个简单版本把函数的参数和返回值存到字典里def memoize(func): cache {} functools.wraps(func) def wrapper(*args, **kwargs): # 注意这里只简化处理了位置参数实际要处理 kwargs 的可哈希性 key args tuple(kwargs.items()) if kwargs else args if key not in cache: cache[key] func(*args, **kwargs) return cache[key] return wrapper配合递归函数可以明显看到效果。比如斐波那契数列直接用递归是指数级复杂度加上缓存装饰器后每个子问题只算一次memoize def fib(n): if n 2: return n return fib(n - 1) fib(n - 2) print(fib(100)) # 秒出结果而不是跑到天荒地老Python 3.9 之后标准库直接提供了functools.cache和functools.lru_cache生产环境没有必要自己写缓存装饰器。但读源码时能看到lru_cache内部就是一个非常精致的类装饰器值得拆开研究。3.4 重试机制处理临时故障外部接口调用、数据库连接、网络请求经常会遇到偶发失败。最简单的处理方式就是重试但不能盲目重试要控制次数、间隔、重试条件。def retry(max_attempts3, delay0.5): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts 1): try: return func(*args, **kwargs) except Exception as e: if attempt max_attempts: raise print(f第 {attempt} 次失败{e!r}{delay} 秒后重试) time.sleep(delay) return wrapper return decorator这里有几个需要注意的细节重试只适用于幂等操作也就是“重复执行和一次执行结果相同”的操作。如果是扣款、发送短信这种不可重试的操作盲目重试会造成重复扣款、重复发短信的严重事故。所以重试装饰器应该配合业务场景只包裹那些可以安全重试的网络请求或临时性 IO 操作。3.5 注册表插件系统和框架的基石装饰器还有一种不常被新手注意但非常重要的用法用来“登记”函数而不是“包裹”它。例如构建一个命令分发器commands {} def command(name): def decorator(func): commands[name] func return func # 注意这里返回原函数不做包装 return decorator command(add) def add_cmd(a, b): return a b command(multiply) def multiply_cmd(a, b): return a * b这种模式里装饰器的作用不是增强函数而是把函数写进某个容器让外部系统可以发现和调用它。Flask 的app.route就是这种模式路由装饰器并没有改变视图函数本身而是把路径和视图函数的映射关系记录到 app 的路由表里。这是理解 Web 框架路由源码的重要一步。4. 常见问题与排查技巧新手最容易踩的坑4.1 装饰器在“定义时”执行不是在调用时执行新手最惊讶的一点装饰器代码是在def语句执行时立即执行的不是函数被调用时才执行。看这个例子print(start) log def foo(): print(foo) print(end)不要以为log只是“登记”一下实际上log(foo)在def foo之后立刻执行。如果装饰器内部有print你会看到它在程序启动时、函数定义时就被打出来。这在带参数的装饰器和带状态初始化的装饰器里尤其容易踩坑——有人会把耗时任务放在装饰器函数主体里导致模块导入阶段卡顿。正确做法重活应该放在wrapper内部或第一次调用时执行而不是放在decorator函数本身。比如连接数据库、加载模型文件这些操作放进装饰器主体会让所有导入模块的人都替它买单。4.2 wraps 不用的后果函数元信息被污染前面已经提过functools.wraps这里再强调一次。没有它staticmethod、classmethod和一些基于函数签名自动生成文档的工具可能全部失效。排查技巧很简单import inspect print(inspect.signature(wrapper_func))如果发现函数签名变成了(*args, **kwargs)说明装饰器没有做任何签名保留。标准库的functools.wraps也只负责复制元信息不负责保留参数签名。如果想完整保留签名需要用到functools.wraps结合__wrapped__属性或者第三方库wrapt。对于大多数业务场景functools.wraps已经足够。4.3 参数不可哈希导致缓存崩溃自己写缓存装饰器时如果函数的参数是列表或字典直接拿args做字典键会抛出TypeError: unhashable type: list。解决办法是先把参数转换成可哈希的形式比如把列表转成元组、把字典用json.dumps序列化或者干脆要求用户只给可哈希参数。functools.lru_cache同样有这样的限制它要求所有参数可哈希。遇到不可哈希参数只能简化处理或限制使用范围。排查时看到“unhashable”报错先别急着怀疑缓存逻辑多半是参数类型的问题。4.4 装饰器叠加后调用顺序混乱多个装饰器叠加时如果外层装饰器抛异常里层逻辑可能还没执行。调试时可以通过在每个装饰器里打印层级确认实际进入顺序def d1(func): functools.wraps(func) def wrapper(*args, **kwargs): print(进入 d1) result func(*args, **kwargs) print(离开 d1) return result return wrapper def d2(func): functools.wraps(func) def wrapper(*args, **kwargs): print(进入 d2) result func(*args, **kwargs) print(离开 d2) return result return wrapper d1 d2 def f(): print( f 本体)运行会看到顺序进入 d1、进入 d2、f 本体、离开 d2、离开 d1。这个顺序很有意思越靠近函数、越先执行函数本体越远离函数、越先执行外层包装。记忆口诀就是“洋葱模型”从外往里剥再从里往外穿。4.5 用functools.partial快速实现带参数装饰器三层嵌套的带参数装饰器让人头大时可以用functools.partial简化。partial可以固定某个函数的参数让“接收参数”这件事先完成。def with_logging(funcNone, *, levellogging.INFO): if func is None: return lambda f: with_logging(f, levellevel) functools.wraps(func) def wrapper(*args, **kwargs): logging.log(level, f调用 {func.__name__}) return func(*args, **kwargs) return wrapper这样写既支持with_logging直接使用也支持with_logging(levellogging.ERROR)带参使用。核心技巧是让func变成可选的通过判断func is None来决定返回真正的装饰器还是装饰后的函数。这种写法在一些高质量开源库里经常出现可以大幅减少代码嵌套深度。4.6 装饰器与调试被包裹后如何找到原函数用wraps之后wrapper 上的__wrapped__属性会指向原函数。如果发现某个函数突然行为异常需要确认它到底有没有被装饰器影响可以看print(hasattr(func, __wrapped__))如果存在__wrapped__说明它被套了一层。依赖注入、单元测试 mock、文档生成工具很多时候就是通过__wrapped__来穿透装饰器、拿到原始函数信息的。这也是为什么养成“所有装饰器都加functools.wraps”的习惯如此重要。5. 进阶扩展装饰器的实现机制与选型建议5.1 装饰器语法糖的底层展开记住装饰器的展开规则任何事情都好解释。对于decorator def func(): passPython 解释器做了两件事执行def func创建函数对象执行func decorator(func)重新绑定名字。所以装饰器返回什么func最终就是什么。如果装饰器返回的是原函数那就是“登记型”装饰器如果返回另一个函数那就是“增强型”装饰器如果返回一个类的实例那func就被替换成了一个可调用对象。理解这一层你就不会被各类装饰器源码绕晕。5.2 闭包变量查找为什么装饰器能“记住”配置装饰器实现依赖闭包而闭包依赖 Python 的 LEGB 规则先找局部作用域Local再逐层向外找闭包作用域Enclosing、全局作用域Global、内置作用域Built-in。wrapper内部引用func、times、cache等值时会向上找到闭包空间中的变量因为它们被decorator函数或更外层函数定义了。这个机制的副作用是如果外层变量是可变对象比如列表或字典内层函数可以直接修改它。这也是为什么类装饰器适合维护状态——状态存在实例上查找路径更直观不需要依赖复杂的嵌套作用域。5.3 函数装饰器与类装饰器怎么选我个人的选型经验是逻辑简单、无状态、只做“调用前后插逻辑”的用函数装饰器因为代码短、易读。需要维护多个状态、有多个辅助方法、带参数配置复杂的情况用类装饰器因为可以把辅助逻辑拆成独立方法而不是层层嵌套。不过还要注意区分“类装饰器”和“装饰器作用于类”。面向对象的dataclass就是装饰器作用于类它接收一个类并返回一个新类。这类装饰器常用于元编程例如自动生成__init__、__repr__等方法。日常开发中dataclass、total_ordering都是典型代表用法大同小异只是被装饰的对象从函数变成了类。5.4 装饰器的性能与内存开销每多一层装饰器函数调用就多一层函数跳转和堆栈帧分配。微基准测试里一万次调用可能多出几毫秒的开销业务系统通常不需要在意。但如果是在双循环的内层、每秒钟调用数十万次的热点函数上堆五六层装饰器性能还是会有明显下降。另一个容易忽略的是内存问题。装饰器通过闭包引用原函数如果原函数又被某个全局容器引用那么这个函数对象和它依赖的环境会一直留在内存里。如果每次运行时动态创建装饰器还把结果保存到长期存活的容器里可能导致“装饰器生成有用对象、但函数和依赖对象无法被回收”的问题。排查时可以结合gc.get_referrers查看对象的引用路径不过一般情况下静态写好的装饰器不会有这个问题。5.5 避免过度装饰装饰器不是万能的装饰器虽好但也不是越多越好。装饰器在阅读顺序上是“倒着”生效的嵌套太多会让代码很难跟踪。如果一个装饰器本身已经超过几十行或者内部逻辑牵扯到数据库事务、锁、分布式链路等复杂概念它可能已经不适合做“装饰器”了应该考虑抽成中间件、上下文管理器或独立工具函数。另外装饰器不要替代正常函数设计。如果一个函数加了装饰器才能演示正确行为、去掉装饰器就无法理解那问题反而出在原函数设计上。装饰器应该是“锦上添花”的横切关注点而不是业务逻辑的载体。写任何装饰器之前先问自己这个功能是否可以无侵入地加在函数外面如果答案是可以用它就没问题如果答案牵强就不要强行装饰。我自己在实际项目里最常见到的装饰器使用场景大致可以汇总成一个速查表场景适合用装饰器建议实现打印调用日志是函数装饰器 wraps权限校验是带参数装饰器计算结果缓存是优先用functools.lru_cache重试临时故障是但需幂等带参数装饰器注册路由或命令是返回原函数的注册型装饰器数据校验和序列化可以用但别滥用类型注解加 dataclass 可能更合适复杂业务流程编排尽量别用改用显式调用或中间件最后分享一个调试小技巧。写装饰器时如果一时分不清哪层是哪层就在每层函数入口打印自己的函数名。代码写完了再把这些调试 print 删掉。这比盯着闭包代码硬想快得多。装饰器是 Python 里少有的“看起来高深、拆开很简单”的语法花一晚上手写两三个版本配合断点走一遍后面基本就通了。理解它的本质之后你会发现读框架源码时满眼都是它的影子这也算是 Python 学习路上一个非常值得跨过的坎了。