一、装饰器是什么?
装饰器(Decorator)是 Python 提供的一种在不修改原函数(或类)源代码的前提下,动态地为其添加额外功能的语法机制。它本质上是一个接收函数作为参数,并返回一个新函数的高阶函数,通过 `@decorator_name` 语法糖应用到目标对象上。
二、为什么需要装饰器?
看一个经典的对比场景:假设我们要给多个函数都加上"打印执行日志"的功能。
如果不使用装饰器:
# ❌ 不使用装饰器:每个函数都要手动写重复代码def add(a, b): print("函数 add 开始执行") result = a + b print("函数 add 执行结束") return resultdef sub(a, b): print("函数 sub 开始执行") result = a - b print("函数 sub 执行结束") return result# 每加一个函数都要复制粘贴日志代码 → 违反 DRY 原则
如果使用装饰器:
# ✅ 使用高阶函数:可以复用逻辑,但调用方式改变了def log(func): def wrapper(a, b): print(f"函数 {func.__name__} 开始执行") result = func(a, b) print(f"函数 {func.__name__} 执行结束") return result return wrapperadd = log(add) # 必须手动重新赋值,不够优雅# ✅ 使用装饰器:简洁、优雅、非侵入式@logdef add(a, b): return a + b# @log 等价于 add = log(add),一行搞定
三、装饰器的工作原理
装饰器背后依赖 Python 的两个核心特性:
特性 | 说明 |
函数是一等对象 | 函数可以被赋值、作为参数传递、作为返回值 |
闭包(Closure) | 内部函数可以捕获并记住外部函数作用域中的变量 |
语法糖的等价转换
@decoratordef func(): pass# 完全等价于:def func(): passfunc = decorator(func)
装饰器的三要素
- 接收函数:装饰器把被装饰的函数作为参数
- 包装函数:内部定义一个 `wrapper` 来增强原函数
- 返回函数:返回 `wrapper` 替代原函数
四、手写一个装饰器 —— 三个由浅入深的例子
例子 1:日志装饰器(基础版)
最经典的入门场景:为函数自动打印调用日志。
from functools import wrapsdef log(func): @wraps(func) # 保留原函数的 __name__、__doc__ 等元信息 def wrapper(*args, **kwargs): print(f"[LOG] 调用 {func.__name__},参数: args={args}, kwargs={kwargs}") result = func(*args, **kwargs) print(f"[LOG] {func.__name__} 返回: {result}") return result return wrapper# 使用@logdef add(a, b): """两数相加""" return a + badd(3, 5)print(add.__name__) # 因为用了 @wraps,输出仍然是 'add'
运行输出:
[LOG] 调用 add,参数: args=(3, 5), kwargs={}[LOG] add 返回: 8add
💡 关键点:`@wraps(func)` 用于保留原函数的元信息(如 `name`、`doc`),是编写装饰器的最佳实践。
例子 2:计时装饰器(实用版)
统计函数运行耗时,是性能分析中非常常用的工具。
import timefrom functools import wrapsdef timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) elapsed = time.time() - start print(f"函数 {func.__name__} 耗时: {elapsed:.4f} 秒") return result return wrapper# 使用@timerdef heavy_calc(n): return sum(i * i for i in range(n))heavy_calc(1_000_000)
输出:
函数 heavy_calc 耗时: 0.0812 秒
例子 3:带参数的装饰器(进阶版)
有时我们希望装饰器本身也能接收参数(比如"重试次数"、"日志级别")。这时需要多套一层函数。
from functools import wrapsimport timedef retry(times=3, delay=1): """带参数的装饰器:失败自动重试""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): last_exc = None for i in range(1, times + 1): try: return func(*args, **kwargs) except Exception as e: last_exc = e print(f"[第 {i} 次尝试失败] {func.__name__}: {e}") time.sleep(delay) print(f"[放弃] {func.__name__} 已重试 {times} 次仍失败") raise last_exc return wrapper return decorator# 使用@retry(times=3, delay=0.5)def unstable_request(): import random if random.random() < 0.7: raise ConnectionError("网络不稳定") return "请求成功"print(unstable_request())
可能的输出(此处有随机值,所以每次输出结果不一致,可多运行几次查看结果):
[第 1 次尝试失败] unstable_request: 网络不稳定[第 2 次尝试失败] unstable_request: 网络不稳定请求成功
> 💡 理解三层结构:
最外层 retry(times, delay):接收装饰器参数,返回真正的装饰器
中间层 decorator(func):接收被装饰函数,返回 wrapper
最内层 wrapper(*args, **kwargs):真正执行的逻辑
五、装饰器的高级用法
1. 多个装饰器叠加(由内向外应用)
@log@timerdef foo(): pass# 等价于:foo = log(timer(foo))# 执行顺序:先进入 log 的 wrapper,再进入 timer 的 wrapper,最后调用 foo
2. 类装饰器(用类实现装饰器)
class CountCalls: def __init__(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)@CountCallsdef greet(): print("Hello!")greet() # greet 第 1 次被调用greet() # greet 第 2 次被调用
3. 常见的内置/标准库装饰器
装饰器 | 作用 |
@staticmethod
| 将类方法声明为静态方法 |
@classmethod
| 将类方法声明为类方法 |
@property
| 将方法变为属性访问 |
@functools.lru_cache
| 自动缓存函数返回值 |
@functools.wraps
| 保留被装饰函数的元信息 |
@dataclasses.dataclass
| 自动生成类的 __init__ 等方法 |
六、总结
要点 | 说明 |
本质 | 一个"接收函数、返回函数"的高阶函数 |
基础 | 依赖闭包与"函数是一等对象"的特性 |
语法 | `@decorator` 是 `func = decorator(func)` 的语法糖 |
优势 | 复用逻辑、非侵入式、代码简洁、职责分离 |
最佳实践 | 使用 `@functools.wraps` 保留原函数元信息 |
应用场景 | 日志、计时、缓存、权限校验、重试、事务、路由注册等 |
一句话记忆:装饰器 = 一个"给函数穿衣服"的函数,在不改变函数本身的情况下,为它加上额外的能力。