1. 为什么每个Python开发者都应该掌握装饰器
我第一次意识到装饰器的威力,是在接手一个遗留项目的时候。那个项目里有几十个API接口函数,每个函数开头都有三四行一模一样的鉴权代码、参数校验代码、日志记录代码。如果要改鉴权逻辑,就得一个函数一个函数去改,漏改一个就可能出安全事故。当时我就在想,Python号称优雅,这种重复代码真的没有更好的解法吗?
答案就是装饰器。
Python装饰器本质上是一个接收函数作为参数、返回新函数的可调用对象,它让你在不修改原函数代码的情况下,给函数附加新的能力。用装饰器把鉴权、日志、性能统计这些横向逻辑抽离出来,业务函数只关心自己的核心逻辑,代码的复用性、可读性、可维护性会直接上一个台阶。
这篇内容适合三类人:
- 刚学完Python基础语法,想理解装饰器到底是什么的新手;
- 已经在用装饰器,但只会抄别人的模板,遇到“装饰器带参数”“多个装饰器叠加”就懵的进阶者;
- 需要在团队里推广代码规范、想减少重复代码的开发者。
不管你是哪一类,读完这篇,你都能自己写装饰器,并且知道什么时候该用、什么时候不该用。
2. 先搞懂装饰器的底层逻辑:函数也是对象
很多教程上来就甩装饰器的语法糖,结果读者看得一头雾水。我换个讲法:先不讲装饰器,先讲Python函数的一个基本特性——函数是对象。理解了这个,装饰器就是顺理成章的事。
2.1 Python里函数的一等公民地位
在Python里,函数和整数、字符串、列表一样,都是对象。这意味着你可以:
- 把函数赋值给一个变量;
- 把函数作为参数传给另一个函数;
- 在一个函数内部定义另一个函数;
- 让函数返回另一个函数。
我用一个极简的例子说明:
def greet(name): return f"Hello, {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.wraps:
import 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(times=3) def say_hello(): print("Hello!") say_hello()输出三遍Hello!。执行过程拆解如下:
- 调用
repeat(times=3),返回decorator; @decorator包裹原函数,等价于say_hello = decorator(say_hello);decorator(say_hello)返回wrapper;- 最终
say_hello指向的是记住times=3的wrapper。
所以带参数的装饰器是“三层函数”:外层接收参数,中间层接收函数,内层增强逻辑。
4.2 参数形式兼容两种用法
如果你希望一个装饰器既能带参数又能不带参数使用(比如@repeat和@repeat(times=3)都支持),需要多写一点判断逻辑:
import functools def repeat(func=None, *, times=2): if func is None: # 被 @repeat(times=3) 使用 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(times=3) def a(): print("a") @repeat def b(): print("b")这个写法里用了一个技巧:*后面的参数必须是关键字参数,这样times不会和func混淆。这种兼容写法在开源项目里很常见,但如果你只是自己用,建议明确一种用法,不要过度设计。
4.3 用类实现装饰器
函数能实现装饰器,类也能。类装饰器的核心是让实例对象可调用,也就是实现__call__方法。上代码:
import functools class Retry: def __init__(self, max_retries=3): 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_retries=5) 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(maxsize=128) 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_retries=3, delay=1, 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_retries=3, delay=0.5, exceptions=(ConnectionError, TimeoutError)) def fetch_data(): # 模拟网络请求 pass这里有几个细节值得注意:
exceptions参数用来指定只重试哪些异常,避免把编程错误(比如TypeError)也盲目重试;- 重试次数用尽后,要
raise抛出原始异常,而不是静默吞掉,否则调用方会以为成功了; - 重试之间的
delay可以固定,也可以改成指数退避,后者对下游服务的压力更小。
5.5 日志记录与异常捕获
给关键函数加上日志装饰器,能统一记录函数名、参数、返回值、耗时、异常信息:
import functools import logging logging.basicConfig(level=logging.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(price=check_positive, quantity=check_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, storage=None): 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", bound=Callable[..., Any]) def timer(func: F) -> F: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: ... return wrapper这里用了TypeVar来保持被装饰函数的类型信息不变。加上类型注解之后,IDE的智能提示不会被破坏,代码库的可维护性也更好。
8.2 用__wrapped__属性追踪原函数
如果你在被装饰函数上用了functools.wraps,Python会自动设置一个__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(name='Alice', age=30)这个装饰器用@动态给类添加了一个from_dict方法。注意from_dict是绑定类而不是类的实例,第一个参数是cls而不是self。这种用法打破了传统装饰器只能用在函数上的认知——装饰器可以装饰任何可调用对象,包括类。
9. 我现在是怎么看待装饰器的
写代码这些年,装饰器是我用得最频繁的Python特性之一。从最初的“照着模板抄”,到后来理解闭包原理,再到能自己设计带参数的装饰器,这个过程其实并不复杂,关键是把底层的函数、对象、闭包三个概念打通。
我现在写代码的原则是:装饰器适合解决横切关注点,不适合过度使用。日志、鉴权、缓存、重试这些和业务无关的逻辑,放到装饰器里是合理的;但如果你发现装饰器把代码变得难以阅读、难以调试,那就该停下来想想是不是用错了地方。装饰器是让你写得更优雅的工具,不是让你炫技的道具。
最后分享一个小技巧:如果你在调试一个被多层装饰器包裹的函数,别慌。先在装饰器的wrapper里加上print(func.__name__),或者在调用前访问func.__wrapped__,很快就能定位是第几层的问题。我踩过的坑告诉我,大部分装饰器相关的诡异bug,都出在“忘了加functools.wraps”或者“装饰器叠加顺序不符合预期”这两件事上。这两个坑,你提前注意了,后面省下的调试时间不是一点半点。