7 minutes
装饰器深入与实践
认识装饰器
装饰器(Decorator)是 Python 中一种强大的元编程工具。简单来说,装饰器就是一个函数,它接受另一个函数作为参数,并返回一个新的函数。装饰器允许我们在不修改原函数源代码的情况下,给函数添加额外的功能。
函数是一等公民
要理解装饰器,先要理解 Python 中"函数是一等公民"这个概念:
# 函数可以赋值给变量
def greet(name):
return f"你好, {name}!"
say_hello = greet
print(say_hello("小明")) # 输出: 你好, 小明!
# 函数可以作为参数传递
def call_twice(func, arg):
return func(arg), func(arg)
result1, result2 = call_twice(greet, "小红")
print(result1) # 输出: 你好, 小红!
# 函数可以嵌套定义
def outer():
def inner():
return "我是内部函数"
return inner
inner_func = outer()
print(inner_func()) # 输出: 我是内部函数
最简单的装饰器
基于上述特性,我们可以写出最简单的装饰器:
def my_decorator(func):
def wrapper():
print("函数调用前执行")
func()
print("函数调用后执行")
return wrapper
def say_hello():
print("你好!")
# 手动应用装饰器
say_hello = my_decorator(say_hello)
say_hello()
# 输出:
# 函数调用前执行
# 你好!
# 函数调用后执行
@ 语法糖
Python 提供了 @ 语法来简化装饰器的应用:
def my_decorator(func):
def wrapper():
print("函数调用前执行")
func()
print("函数调用后执行")
return wrapper
@my_decorator
def say_hello():
print("你好!")
# 等同于: say_hello = my_decorator(say_hello)
say_hello()
@my_decorator 只是语法糖,它的作用完全等同于手动调用 my_decorator(say_hello)。
装饰带参数的函数
上面的装饰器无法处理带参数的函数,我们需要让 wrapper 接受任意参数:
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用函数: {func.__name__}")
result = func(*args, **kwargs)
print(f"函数返回: {result}")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
@my_decorator
def greet(name, greeting="你好"):
return f"{greeting}, {name}!"
print(add(3, 5))
# 输出:
# 调用函数: add
# 函数返回: 8
# 8
print(greet("小明"))
# 输出:
# 调用函数: greet
# 函数返回: 你好, 小明!
# 你好, 小明!
使用 functools.wraps 保留元信息
装饰后的函数会丢失原来的元信息(函数名、文档字符串等):
def my_decorator(func):
def wrapper(*args, **kwargs):
"""包装函数"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
"""计算两个数的和"""
return a + b
print(add.__name__) # 输出: wrapper(不是 add)
print(add.__doc__) # 输出: 包装函数(不是 计算两个数的和)
使用 @functools.wraps 可以解决这个问题:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""包装函数"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
"""计算两个数的和"""
return a + b
print(add.__name__) # 输出: add
print(add.__doc__) # 输出: 计算两个数的和
规则: 每次编写装饰器时,都应该在 wrapper 函数上使用 @functools.wraps。
带参数的装饰器
有时我们需要装饰器本身接受参数。这需要再嵌套一层函数:
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(name):
print(f"你好, {name}!")
say_hello("小明")
# 输出:
# 你好, 小明!
# 你好, 小明!
# 你好, 小明!
理解这个结构:repeat(times=3) 返回 decorator 函数,@decorator 再将 say_hello 函数作为参数传入,返回 wrapper 函数。
可选参数的装饰器
如果希望装饰器既可以 @decorator 也可以 @decorator(args) 使用,需要更复杂的处理:
import functools
def repeat(func=None, *, times=1):
"""支持可选参数的装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
if func is not None:
# 没有参数: @repeat
return decorator(func)
# 有参数: @repeat(times=3)
return decorator
@repeat
def greet1(name):
print(f"你好, {name}!")
@repeat(times=3)
def greet2(name):
print(f"你好, {name}!")
greet1("小红") # 输出一次
greet2("小明") # 输出三次
多个装饰器的堆叠顺序
多个装饰器叠加时,执行顺序是从下到上(离函数定义最近的先执行):
import functools
def deco1(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("装饰器1 - 进入")
result = func(*args, **kwargs)
print("装饰器1 - 退出")
return result
return wrapper
def deco2(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("装饰器2 - 进入")
result = func(*args, **kwargs)
print("装饰器2 - 退出")
return result
return wrapper
@deco1
@deco2
def say_hello(name):
print(f"你好, {name}!")
say_hello("小明")
# 输出:
# 装饰器1 - 进入
# 装饰器2 - 进入
# 你好, 小明!
# 装饰器2 - 退出
# 装饰器1 - 退出
理解方式:say_hello = deco1(deco2(say_hello)),所以 deco2 先包装原始函数,然后 deco1 再包装。
类装饰器
装饰器不限于函数,也可以是类。类装饰器通过 __call__ 方法实现:
import functools
import time
class Timer:
"""类装饰器:统计函数执行时间"""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs):
start = time.perf_counter()
result = self.func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{self.func.__name__} 执行耗时: {elapsed:.4f}秒")
return result
@Timer
def slow_function():
time.sleep(0.5)
return "完成"
print(slow_function())
# 输出:
# slow_function 执行耗时: 0.5002秒
# 完成
带参数的类装饰器
import functools
class CountCalls:
"""统计函数调用次数"""
def __init__(self, func=None, *, verbose=False):
functools.update_wrapper(self, func) if func else None
self.func = func
self.verbose = verbose
self.count = 0
def __call__(self, *args, **kwargs):
if self.func is None:
# 带参数调用时返回装饰器实例
return type(self)(func=args[0], verbose=self.verbose)
self.count += 1
if self.verbose:
print(f"{self.func.__name__} 被调用了 {self.count} 次")
return self.func(*args, **kwargs)
@CountCalls(verbose=True)
def say_hello(name):
print(f"你好, {name}!")
say_hello("小明")
say_hello("小红")
# 输出:
# say_hello 被调用了 1 次
# 你好, 小明!
# say_hello 被调用了 2 次
# 你好, 小红!
实战装饰器
下面介绍几个项目中常用的实用装饰器。
1. 计时器装饰器
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:.6f}秒")
return result
return wrapper
@timer
def compute_sum(n):
return sum(range(n))
print(compute_sum(10_000_000))
# 输出类似: [compute_sum] 耗时: 0.234567秒
2. 日志/调试装饰器
import functools
def debug(func):
"""打印函数的调用详情"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"调用: {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"返回: {func.__name__}({signature}) -> {result!r}")
return result
return wrapper
@debug
def add(a, b):
return a + b
@debug
def greet(name, greeting="你好"):
return f"{greeting}, {name}!"
add(3, 5)
# 调用: add(3, 5)
# 返回: add(3, 5) -> 8
greet("小明")
# 调用: greet('小明')
# 返回: greet('小明') -> '你好, 小明!'
3. 重试装饰器
import functools
import time
import random
def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
"""失败时自动重试的装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
raise
print(f"第{attempt}次尝试失败: {e},{delay}秒后重试...")
time.sleep(delay)
return None
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def unstable_network_request():
"""模拟不稳定的网络请求"""
if random.random() < 0.7:
raise ConnectionError("网络连接超时")
return "请求成功!"
try:
result = unstable_network_request()
print(result)
except ConnectionError:
print("所有重试都失败了")
4. 缓存/记忆化装饰器
import functools
def memoize(func):
"""缓存函数的返回值(手动实现)"""
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
"""计算斐波那契数列"""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # 立刻返回,不会卡死
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(100))
# 查看缓存统计
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
lru_cache 使用 LRU(Least Recently Used)淘汰策略,maxsize 参数控制缓存上限。
5. 限速器装饰器
import functools
import time
def rate_limit(calls_per_second):
"""限制函数每秒调用次数"""
min_interval = 1.0 / calls_per_second
last_called = [0.0] # 使用列表作为可变容器
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.perf_counter() - last_called[0]
if elapsed < min_interval:
time.sleep(min_interval - elapsed)
last_called[0] = time.perf_counter()
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(calls_per_second=2) # 每秒最多调用2次
def api_request(url):
print(f"请求: {url}")
return f"数据来自 {url}"
for i in range(5):
api_request(f"https://api.example.com/data/{i}")
6. 类型检查装饰器
import functools
def type_check(**expected_types):
"""检查函数参数类型"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 获取函数签名
import inspect
bound = inspect.signature(func).bind(*args, **kwargs)
bound.apply_defaults()
for name, value in bound.arguments.items():
if name in expected_types:
expected = expected_types[name]
if not isinstance(value, expected):
raise TypeError(
f"参数 {name} 期望类型 {expected.__name__},"
f"实际得到 {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@type_check(x=int, y=int)
def divide(x, y):
return x / y
print(divide(10, 3)) # 正常运行
print(divide("10", 3)) # TypeError: 参数 x 期望类型 int,实际得到 str
内置装饰器回顾
Python 内置的装饰器在面向对象编程中非常常用:
@property
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""获取半径"""
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("半径必须为正数")
self._radius = value
@property
def area(self):
"""计算面积(只读属性)"""
import math
return math.pi * self._radius ** 2
circle = Circle(5)
print(circle.radius) # 5(像属性一样访问)
circle.radius = 10 # 像属性一样设置
print(circle.area) # 314.159...
# circle.area = 100 # AttributeError: can't set attribute
@staticmethod
class MathUtils:
@staticmethod
def is_even(n):
"""静态方法:不依赖实例或类"""
return n % 2 == 0
@staticmethod
def add(a, b):
return a + b
print(MathUtils.is_even(4)) # True
print(MathUtils.add(3, 5)) # 8
@classmethod
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_str):
"""类方法:从字符串创建实例"""
year, month, day = map(int, date_str.split("-"))
return cls(year, month, day)
@classmethod
def today(cls):
"""类方法:返回今天的日期"""
import datetime
now = datetime.date.today()
return cls(now.year, now.month, now.day)
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
d1 = Date.from_string("2026-05-21")
d2 = Date.today()
print(d1) # Date(2026, 5, 21)
print(d2) # Date(2026, ..., ...)
装饰器的常见陷阱
陷阱1: mutable 默认参数
def decorator_with_list(arg=[]): # BAD: 可变默认参数只初始化一次
...
def decorator_with_list(arg=None): # GOOD
if arg is None:
arg = []
...
陷阱2: 装饰器丢失原始函数信息(不用 wraps)
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def f():
pass
print(f.__name__) # wrapper(错误)
陷阱3: 装饰器内的闭包变量
# BAD
def create_decorators():
decorators = []
for i in range(5):
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) + i # i 是延迟绑定的!
return wrapper
decorators.append(decorator)
return decorators
# GOOD
def create_decorators():
decorators = []
for i in range(5):
def decorator(func, i=i): # 将 i 作为默认参数,立即绑定
def wrapper(*args, **kwargs):
return func(*args, **kwargs) + i
return wrapper
decorators.append(decorator)
return decorators
总结与最佳实践
装饰器的应用场景
| 场景 | 说明 |
|---|---|
| 日志记录 | 自动记录函数调用和参数 |
| 性能计时 | 测量函数执行时间 |
| 权限校验 | 检查用户是否有权限执行操作 |
| 缓存 | 缓存函数返回结果,避免重复计算 |
| 重试 | 失败后自动重试 |
| 限速 | 控制函数调用频率 |
| 事务管理 | 数据库事务的开启和提交/回滚 |
| 输入验证 | 检查参数类型和取值范围 |
编写装饰器的最佳实践
- 始终使用
@functools.wraps保留原函数的元信息 - 使用
*args, **kwargs确保装饰器通用 - 保持装饰器简单——一个装饰器只做一件事
- 文档化你的装饰器——写清楚的文档字符串
- 考虑组合性——确保多个装饰器可以叠加使用
- 优先使用 functools 和标准库——内置工具通常更好
小结
装饰器是 Python 元编程的核心工具,它基于"函数是一等公民"的特性,允许我们以声明式的方式给函数添加额外行为。本章我们学习了装饰器的基本原理(函数嵌套函数)、@ 语法糖的工作机制、带参数装饰器的三层嵌套结构、多个装饰器的堆叠顺序,以及基于类的装饰器实现。通过计时器、日志、重试、缓存、限速和类型检查等实战示例,你应该已经掌握了装饰器的常见应用模式。最后,我们回顾了 @property、@staticmethod 和 @classmethod 这三个内置装饰器的用法。记住:始终使用 @functools.wraps,保持装饰器的单一职责,这是编写高质量装饰器的关键。
Summary: 装饰器原理、语法糖、参数传递、堆叠与实战应用。