Python 装饰器进阶:从理解到实战
装饰器是 Python 中一个非常强大的特性,它允许你在不修改原函数代码的情况下,为函数添加额外的功能。很多开发者虽然用过装饰器,但对其底层原理和高级用法理解不深。本文将从基础出发,深入讲解装饰器的工作原理,并通过多个实用的原创示例,带你掌握装饰器的进阶用法,包括带参数的装饰器、类装饰器、装饰器链以及实际开发中的常见应用场景。
什么是装饰器
装饰器本质上是一个接受函数作为输入,并返回一个新函数的函数。它利用了 Python 的闭包特性,允许我们在函数执行前后注入自定义逻辑。
先看一个最简单的装饰器示例:
def simple_decorator(func):
def wrapper(*args, **kwargs):
print("函数执行前")
result = func(*args, **kwargs)
print("函数执行后")
return result
return wrapper
@simple_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("World")
输出结果:
函数执行前 Hello, World! 函数执行后
这里 @simple_decorator 等价于 say_hello = simple_decorator(say_hello),语法糖让代码更简洁。
为什么需要装饰器
装饰器的核心价值在于遵循开闭原则:对扩展开放,对修改关闭。当你需要为多个函数添加相同的逻辑(比如日志记录、性能统计、权限校验等),装饰器可以帮助你避免代码重复,保持代码整洁。
常见使用场景:
- 日志记录
- 性能测试
- 权限校验
- 缓存
- 输入验证
- 事务处理
保留原函数元信息
使用装饰器后,原函数的 __name__、__doc__ 等元信息会被装饰器返回的 wrapper 函数覆盖,这对调试和文档生成不友好。解决方法是使用 functools.wraps:
import functools
def preserved_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 这里添加装饰逻辑
return func(*args, **kwargs)
return wrapper
@preserved_decorator
def test_func():
"""这是测试函数的文档字符串"""
pass
print(test_func.__name__) # 输出: test_func
print(test_func.__doc__) # 输出: 这是测试函数的文档字符串
如果不使用 functools.wraps,输出会变成 wrapper 和 None,所以养成习惯,永远给你的装饰器加上 functools.wraps。
带参数的装饰器
有时候我们希望装饰器本身可以接受参数,来配置它的行为。这时候需要再封装一层:
import functools
def repeat(times):
"""重复执行函数的装饰器,接受次数参数"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello {name}!")
greet("Alice")
输出:
Hello Alice! Hello Alice! Hello Alice!
这里 @repeat(times=3) 的执行过程是:
- 调用
repeat(times=3)返回decorator函数 - 将原函数传递给
decorator,返回wrapper - 原函数被替换为
wrapper
类装饰器
除了函数,类也可以作为装饰器。类装饰器主要依靠 __call__ 方法,当你使用 @ 语法装饰函数时,会调用类的构造函数,然后把原函数作为参数传递。
一个简单的示例,统计函数被调用的次数:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"函数 {self.func.__name__} 已被调用 {self.num_calls} 次")
return self.func(*args, **kwargs)
@CountCalls
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print(fib(5))
输出中会看到每个递归调用都会计数,非常直观。类装饰器适合需要维护状态,或者多个方法协作的场景。
多个装饰器链式调用
一个函数可以被多个装饰器装饰,执行顺序是从下到上应用,从上到下执行。看这个例子:
import functools
def decorator1(func):
@functools.wraps(func)
def wrapper():
print("装饰器1 执行前")
func()
print("装饰器1 执行后")
return wrapper
def decorator2(func):
@functools.wraps(func)
def wrapper():
print("装饰器2 执行前")
func()
print("装饰器2 执行后")
return wrapper
@decorator1
@decorator2
def say_hi():
print("Hi!")
say_hi()
输出:
装饰器1 执行前 装饰器2 执行前 Hi! 装饰器2 执行后 装饰器1 执行后
等价于 say_hi = decorator1(decorator2(say_hi)),所以应用顺序是先 decorator2,再 decorator1,执行的时候外层先执行,所以 decorator1 的前处理先执行,然后进入 decorator2。
实用装饰器示例
下面是几个实际开发中经常用到的原创装饰器示例。
1. 计时装饰器
统计函数执行时间:
import time
import functools
def timer(func):
"""计时装饰器,打印函数执行耗时"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
elapsed = end_time - start_time
print(f"{func.__name__} 执行耗时: {elapsed:.4f} 秒")
return result
return wrapper
@timer
def slow_calculation():
time.sleep(1)
return sum(i * i for i in range(10000))
slow_calculation()
2. 缓存装饰器
对函数结果进行缓存,避免重复计算:
import functools
def cache(func):
"""简单的基于字典的缓存装饰器"""
cached_results = {}
@functools.wraps(func)
def wrapper(*args):
# 只有位置参数可以作为键,关键字参数需要额外处理
if args not in cached_results:
cached_results[args] = func(*args)
return cached_results[args]
return wrapper
@cache
def fibonacci(n):
print(f"计算 fibonacci({n})")
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10))
print("
第二次调用:")
print(fibonacci(10))
第一次计算会递归计算所有子问题,第二次直接从缓存读取,速度提升非常明显。Python 标准库实际上已经提供了 functools.lru_cache,这个示例主要帮助理解缓存装饰器的原理。
3. 输入验证装饰器
验证函数参数类型:
import functools
def validate_types(*expected_types):
def decorator(func):
@functools.wraps(func)
def wrapper(*args):
if len(args) != len(expected_types):
raise ValueError("参数数量不匹配")
for arg, expected_type in zip(args, expected_types):
if not isinstance(arg, expected_type):
raise TypeError(
f"参数 {arg} 类型应为 {expected_type.__name__}, 实际为 {type(arg).__name__}"
)
return func(*args)
return wrapper
return decorator
@validate_types(int, int)
def add(a, b):
return a + b
print(add(2, 3)) # 正常输出 5
# print(add("2", 3)) # 抛出 TypeError
这个装饰器可以快速对函数参数做类型检查,在开发阶段能提前发现很多错误。
4. 异步任务重试装饰器
对于不稳定的网络请求,添加自动重试逻辑:
import asyncio
import functools
def async_retry(max_retries=3, delay=1):
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries):
try:
return await func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"尝试 {attempt + 1} 失败: {e}, {delay} 秒后重试...")
await asyncio.sleep(delay)
# 所有重试都失败后抛出异常
raise last_exception
return wrapper
return decorator
# 使用示例
import aiohttp
@async_retry(max_retries=3, delay=2)
async def fetch_url(url):
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=10) as response:
return await response.text()
# 如果要运行:
# asyncio.run(fetch_url("https://example.com"))
装饰器的常见坑
多个装饰器顺序错误
很多新手容易搞反装饰器顺序,记住:离函数近的先应用,离函数远的后应用。不保留元信息
忘记使用functools.wraps,导致调试困难,文档错误。装饰器不支持关键字参数
写wrapper时只用了*args没加**kwargs,导致带关键字参数的函数无法使用装饰器。正确做法永远是def wrapper(*args, **kwargs)。带参数装饰器忘记加括号
当使用带参数装饰器时,@repeat和@repeat()是不同的:@repeat会把函数直接传给repeat,返回的是decorator,调用时会出错@repeat(times=3)才是正确用法
总结
装饰器是 Python 中非常优雅的一个特性,掌握它能让你的代码更简洁、更符合设计原则。本文从基础原理出发,讲解了:
- 装饰器的本质:接受函数,返回函数的闭包
- 如何保留原函数元信息:使用
functools.wraps - 带参数装饰器的实现方法:再嵌套一层
- 类装饰器的实现:利用
__call__方法 - 多个装饰器的执行顺序
- 四个实用的原创装饰器示例:计时、缓存、验证、异步重试
- 常见的坑和避免方法
装饰器的学习曲线比较平缓,从简单的无参数装饰器开始,逐步理解带参数和类装饰器,多写几个示例就能掌握。在实际开发中合理使用装饰器,可以大幅提升代码的可维护性和复用性。
