一、核心概念
1. 什么是装饰器
装饰器(Decorator)本质上是一个高阶函数——它接收一个函数作为参数,并返回一个新的函数。它允许你在不修改原函数代码和调用方式的前提下,为函数动态添加额外功能。
2. 设计原则
装饰器遵循开放-封闭原则:
对扩展开放:允许新增功能
对修改封闭:不需要修改原函数内部代码
3. 语法糖
使用@decorator_name放在函数定义上方,是以下写法的简洁形式:
@decorator def func(): pass # 等价于 def func(): pass func = decorator(func)二、前置知识
要理解装饰器,必须先掌握两个概念:
1. 函数是一等公民
在 Python 中,函数可以像变量一样被赋值、作为参数传递、作为返回值。这是装饰器存在的基础。
2. 闭包(Closure)
在一个内部函数中,对外部作用域(非全局作用域)的变量进行引用,这个内部函数就是闭包。装饰器的核心实现正是利用了闭包的特性。
三、工作原理解析
def my_decorator(func): def wrapper(*args, **kwargs): # 执行前的操作 print("调用函数前") result = func(*args, **kwargs) # 执行原函数 # 执行后的操作 print("调用函数后") return result return wrapper @my_decorator def say_hello(name): print(f"Hello, {name}!") say_hello("Alice") # 输出: # 调用函数前 # Hello, Alice! # 调用函数后执行流程:
- Python 解释器遇到
@my_decorator - 执行
say_hello = my_decorator(say_hello) - 现在
say_hello实际指向wrapper函数 - 调用时执行
wrapper中的逻辑
四、关键细节:functools.wraps
使用装饰器后,原函数的元信息(如__name__、__doc__)会被wrapper覆盖,影响调试和文档生成。
解决方案:使用functools.wraps。
from functools import wraps def my_decorator(func): @wraps(func) # 保留原函数的 __name__ 和 docstring def wrapper(*args, **kwargs): """这是 wrapper 的文档""" return func(*args, **kwargs) return wrapper @my_decorator def hello(): """这是 hello 的文档""" pass print(hello.__name__) # 输出: hello (没有 wraps 则输出 wrapper) print(hello.__doc__) # 输出: 这是 hello 的文档五、典型案例
案例 1:计时装饰器(性能分析)
快速测量函数执行时间,是最常见的应用场景之一。
import time import functools 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"[TIMER] {func.__name__} 耗时: {elapsed:.4f} 秒") return result return wrapper @timer def slow_function(): time.sleep(1) return "完成" slow_function() # 输出: [TIMER] slow_function 耗时: 1.0002 秒案例 2:日志记录装饰器
记录函数的调用信息和返回值,是最常见的应用场景之一。
import functools def log(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"[LOG] 调用 {func.__name__},参数: {args}, {kwargs}") result = func(*args, **kwargs) print(f"[LOG] {func.__name__} 返回: {result}") return result return wrapper @log def add(a, b): return a + b add(2, 3) # 输出: # [LOG] 调用 add,参数: (2, 3), {} # [LOG] add 返回: 5案例 3:登录权限验证(Web 开发常见)
检查用户登录态或角色权限,是 Web 开发中的典型用法。
import functools def login_required(func): @functools.wraps(func) def wrapper(user, *args, **kwargs): if not user.get("is_login", False): raise PermissionError("用户未登录,请先登录") return func(user, *args, **kwargs) return wrapper @login_required def view_profile(user): print(f"查看用户: {user['name']} 的个人资料") # 测试 user_a = {"name": "张三", "is_login": True} user_b = {"name": "李四", "is_login": False} view_profile(user_a) # 正常执行 # view_profile(user_b) # 抛出 PermissionError参数传递原理解析
在这个代码中,wrapper(user, *args, **kwargs)里的user是wrapper函数的第一个形参,它不是从别处"继承"来的,而是由调用者在调用被装饰后的函数时传入的实参。
1. 装饰器语法糖展开
# 下面的写法: @login_required def view_profile(user): print(f"查看用户: {user['name']} 的个人资料") # 等价于: def view_profile(user): print(f"查看用户: {user['name']} 的个人资料") view_profile = login_required(view_profile)2. 执行后的结果
login_required(view_profile)执行后,返回了内部定义的wrapper函数此时变量名
view_profile不再指向原来的函数,而是指向了wrapper这个新函数
所以当你最后调用view_profile(user_a)时,实际上调用的是:
wrapper(user_a) # 因为 wrapper 的第一个参数名叫 user,所以 user_a 就传给了它3.user参数的来源总结
定义来源:
wrapper函数定义时自己声明的第一个位置参数值来源:来自于外部调用时传入的第一个实参(在上例中是
user_a字典)
4. 为什么要这样设计?
因为被装饰的原函数view_profile本身就需要一个user参数,而装饰器wrapper为了能"替换"原函数并正常工作,必须保持相同的调用签名(即接收同样的参数),然后把这些参数原封不动地传递给原函数func:
wrapper(user, *args, **kwargs)负责接收所有传入参数func(user, *args, **kwargs)负责把参数传给真正的业务函数
如果原函数有多个参数,比如def view_profile(user, age),那么调用时age就会通过*args或**kwargs捕获并传递过去。
补充说明:
user在这里只是一个普通的参数名,你可以把它改成u或person,效果完全一样(只要内部一致即可)。它和面向对象中的"继承"没有任何关系。
案例 4:重试机制(处理网络请求)
在网络请求等不稳定场景中,失败后自动重试。
import time import functools def retry(max_attempts=3, delay=1): 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 e print(f"第 {attempt} 次失败,{delay}秒后重试...") time.sleep(delay) return None return wrapper return decorator @retry(max_attempts=3, delay=0.5) def unstable_request(): import random if random.random() < 0.7: raise ConnectionError("网络不稳定") return "请求成功" # unstable_request()代码结构总览
这段代码实现了三层嵌套函数,因为装饰器本身需要接收参数:
retry(max_attempts, delay) → 返回 decorator 函数 decorator(func) → 返回 wrapper 函数 wrapper(*args, **kwargs) → 实际执行逻辑逐层详细拆解
第1层:retry(max_attempts=3, delay=1)
def retry(max_attempts=3, delay=1): def decorator(func): # ... return decorator作用:这是装饰器的工厂函数,接收配置参数,返回真正的装饰器
参数说明:
max_attempts=3:最大尝试次数(默认3次)delay=1:每次重试的等待秒数(默认1秒)
关键点:因为装饰器本身需要参数,所以要多一层函数来接收这些参数
第2层:decorator(func)
def decorator(func): def wrapper(*args, **kwargs): # ... return wrapper作用:这是真正的装饰器,接收被装饰的函数
func,返回包装后的函数wrapper闭包特性:
decorator内部可以访问外层retry的max_attempts和delay参数
第3层:wrapper(*args, **kwargs)
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 e print(f"第 {attempt} 次失败,{delay}秒后重试...") time.sleep(delay) return None- 作用:包装后的函数,包含重试逻辑
逐行解析:
循环控制:
for attempt in range(1, max_attempts + 1),attempt从 1 到max_attempts(包含)尝试执行:
try: return func(*args, **kwargs),如果成功直接return结果,不继续循环异常处理:
捕获任何异常
如果是最后一次尝试(
attempt == max_attempts),直接抛出异常raise e否则打印提示并等待
delay秒后继续循环
兜底返回:
return None,理论上不会执行到这里,仅作为语法完整性保留
应用到具体函数
@retry(max_attempts=3, delay=0.5) def unstable_request(): import random if random.random() < 0.7: raise ConnectionError("网络不稳定") return "请求成功"等价于:
def unstable_request(): # ... 原函数代码 unstable_request = retry(max_attempts=3, delay=0.5)(unstable_request) # 等价于: # decorator = retry(max_attempts=3, delay=0.5) # unstable_request = decorator(unstable_request)完整执行流程示例
假设random.random()返回 0.5(小于0.7,触发异常):
unstable_request() # ↓ # 进入 wrapper,attempt = 1 # try: 执行原函数 → 抛出 ConnectionError # except: attempt=1 != 3,打印 "第 1 次失败,0.5秒后重试..." # 等待 0.5 秒 # ↓ # 进入下一轮循环,attempt = 2 # try: 执行原函数 → 又抛出 ConnectionError # except: attempt=2 != 3,打印 "第 2 次失败,0.5秒后重试..." # 等待 0.5 秒 # ↓ # 进入下一轮循环,attempt = 3 # try: 执行原函数 → 再次抛出 ConnectionError # except: attempt=3 == 3,执行 raise e,抛出异常 # ↓ # 程序崩溃,显示 ConnectionError: 网络不稳定如果第3次成功了(假设random.random()返回 0.8):
# attempt = 3 # try: 执行原函数 → 返回 "请求成功" # return "请求成功"(直接退出函数) # ↓ # 最终输出:请求成功关键设计思想
1. 闭包(Closure)
retry → decorator → wrapper每一层都能访问外层变量,形成了闭包链。
2. 参数传递路径
max_attempts=3, delay=0.5 ↓ decorator 捕获这些参数 ↓ wrapper 使用这些参数控制重试逻辑3. 透明传递
使用*args和**kwargs确保任何参数都能传递给原函数,装饰器不关心具体参数。
4. 异常处理策略
不吞异常:最后尝试失败时,重新抛出原始异常
保留堆栈:使用
raise e而不是raise,但raise e会重置堆栈信息(更好的做法是直接raise)
改进建议
如果想保留完整的异常堆栈,可以改为:
except Exception as e: if attempt == max_attempts: raise # 不传 e,保留完整堆栈更健壮的版本可以添加指数退避:
time.sleep(delay * (2 ** (attempt - 1))) # 0.5, 1.0, 2.0...总结对比
| 组件 | 职责 | 参数 |
|---|---|---|
retry | 配置工厂 | max_attempts, delay |
decorator | 装饰器主体 | func |
wrapper | 包装逻辑 | *args, **kwargs |
这种三层结构是带参数装饰器的标准写法,核心思想是将配置与逻辑分离,使装饰器既灵活又复用性强。
案例 5:缓存计算结果(Memoization)
利用闭包保存状态,避免重复计算,大幅提升性能。
import functools def memoize(func): cache = {} @functools.wraps(func) def wrapper(n): if n not in cache: cache[n] = func(n) return cache[n] return wrapper @memoize def fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(35)) # 第一次稍慢,后续极快实际项目中可直接使用内置的
@functools.lru_cache。
案例 6:带参数的装饰器(日志级别控制)
当装饰器本身需要参数时,需要三层嵌套(工厂模式)。
import functools def log(level="INFO"): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"[{level}] 调用函数: {func.__name__}") return func(*args, **kwargs) return wrapper return decorator @log(level="ERROR") def dangerous_operation(): print("执行危险操作") dangerous_operation() # 输出: [ERROR] 调用函数: dangerous_operation逻辑链条:
@log(level="ERROR")先执行,返回decoratordecorator(dangerous_operation)执行,返回wrapperdangerous_operation()实际调用的是wrapper
案例 7:类装饰器(单例模式)
用类实现装饰器,可以管理更复杂的状态。
import functools class Singleton: def __init__(self, cls): self.cls = cls self.instance = None def __call__(self, *args, **kwargs): if self.instance is None: self.instance = self.cls(*args, **kwargs) return self.instance @Singleton class Database: def __init__(self): print("初始化数据库连接...") db1 = Database() # 初始化数据库连接... db2 = Database() print(db1 is db2) # True,始终是同一个实例案例 8:类装饰器(调用计数)
利用__call__方法使实例可调用,实现对函数调用次数的统计。
import functools class CountCalls: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print(f"{self.func.__name__} 已被调用 {self.count} 次") return self.func(*args, **kwargs) @CountCalls def greet(name): return f"Hello, {name}!" greet("Alice") greet("Bob") # 输出: # greet 已被调用 1 次 # greet 已被调用 2 次六、多个装饰器的执行顺序
当多个装饰器叠加时,从下往上"包裹",从上往下"执行":
@timer # 后执行外层(后包裹),先执行外层逻辑 @log(level="DEBUG") # 先被包裹(内层),后执行内层逻辑 def add(a, b): time.sleep(0.1) return a + b # 等价于: add = timer(log(level="DEBUG")(add)) # 执行顺序: 先执行 log 的 wrapper 逻辑,再执行 timer 的 wrapper 逻辑 # 实际输出: # [DEBUG] 调用 add,参数: (3, 5), {} # [DEBUG] add 返回: 8 # [TIMER] add 耗时: 0.1005 秒七、常见应用场景
| 场景 | 说明 | 关键技术 |
|---|---|---|
| 日志记录 | 自动记录函数调用信息和参数 | 两层嵌套 +wraps |
| 性能监控 | 统计执行时间、调用次数 | 计时装饰器 |
| 权限校验 | 检查用户登录态或角色权限 | 带参数的装饰器 |
| 重试与降级 | 失败后自动重试或返回默认值 | 三层嵌套(工厂模式) |
| 缓存 | 减少重复计算,提高响应速度 | 闭包字典存状态 |
| 事务管理 | 数据库操作的自动提交/回滚 | 装饰器模式 |
| 限流 | 控制函数调用频率 | 装饰器 + 状态管理 |
| 单例模式 | 确保类只有一个实例 | 类装饰器 |
八、Python 标准库中常用的装饰器
| 装饰器 | 说明 |
|---|---|
@functools.lru_cache | 函数结果缓存 |
@staticmethod | 定义静态方法 |
@classmethod | 定义类方法 |
@property | 属性访问控制 |
@functools.singledispatch | 基于类型的函数重载 |
@functools.wraps | 保留原函数元信息 |
九、总结
装饰器的核心价值在于分离关注点:
核心业务逻辑保持纯粹
横切关注点(日志、验证、缓存、计时等)通过装饰器叠加
掌握装饰器需要注意的关键点:
- 本质:装饰器是高阶函数,接收函数,返回新函数
- 语法糖:
@decorator只是func = decorator(func)的简写 - 规范:务必使用
@functools.wraps(func)保留原函数属性 - 参数:处理原函数参数用
*args, **kwargs;处理装饰器参数需要多一层嵌套 - 场景:日志、计时、缓存、权限校验、重试机制、事务处理等横切关注点
装饰器的精髓在于**"开闭原则"**——对扩展开放,对修改封闭。当你发现多个函数需要重复编写同一段前置/后置逻辑时,就是该用装饰器的时候了。掌握装饰器,你的 Python 代码将从"过程式脚本"迈向"Pythonic 的工程化代码"。