Python装饰器完全指南:从基础到高级应用
让我们从最简单的例子开始,看看装饰器最基本的结构是什么样的。
python
def simple_decorator(func):
def wrapper():
print("函数调用前执行")
func()
print("函数调用后执行")
return wrapper
@simple_decorator
def hello():
print("Hello, World!")
hello()
输出结果:
函数调用前执行
Hello, World!
函数调用后执行
上面这个例子非常直观,我们通过装饰器给 `hello` 函数增加了在调用前后打印日志的功能,而完全不需要修改 `hello` 函数本身的代码。这就是装饰器的核心优势——开闭原则:对扩展开放,对修改关闭。
上面的示例中被装饰的函数没有参数,如果原函数有参数怎么办呢?其实很简单,我们可以在 `wrapper` 函数中使用 `*args` 和 `**kwargs` 来接收任意数量的位置参数和关键字参数,然后透传给原函数。
python
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用函数: {func.__name__}, 参数: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"函数 {func.__name__} 执行完毕")
return result
return wrapper
@log_decorator
def add(a, b):
return a + b
@log_decorator
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
print(add(2, 3))
greet("Alice", greeting="Good morning")
输出结果:
调用函数: add, 参数: (2, 3), {}
函数 add 执行完毕
5
调用函数: greet, 参数: ('Alice',), {'greeting': 'Good morning'}
Good morning, Alice!
函数 greet 执行完毕
现在我们的装饰器已经可以处理任意参数的函数了。`*args` 接收所有位置参数,`**kwargs` 接收所有关键字参数,然后透传给原函数 `func`,最后还把原函数的返回值正确返回出去了,完美!
不知道你有没有发现一个小问题:被装饰之后,函数的元信息变了。比如我们打印 `add.__name__`,会得到 `wrapper` 而不是原来的 `add`。这是因为我们返回的是新函数 `wrapper`,原函数的元信息都丢失了。这对于调试和一些反射操作来说不太友好。
怎么解决这个问题呢?Python 标准库提供了 `functools.wraps` 装饰器专门用来处理这个问题,它会把原函数的元信息复制到包装函数中。
python
import functools
def log_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"调用函数: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_decorator
def add(a, b):
return a + b
print(add.__name__) # 输出: add,而不是 wrapper
所以记住,写装饰器的时候一定要加上 `@functools.wraps(func)`,这是一个很好的习惯,可以避免很多奇怪的问题。
有时候我们希望装饰器本身也可以接收参数,来配置装饰器的行为。比如,我们想要一个日志装饰器,可以指定日志级别。怎么实现呢?其实很简单,只需要再包一层,外层函数接收参数,然后返回真正的装饰器。
python
import functools
import logging
def log(level="INFO"):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if level == "INFO":
logging.info(f"调用函数 {func.__name__}")
elif level == "DEBUG":
logging.debug(f"调用函数 {func.__name__}, 参数 {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
return decorator
@log(level="DEBUG")
def multiply(a, b):
return a * b
@log()
def subtract(a, b):
return a - b
如果你希望装饰器既可以带参数使用,也可以不带参数直接使用,需要做一点特殊处理,让它自动识别:
python
import functools
def smart_decorator(func_or_arg=None, *, option=False):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if option:
print("Option is enabled")
return func(*args, **kwargs)
return wrapper
if func_or_arg is None:
# 带参数调用: @smart_decorator(option=True)
return decorator
else:
# 不带参数调用: @smart_decorator
return decorator(func_or_arg)
@smart_decorator(option=True)
def test1():
pass
@smart_decorator
def test2():
pass
这样就完美支持两种用法了。
现在我们来看几个装饰器在实际开发中的常见用法,这些都是可以直接用到项目中的代码示例。
对于一些计算密集型的函数,如果输入参数相同,我们希望把结果缓存起来,避免重复计算,这就是缓存装饰器的作用。
python
import functools
def cache(func):
cache_dict = {}
@functools.wraps(func)
def wrapper(*args):
# 注意:这里只支持不可变的位置参数作为缓存键
if args not in cache_dict:
cache_dict[args] = func(*args)
return cache_dict[args]
return wrapper
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # 计算非常快,因为已经缓存了中间结果
Python 标准库其实已经提供了成熟的缓存实现 `functools.lru_cache`,比我们自己写的更完善,支持最大缓存数量限制,支持清除缓存等功能。但是理解了原理之后,你就可以根据自己的需求定制缓存逻辑了。
用来统计函数执行时间,性能优化的时候非常有用。
python
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()
print(f"{func.__name__} 执行时间: {end_time - start_time:.6f} 秒")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "Done"
slow_function()
输出:
slow_function 执行时间: 1.001234 秒
当网络请求失败的时候,我们希望自动重试几次,而不是直接报错,这用装饰器实现非常优雅:
python
import functools
import time
def retry(max_retries=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"第 {attempt + 1} 次尝试失败,{delay} 秒后重试...")
time.sleep(delay)
# 所有重试都失败了,抛出最后捕获的异常
raise last_exception
return wrapper
return decorator
import requests
@retry(max_retries=3, delay=2)
def fetch_url(url):
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.text
Python 是动态类型语言,但有时候我们希望强制检查函数参数类型,可以用装饰器实现:
python
import functools
def type_check(*types):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if len(args) != len(types):
raise ValueError(f"参数数量不匹配,期望 {len(types)} 个,实际 {len(args)} 个")
for arg, expected_type in zip(args, types):
if not isinstance(arg, expected_type):
raise TypeError(f"参数类型错误,期望 {expected_type.__name__},实际是 {type(arg).__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@type_check(int, int)
def add(a, b):
return a + b
print(add(2, 3)) # 正常输出 5
你可以给一个函数同时应用多个装饰器,它们会从内向外执行,或者说从下向上应用。让我们看个例子:
python
def decorator1(func):
def wrapper():
print("装饰器 1 开始")
func()
print("装饰器 1 结束")
return wrapper
def decorator2(func):
def wrapper():
print("装饰器 2 开始")
func()
print("装饰器 2 结束")
return wrapper
@decorator1
@decorator2
def hello():
print("Hello")
hello()
输出结果:
装饰器 1 开始
装饰器 2 开始
Hello
装饰器 2 结束
装饰器 1 结束
堆叠顺序非常重要,记住:离函数最近的装饰器先应用,然后外层的装饰器再包装内层包装后的结果。上面的例子等价于 `hello = decorator1(decorator2(hello))`。
除了函数装饰器,我们还可以用类来实现装饰器,只需要让类实现 `__call__` 方法即可。类装饰器适合需要维护内部状态的场景,比如统计调用次数:
python
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 say_hello():
print("Hello")
say_hello()
say_hello()
say_hello()
print(say_hello.num_calls) # 输出: 3
这里我们用 `functools.update_wrapper(self, func)` 来更新类实例的元信息,作用和 `@functools.wraps` 类似。类装饰器非常强大,因为它可以很方便地用实例属性来保存状态,而函数装饰器要保存状态就需要用到闭包或者 nonlocal 关键字。
当你想用装饰器修饰类的实例方法时,需要注意什么吗?其实不需要特殊处理,因为 `self` 也只是一个普通的参数而已,`*args` 会自动把 `self` 作为第一个参数接收进去,然后透传给原方法。让我们看个例子:
python
import functools
def instance_method_decorator(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
print(f"调用方法 {self.__class__.__name__}.{func.__name__}")
return func(self, *args, **kwargs)
return wrapper
class MyClass:
@instance_method_decorator
def say_hello(self, name):
print(f"Hello {name}")
obj = MyClass()
obj.say_hello("World")
即使你不明确写 `self`,直接用 `*args` 也没问题,因为 `args` 会把 `self` 作为第一个元素包含进去,透传的时候不会出问题。明确写出 `self` 只是让代码可读性更好而已。
1. **不要忘记返回结果**:很多新手写包装函数的时候忘记 `return func(*args, **kwargs)`,结果被装饰的函数变成了返回 `None`,调试半天才能发现问题。
2. **一定要保留原函数元信息**:使用 `@functools.wraps`,否则函数名、文档字符串都会丢失,调试的时候非常痛苦。
3. **慎用装饰器修改原函数参数**:装饰器最好透传参数,不要轻易修改参数,这样会让函数行为变得不直观,增加理解成本。
4. **不要滥用装饰器**:不是所有场景都需要用装饰器,如果一个简单的函数调用就能解决问题,就不需要非要用装饰器。过度使用装饰器会让代码变得难以理解和调试。
5. **类装饰器和函数装饰器怎么选?**:如果需要维护状态或者需要更复杂的逻辑,用类装饰器更清晰;简单场景用函数装饰器写起来更简洁。
装饰器是 Python 中一个非常优雅的特性,它可以帮助我们写出更简洁、更易维护的代码,实现横切关注点的分离。本文从最基础的概念开始,一步步讲解了装饰器的各种用法,包括处理参数、保存元信息、带参数的装饰器、实际应用场景、装饰器堆叠、类装饰器等等,并且给出了大量可以直接运行的原创代码示例。
读完这篇文章之后,你应该对装饰器有了完整的理解,可以开始在你的项目中使用装饰器了。记住,实践是最好的老师,看完教程之后,不妨自己动手写几个装饰器,体验一下它的强大之处。
- Python 官方文档: https://docs.python.org/3/glossary.html#term-decorator
- 《Python Cookbook》第三版,第 9 章元编程
