前言
装饰器(Decorator)是 Python 最具代表性的语言特性之一。表面上,它只是"给函数加一层包装"的语法糖;实际上,它背后藏着 Python 元编程的核心能力。很多人用装饰器只是 @staticmethod、@property 这类内置的,但真正的威力在于自定义装饰器——尤其是支持参数、带状态、可堆叠的那种。
本篇文章,我们把装饰器从"会用"推进到"用好",覆盖以下核心内容:
- 装饰器的基础运行机制(闭包视角)
- 带参数的装饰器工厂
- 带状态的装饰器(类装饰器)
functools.wraps的正确姿势- 多个装饰器的执行顺序
- 实际生产场景:重试、限流、注册、缓存
- 类方法装饰器的特殊处理
一、装饰器到底是什么?
先来看一个最朴素的装饰器:
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
调用后
等价于:
say_hello = my_decorator(say_hello)
装饰器就是一个接收函数并返回新函数的函数。这层转换发生在定义时,而非调用时。
二、用闭包视角理解装饰器
很多教程只告诉你"装饰器就是函数包装",但理解不深的人会在遇到复杂装饰器时一头雾水。关键在于闭包(Closure):
def outer(x): # 外层函数
def inner(func): # 中层函数(装饰器)
def wrapper(*args, **kwargs):
print(f"[{x}] 开始执行")
return func(*args, **kwargs)
return wrapper
return inner
三层嵌套,每层都"记住"了外层的变量。装饰器工厂返回的是 inner,inner 返回 wrapper,wrapper 闭包捕获了 x。
@outer("DEBUG")
def process():
pass
等价于 process = outer("DEBUG")(process)。
三、装饰器工厂:带参数的装饰器
如果装饰器本身需要配置参数,就需要装饰器工厂——在装饰器外面再包一层。
3.1 最简单的参数化
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"你好,{name}")
greet("张三")
# 输出:你好,张三(连续3次)
3.2 带可选参数的装饰器
很多场景需要装饰器同时支持有参数和无参数调用:
import functools
import time
def log(level="INFO"):
"""支持 @log 或 @log(level='DEBUG')"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
print(f"[{level}] 调用 {func.__name__} ...")
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[{level}] {func.__name__} 完成,耗时 {elapsed:.3f}s")
return result
return wrapper
# 如果传入的是函数而非字符串,说明是 @log 这样无参数调用
if callable(level):
func = level
level = "INFO"
return decorator(func)
return decorator
@log
def func_a():
pass
@log("DEBUG")
def func_b():
pass
四、functools.wraps——必须养成的习惯
不写 @functools.wraps(func) 的装饰器,会让原函数的元信息全部丢失:
# ❌ 不使用 wraps
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def add(a, b):
"""两数相加"""
return a + b
print(add.__name__) # wrapper ——元信息丢失!
print(add.__doc__) # None
# ✅ 使用 wraps
import functools
def good_decorator(func):
@functools.wraps(func) # 这一行必须加!
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def add(a, b):
"""两数相加"""
return a + b
print(add.__name__) # add
print(add.__doc__) # 两数相加
元信息在装饰器中丢失会直接影响:
- 调试时打印堆栈(看不到原函数名)
inspect.signature()获取签名- Sphinx / pdoc 自动生成文档
五、装饰器执行顺序:就近优先
多个装饰器叠加时,从近到远依次执行(类似"从下到上"堆叠):
def deco_a(func):
def wrapper(*args, **kwargs):
print("A 进入")
result = func(*args, **kwargs)
print("A 退出")
return result
return wrapper
def deco_b(func):
def wrapper(*args, **kwargs):
print("B 进入")
result = func(*args, **kwargs)
print("B 退出")
return result
return wrapper
@deco_a
@deco_b
def target():
print("target 执行")
target()
输出:
A 进入
B 进入
target 执行
B 退出
A 退出
六、类装饰器:带状态的装饰器
类装饰器可以持有状态,适合需要"计数器"“缓存”"注册表"等场景:
6.1 注册器装饰器
class Registry:
def __init__(self):
self._registry = {}
def register(self, name=None):
"""装饰器工厂:@registry.register('my_name')"""
def decorator(func):
key = name or func.__name__
self._registry[key] = func
return func
return decorator
def get(self, name):
return self._registry.get(name)
def list_all(self):
return list(self._registry.keys())
# 全局注册表
router = Registry()
@router.register("home")
def home_page():
return "首页"
@router.register("about")
def about_page():
return "关于页"
@router.register("contact")
def contact_page():
return "联系页"
# 动态调用
print(router.list_all()) # ['home', 'about', 'contact']
print(router.get("home")()) # 首页
6.2 带计数的装饰器
import functools
class CallCounter:
def __init__(self):
self._counts = {}
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
self._counts[func.__name__] = self._counts.get(func.__name__, 0) + 1
return func(*args, **kwargs)
# 暴露计数器属性,方便外部查询
wrapper.call_count = lambda: self._counts[func.__name__]
return wrapper
def report(self):
return dict(self._counts)
counter = CallCounter()
@counter
def fetch_data():
pass
@counter
def save_data():
pass
fetch_data()
fetch_data()
save_data()
print(counter.report()) # {'fetch_data': 2, 'save_data': 1}
七、生产场景实战
7.1 重试装饰器
import functools
import time
import logging
logger = logging.getLogger(__name__)
def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
"""指数退避重试装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except exceptions as e:
attempts += 1
if attempts == max_attempts:
logger.error(f"{func.__name__} 重试{max_attempts}次后仍失败: {e}")
raise
wait = delay * (2 ** (attempts - 1)) # 指数退避
logger.warning(f"{func.__name__} 失败,{wait}s后重试(第{attempts}次)...")
time.sleep(wait)
return wrapper
return decorator
@retry(max_attempts=3, delay=1, exceptions=(ConnectionError, TimeoutError))
def call_api(url):
# 模拟网络不稳定
import random
if random.random() < 0.7:
raise ConnectionError("网络错误")
return "API响应数据"
7.2 速率限制装饰器
import time
import functools
from threading import Lock
def rate_limit(calls_per_second):
"""令牌桶限流装饰器"""
min_interval = 1.0 / calls_per_second
lock = Lock()
last_called = 0.0
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
nonlocal last_called
with lock:
elapsed = time.time() - last_called
if elapsed < min_interval:
time.sleep(min_interval - elapsed)
last_called = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(calls_per_second=5)
def process_item(item):
print(f"处理: {item}")
for i in range(10):
process_item(i)
7.3 缓存装饰器(带TTL)
import functools
import time
def memoize(ttl=60):
"""带过期时间的缓存装饰器"""
cache = {}
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
now = time.time()
if key in cache:
result, timestamp = cache[key]
if now - timestamp < ttl:
print(f"[缓存命中] {func.__name__}")
return result
result = func(*args, **kwargs)
cache[key] = (result, now)
return result
return wrapper
return decorator
@memoize(ttl=5)
def expensive_computation(n):
time.sleep(1) # 模拟耗时操作
return n * n
print(expensive_computation(10)) # 第一次,无缓存
print(expensive_computation(10)) # 5秒内第二次,命中缓存
八、类方法装饰器注意事项
类方法装饰器需要注意 self 参数的处理:
import functools
def log_method(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
print(f"调用方法: {func.__name__}")
return func(self, *args, **kwargs)
return wrapper
class Calculator:
def __init__(self, precision=2):
self.precision = precision
@log_method # 实例方法装饰器
def add(self, a, b):
return round(a + b, self.precision)
@classmethod
@log_method # 类方法 + 装饰器
def info(cls):
return f"Calculator 类(精度: {cls.precision})"
calc = Calculator(precision=4)
print(calc.add(3.14159, 2.71828))
九、装饰器 vs 类组合
有时候类组合比装饰器更灵活:
class Timer:
def __init__(self, func):
self._func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
import time
start = time.time()
result = self._func(*args, **kwargs)
print(f"{self._func.__name__} 耗时 {time.time() - start:.4f}s")
return result
@Timer
def slow_function():
time.sleep(0.5)
return 42
十、常见陷阱总结
| 陷阱 | 后果 | 解决方案 |
|---|---|---|
不加 @functools.wraps | __name__、__doc__ 丢失 | 养成习惯,必加 |
| 装饰器修改了原函数签名 | 类型检查工具、静态检查报错 | 用 functools.wraps + 保持签名一致 |
| 装饰器意外捕获了全局变量 | 并发/多线程场景出问题 | 显式传入参数,避免隐式依赖 |
| 装饰器在模块加载时执行副作用 | 循环依赖、初始化顺序问题 | 避免在装饰器顶层执行 I/O |
| 对异步函数使用同步装饰器 | 异步行为被破坏 | 用 functools.wraps + 判断 asyncio.iscoroutinefunction |
结语
装饰器是 Python 中"一等公民函数"最典型的应用体现。它不是炫技,而是一种优雅的横切关注点分离方式——日志、重试、限流、缓存、权限校验,这些逻辑如果全部写在业务函数里,代码将无法维护。
掌握好装饰器,意味着你能写出既简洁又可扩展的 Python 代码。下一篇文章我们将进入另一个元编程核心——元类(Metaclass),敬请期待。
系列导航:
- ← 序章:Python高级特性概览
- (一)装饰器的深度实践 ← 当前
- (二)元类:Python面向对象的暗物质
- (三)描述符与属性控制
- (四)上下文管理器与 with 语句
- (五)生成器与协程的精髓
- ……
如果这篇文章对你有帮助,欢迎点赞、收藏!有问题欢迎在评论区交流。
1044



被折叠的 条评论
为什么被折叠?



