Python装饰器完全指南:原理、实战与避坑
1. 为什么每个Python开发者都应该掌握装饰器我第一次意识到装饰器的威力是在接手一个遗留项目的时候。那个项目里有几十个API接口函数每个函数开头都有三四行一模一样的鉴权代码、参数校验代码、日志记录代码。如果要改鉴权逻辑就得一个函数一个函数去改漏改一个就可能出安全事故。当时我就在想Python号称优雅这种重复代码真的没有更好的解法吗答案就是装饰器。Python装饰器本质上是一个接收函数作为参数、返回新函数的可调用对象它让你在不修改原函数代码的情况下给函数附加新的能力。用装饰器把鉴权、日志、性能统计这些横向逻辑抽离出来业务函数只关心自己的核心逻辑代码的复用性、可读性、可维护性会直接上一个台阶。这篇内容适合三类人刚学完Python基础语法想理解装饰器到底是什么的新手已经在用装饰器但只会抄别人的模板遇到“装饰器带参数”“多个装饰器叠加”就懵的进阶者需要在团队里推广代码规范、想减少重复代码的开发者。不管你是哪一类读完这篇你都能自己写装饰器并且知道什么时候该用、什么时候不该用。2. 先搞懂装饰器的底层逻辑函数也是对象很多教程上来就甩装饰器的语法糖结果读者看得一头雾水。我换个讲法先不讲装饰器先讲Python函数的一个基本特性——函数是对象。理解了这个装饰器就是顺理成章的事。2.1 Python里函数的一等公民地位在Python里函数和整数、字符串、列表一样都是对象。这意味着你可以把函数赋值给一个变量把函数作为参数传给另一个函数在一个函数内部定义另一个函数让函数返回另一个函数。我用一个极简的例子说明def greet(name): return fHello, {name} # 把函数赋值给变量 say_hello greet print(say_hello(Alice)) # 输出: Hello, Alice # 把函数作为参数传入 def call_twice(func, arg): return func(func(arg)) print(call_twice(greet, Bob)) # 输出: Hello, Hello, Bob这段代码里greet和say_hello指向同一个函数对象所以你用say_hello(Alice)就等于在调用greet(Alice)。call_twice接收一个函数作为参数在内部调用它——这就是“函数作为一等公民”的含义。这个特性为什么重要因为装饰器的核心机制就是“把函数传来传去、包来包去”。2.2 闭包装饰器的灵魂光有“函数是对象”还不够装饰器还依赖另一个概念闭包。闭包指的是内层函数引用了外层函数的变量并且外层函数把这个内层函数返回出去。只要内层函数还活着它就能记住外层函数定义时的环境变量即使外层函数已经执行完了。看一个最经典的闭包例子def outer(x): def inner(y): return x y return inner add_5 outer(5) print(add_5(3)) # 输出: 8 print(add_5(10)) # 输出: 15这里outer(5)执行完返回了inner但inner依然能访问x 5这个变量。这个在函数返回后依然保留的变量环境就是闭包的本质。装饰器其实就是闭包的一种典型应用外层函数接收一个函数原函数内层函数里做增强逻辑最后返回内层函数新函数。2.3 手动实现一个装饰器了解了上面这些我们完全不用语法糖手动写一个装饰器看看def my_decorator(func): def wrapper(): print(函数执行前……) func() print(函数执行后……) return wrapper def say_hello(): print(Hello!) say_hello my_decorator(say_hello) say_hello()输出函数执行前…… Hello! 函数执行后……这个过程发生了什么my_decorator(say_hello)把原函数say_hello作为参数传入内部定义了一个新函数wrapper它包裹了原函数的调用并在前后追加了打印逻辑返回wrapper赋值回say_hello这个变量名。于是外界调用的say_hello其实已经是wrapper了只是函数名没变。这就是装饰器的全部奥秘。3. 从原理到语法糖装饰器的标准写法手动实现能让你理解本质但实际写代码当然不会每次都手动赋值。Python提供了语法糖让装饰器的使用变得非常清爽。3.1 语法糖的本质还是上面那个例子用语法糖来写def my_decorator(func): def wrapper(): print(函数执行前……) func() print(函数执行后……) return wrapper my_decorator def say_hello(): print(Hello!) say_hello()my_decorator放在函数定义上面等价于say_hello my_decorator(say_hello)。它就是在函数定义完成后立刻用装饰器包裹一层再绑定回原名字。输出结果和手动版完全一样。注意语法糖只是写法上的简化它并没有改变装饰器的本质逻辑。理解这一点后面遇到“装饰器带参数”“多个装饰器叠加”就更容易拆解。3.2 保留原函数元信息functools.wraps如果你直接运行上面的代码然后打印say_hello.__name__会发现结果是wrapper而不是say_hello。这就带来了一个问题原函数的名称、文档字符串、参数签名等信息全都被wrapper覆盖了。这在调试、生成API文档、做序列化的时候都会造成困扰。解决办法是使用functools.wrapsimport functools def my_decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): print(函数执行前……) result func(*args, **kwargs) print(函数执行后……) return result return wrapper my_decorator def add(a, b): 计算两个数的和 return a b print(add.__name__) # 输出: add print(add.__doc__) # 输出: 计算两个数的和functools.wraps做的事就是把原函数的__name__、__doc__、__module__、__dict__等属性复制到wrapper函数上。我强烈建议你写装饰器时永远加上functools.wraps这是一个好习惯否则调试的时候会有一堆莫名其妙的坑。另外wrapper的形参最好写成*args, **kwargs这样能接收任意位置参数和关键字参数保证装饰器能用在各种签名不同的函数上。3.3 支持任意参数的通用装饰器模板综合上面的要点我给出一个最通用的装饰器模板建议收藏import functools def my_decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): # 执行前逻辑 result func(*args, **kwargs) # 执行后逻辑 return result return wrapper这个模板能应对90%的场景。你只需要在“执行前逻辑”和“执行后逻辑”两个位置填上自己的业务代码即可。4. 装饰器的进阶形态带参数的装饰器基础的装饰器已经能解决很多问题但有时候我们还需要给装饰器本身传参数。比如给一个接口加“限流”装饰器希望限流频率是可配置的或者加“重试”装饰器希望重试次数可配置。这时候就需要三层结构。4.1 三层嵌套的写法带参数的装饰器本质上是在普通装饰器外面再套一层函数用来接收参数。我直接上代码import functools 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(times3) def say_hello(): print(Hello!) say_hello()输出三遍Hello!。执行过程拆解如下调用repeat(times3)返回decoratordecorator包裹原函数等价于say_hello decorator(say_hello)decorator(say_hello)返回wrapper最终say_hello指向的是记住times3的wrapper。所以带参数的装饰器是“三层函数”外层接收参数中间层接收函数内层增强逻辑。4.2 参数形式兼容两种用法如果你希望一个装饰器既能带参数又能不带参数使用比如repeat和repeat(times3)都支持需要多写一点判断逻辑import functools def repeat(funcNone, *, times2): if func is None: # 被 repeat(times3) 使用 def decorator(f): functools.wraps(f) def wrapper(*args, **kwargs): for _ in range(times): f(*args, **kwargs) return wrapper return decorator else: # 被 repeat 直接使用 functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(times): func(*args, **kwargs) return wrapper repeat(times3) def a(): print(a) repeat def b(): print(b)这个写法里用了一个技巧*后面的参数必须是关键字参数这样times不会和func混淆。这种兼容写法在开源项目里很常见但如果你只是自己用建议明确一种用法不要过度设计。4.3 用类实现装饰器函数能实现装饰器类也能。类装饰器的核心是让实例对象可调用也就是实现__call__方法。上代码import functools class Retry: def __init__(self, max_retries3): self.max_retries max_retries def __call__(self, func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(self.max_retries): try: return func(*args, **kwargs) except Exception as e: if attempt self.max_retries - 1: raise print(f第{attempt 1}次失败重试……) return wrapper Retry(max_retries5) def unstable_network_call(): # 模拟可能失败的调用 pass用类的优势在于可以方便地把状态存在实例属性里比如计数器、配置项也便于和其他面向对象的设计配合。缺点是读起来没有函数嵌套那么直观。两种方式没有绝对优劣我个人推荐简单场景用函数式需要维护状态的复杂场景用类。5. 装饰器实战六个高频场景的完整实现讲了这么多原理不落地就是纸上谈兵。下面我挑六个最常用的实战场景每个都给出完整可运行的代码你在自己的项目里稍作调整就能直接用。5.1 函数执行时间统计性能调优时最常用的工具比手写time.time()要干净得多import functools import time def timer(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) elapsed time.perf_counter() - start print(f{func.__name__} 执行耗时: {elapsed:.4f} 秒) return result return wrapper timer def slow_task(): time.sleep(0.5) return done slow_task() # 输出: slow_task 执行耗时: 0.5001 秒注意我用的是time.perf_counter()而不是time.time()。perf_counter提供最高可用精度的计时器适合测量短时间间隔time.time()更适合获取墙上时钟时间。在性能测试场景两者精度差别很大。5.2 登录鉴权与权限校验Web开发里最常见的需求。假设函数是某个接口的处理函数我们需要确认用户已经登录、并且有权限执行操作import functools def login_required(func): functools.wraps(func) def wrapper(*args, **kwargs): user get_current_user() if user is None: raise PermissionError(请先登录) return func(*args, **kwargs) return wrapper def permission_required(permission): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): user get_current_user() if user is None: raise PermissionError(请先登录) if permission not in user.permissions: raise PermissionError(f缺少权限: {permission}) return func(*args, **kwargs) return wrapper return decorator login_required def view_profile(): return 个人资料 permission_required(admin) def delete_user(user_id): return f用户 {user_id} 已删除在真实项目中鉴权逻辑往往要读取数据库、查询权限表这部分代码抽到装饰器里业务函数就能专注处理数据和返回结果不用再担心鉴权问题。5.3 缓存函数计算结果对于计算成本高、且输入输出确定的函数缓存能大幅提升性能。Python内置的functools.lru_cache就是干这个的import functools functools.lru_cache(maxsize128) def fibonacci(n): if n 2: return n return fibonacci(n - 1) fibonacci(n - 2) print(fibonacci(50)) # 瞬间返回结果没有缓存的话fibonacci(50)会递归计算数亿次有了lru_cache每个n只算一次速度提升几个数量级。如果你想自己实现一个简单的缓存装饰器也非常直观import functools def memoize(func): cache {} functools.wraps(func) def wrapper(*args): if args in cache: return cache[args] result func(*args) cache[args] result return result return wrapper自己实现缓存时要注意缓存的key必须可哈希所以args里的元素必须都是可哈希类型。如果参数里有列表、字典直接用args当key就会报错。5.4 重试机制网络请求、数据库连接这类操作天然具有瞬时失败的可能。重试装饰器能显著提升系统稳定性import functools import time def retry(max_retries3, delay1, exceptions(Exception,)): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_retries 1): try: return func(*args, **kwargs) except exceptions as e: if attempt max_retries: raise print(f第{attempt}次失败: {e}{delay}秒后重试……) time.sleep(delay) return wrapper return decorator retry(max_retries3, delay0.5, exceptions(ConnectionError, TimeoutError)) def fetch_data(): # 模拟网络请求 pass这里有几个细节值得注意exceptions参数用来指定只重试哪些异常避免把编程错误比如TypeError也盲目重试重试次数用尽后要raise抛出原始异常而不是静默吞掉否则调用方会以为成功了重试之间的delay可以固定也可以改成指数退避后者对下游服务的压力更小。5.5 日志记录与异常捕获给关键函数加上日志装饰器能统一记录函数名、参数、返回值、耗时、异常信息import functools import logging logging.basicConfig(levellogging.INFO) def log_call(func): functools.wraps(func) def wrapper(*args, **kwargs): logging.info(f调用 {func.__name__}, args{args}, kwargs{kwargs}) try: result func(*args, **kwargs) logging.info(f{func.__name__} 返回值: {result}) return result except Exception as e: logging.exception(f{func.__name__} 抛出异常: {e}) raise return wrapper log_call def divide(a, b): return a / b divide(10, 2) # 正常记录 # divide(10, 0) # 异常也会被记录注意logging.exception只能在except块里使用它会自动附加上当前的异常堆栈信息排查问题时非常有用。5.6 输入参数校验在数据入口处统一做参数校验能避免脏数据进入核心逻辑import functools def validate_args(**validators): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): # 把位置参数和关键字参数合并成字典 all_args kwargs.copy() func_params list(func.__code__.co_varnames[:func.__code__.co_argcount]) for name, value in zip(func_params, args): all_args[name] value for field, validator in validators.items(): if field in all_args: validator(field, all_args[field]) return func(*args, **kwargs) return wrapper return decorator def check_positive(field, value): if value 0: raise ValueError(f{field} 必须为正数当前值: {value}) validate_args(pricecheck_positive, quantitycheck_positive) def create_order(price, quantity): return f订单金额: {price * quantity} print(create_order(10, 2)) # 正常 # print(create_order(-5, 2)) # 抛出 ValueError这个实现里我用了func.__code__.co_varnames来获取函数参数名然后把位置参数映射进去。这个技巧在写通用装饰器时很常见值得记一下。如果你用的是Python 3.10也可以用inspect.signature来做更复杂的参数绑定。6. 容易踩的坑装饰器的叠加顺序和常见陷阱装饰器用多了自然就会遇到各种诡异的问题。我在生产和开源项目里踩过不少坑这里挑几个最有代表性的讲一讲。6.1 多个装饰器的执行顺序先看例子def decorator_a(func): print(进入 decorator_a) functools.wraps(func) def wrapper(*args, **kwargs): print(执行 decorator_a wrapper) return func(*args, **kwargs) return wrapper def decorator_b(func): print(进入 decorator_b) functools.wraps(func) def wrapper(*args, **kwargs): print(执行 decorator_b wrapper) return func(*args, **kwargs) return wrapper decorator_a decorator_b def hello(): print(hello)运行结果是进入 decorator_b 进入 decorator_a 执行 decorator_a wrapper 执行 decorator_b wrapper hello这里有两个关键点装饰器函数本身外层是自下而上执行的先执行decorator_b再执行decorator_a。所以你会看到先打印“进入 decorator_b”。被包装后的函数wrapper是自上而下执行的调用hello()时先进入decorator_a的wrapper再进入decorator_b的wrapper最后才执行真正的hello函数。这个顺序怎么记我习惯把它理解为“洋葱模型”装饰器从外层向内层包裹调用时从外层向内层穿透。注意装饰器叠加顺序不同行为完全不同。比如login_required和timer叠加如果timer在外层就会连带把鉴权时间也算进去如果login_required在外层未登录用户根本不会触发计时。实际项目里要结合业务语义仔细选择顺序。6.2 忘记 functools.wraps 的后果很多人初学装饰器时不加functools.wraps短期内程序能跑但长期会有几个问题被装饰函数的__name__变成了wrapper日志和调试信息失去可读性被装饰函数的__doc__丢失自动生成的API文档会一片混乱当多个装饰器叠加时元信息会层层丢失排查问题像在拆盲盒。有些框架底层就是依赖函数签名的。比如FastAPI就是通过参数签名来生成API接口文档的如果你的装饰器不加functools.wraps直接把__wrapped__链弄断了框架可能无法正确解析参数接口文档就会出错。这不是危言耸听我是真的见过生产环境因为这个原因导致接口文档残缺的案例。6.3 装饰器参数里有可变对象如果你的装饰器内部维护了可变状态比如列表、字典并且不希望多个函数共享这个状态要格外小心。看这个例子def track(func, storage[]): # 默认参数 storage 是共享的 ...Python的默认参数在函数定义时就被求值一次之后所有调用共享同一个列表。如果你在每个装饰器里都往这个列表里append你会发现所有被装饰的函数共享了同一份记录。这就是经典的“可变默认参数陷阱”。更安全的做法是使用None占位def track(func, storageNone): if storage is None: storage [] ...6.4 类方法装饰器的self问题当你给类方法加装饰器时要注意self参数的传递。比如def log_call(func): functools.wraps(func) def wrapper(*args, **kwargs): print(f调用 {func.__name__}) return func(*args, **kwargs) return wrapper class Service: log_call def process(self, data): return f处理 {data}这里process被装饰后wrapper接收*args其中args[0]就是self所以func(*args, **kwargs)能正确传给原方法。只要你的wrapper用*args, **kwargs类方法一般没问题。但如果装饰器里硬编码了参数比如wrapper(self)那就只能装饰固定签名的类方法了。通用做法还是尽量保持*args, **kwargs。6.5 用inspect保留函数签名functools.wraps能保留__name__、__doc__等属性但保留不了完整的参数签名。也就是说help()函数和某些框架查看函数签名时看到的还是*args, **kwargs。如果你需要精确保留签名可以用inspect.signature手动设置或者直接使用第三方库decorator。不过说实话绝大多数场景下functools.wraps已经够用了追求完美签名属于锦上添花。7. 几个容易混淆的概念装饰器模式、语法糖、闭包写装饰器相关代码久了大家总会碰到一些概念混淆。我经常在面试里问这几个区别发现很多人说不清楚这里一并讲透。7.1 装饰器模式 vs Python装饰器装饰器模式是面向对象设计模式的一种最早来自《设计模式》那本书。它通过“包装”一个对象在不修改原类代码的情况下扩展它的行为。比如给一个文件读写类套一个带加密功能的包装类这就是装饰器模式。Python装饰器是一种语言层面的语法特性它的核心是语法糖和闭包。不过Python装饰器也可以用来实现装饰器模式两者并不冲突。它们的关系可以理解为装饰器模式是一种设计思想Python装饰器是一种具体实现工具。在Python里你既可以用函数装饰器也可以用类实现装饰器模式甚至很多设计模式的书里都推荐用装饰器来替代继承。7.2 语法糖 vs 装饰器语法语法糖Syntactic Sugar指的是编程语言中为了简化写法而设计的语法它不增加新功能只是让代码更易读。语法糖就是装饰器的语法糖它隐藏了“把原函数传进去、把新函数绑回去”的过程。但要注意不是所有“用修饰”的东西都是装饰器。比如staticmethod、classmethod、property它们也是语法糖但本质是Python内置的装饰器其背后逻辑也是“接收函数返回对象”这一点和自定义装饰器完全一致。7.3 装饰器 vs 闭包闭包是装饰器实现的技术基础但闭包本身是一个更宽泛的概念只要内层函数引用了外层函数的变量并且外层函数返回了内层函数就构成了闭包。装饰器是闭包的一种典型应用但闭包还有很多其他用途比如生成器、偏函数、工厂函数等。所以不要把它们划等号。8. 装饰器的实用技巧三个能直接抄的进阶写法基础会了坑也讲了最后再分享三个进阶技巧。这些技巧我平时写代码经常用能让代码再优雅一个档次。8.1 给装饰器加类型注解Python 3.10支持更精确的函数注解装饰器也可以标注类型。比如from collections.abc import Callable from typing import Any, TypeVar F TypeVar(F, boundCallable[..., Any]) def timer(func: F) - F: functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) - Any: ... return wrapper这里用了TypeVar来保持被装饰函数的类型信息不变。加上类型注解之后IDE的智能提示不会被破坏代码库的可维护性也更好。8.2 用__wrapped__属性追踪原函数如果你在被装饰函数上用了functools.wrapsPython会自动设置一个__wrapped__属性指向原始函数my_decorator def add(a, b): return a b print(add.__wrapped__(1, 2)) # 直接调用原函数绕过了装饰器这在单元测试中非常有用你可以绕过装饰器直接测试业务函数的原始逻辑避免日志、计时等副作用干扰测试结果。8.3 把装饰器和dataclass结合Python 3.7的dataclass和装饰器是绝配。比如给数据类加上一个“从字典创建实例”的功能from dataclasses import dataclass def from_dict(cls): def _from_dict(data): return cls(**data) cls.from_dict _from_dict return cls from_dict dataclass class User: name: str age: int user User.from_dict({name: Alice, age: 30}) print(user) # 输出: User(nameAlice, age30)这个装饰器用动态给类添加了一个from_dict方法。注意from_dict是绑定类而不是类的实例第一个参数是cls而不是self。这种用法打破了传统装饰器只能用在函数上的认知——装饰器可以装饰任何可调用对象包括类。9. 我现在是怎么看待装饰器的写代码这些年装饰器是我用得最频繁的Python特性之一。从最初的“照着模板抄”到后来理解闭包原理再到能自己设计带参数的装饰器这个过程其实并不复杂关键是把底层的函数、对象、闭包三个概念打通。我现在写代码的原则是装饰器适合解决横切关注点不适合过度使用。日志、鉴权、缓存、重试这些和业务无关的逻辑放到装饰器里是合理的但如果你发现装饰器把代码变得难以阅读、难以调试那就该停下来想想是不是用错了地方。装饰器是让你写得更优雅的工具不是让你炫技的道具。最后分享一个小技巧如果你在调试一个被多层装饰器包裹的函数别慌。先在装饰器的wrapper里加上print(func.__name__)或者在调用前访问func.__wrapped__很快就能定位是第几层的问题。我踩过的坑告诉我大部分装饰器相关的诡异bug都出在“忘了加functools.wraps”或者“装饰器叠加顺序不符合预期”这两件事上。这两个坑你提前注意了后面省下的调试时间不是一点半点。