共计 8576 个字符,预计需要花费 22 分钟才能阅读完成。
Python 是一种以其简洁性和强大功能而闻名的语言。在其众多高级特性中,装饰器(Decorators)无疑是最优雅和强大的特性之一。它们允许我们在不修改函数或类原始代码的情况下,动态地“装饰”它们,从而添加新的功能或修改其行为。从日志记录、性能分析到权限控制,装饰器在实际开发中无处不在。
本文将带领你从装饰器的基本概念入手,逐步深入探索带参数的装饰器以及如何使用类来实现装饰器,并辅以丰富的代码示例,助你从入门走向精通,将装饰器真正融入你的日常开发。
装饰器初探:什么是装饰器?
在深入研究带参数装饰器和类装饰器之前,我们首先需要理解装饰器的核心概念。简单来说,装饰器是一个函数,它接受一个函数作为参数,并返回一个新的函数(或可调用对象),新函数通常在原始函数执行前后添加额外的逻辑。Python 提供了 @ 语法糖,使得装饰器的使用非常直观。
让我们看一个最简单的装饰器示例:一个用于测量函数执行时间的装饰器。
import time
def timer(func):
"""一个简单的装饰器,用于测量函数执行时间。"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs) # 执行原始函数
end_time = time.time()
print(f"函数 {func.__name__} 执行耗时: {end_time - start_time:.4f} 秒")
return result
return wrapper
@timer
def long_running_function(n):
"""一个模拟耗时操作的函数。"""
sum_val = 0
for i in range(n):
sum_val += i * i
time.sleep(0.1) # 模拟 I / O 等待
return sum_val
@timer
def another_function():
time.sleep(0.2)
print("Another function finished.")
# 调用被装饰的函数
result = long_running_function(1000000)
print(f"Long running function returned: {result}")
another_function()
在这个例子中:
timer是我们的装饰器函数。它接受一个函数func作为参数。timer内部定义了一个wrapper函数。这个wrapper函数才是真正替代原始func执行的函数。wrapper函数使用*args和**kwargs来确保它可以接收任何数量和类型的参数,并将其传递给原始函数func。timer返回wrapper函数。@timer语法糖等价于long_running_function = timer(long_running_function)。它将long_running_function函数传递给timer,然后用timer返回的新函数(即wrapper)替换掉long_running_function的定义。
通过这个例子,我们理解了装饰器的基本工作原理:在不改变原始函数代码的情况下,为其添加了计时功能。
注意: 在实际开发中,为了保留被装饰函数的元信息(如 __name__, __doc__ 等),我们通常会使用 functools.wraps 装饰器来包装 wrapper 函数。
import time
from functools import wraps
def timer(func):
@wraps(func) # 使用 functools.wraps 保留原函数的元信息
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"函数 {func.__name__} 执行耗时: {end_time - start_time:.4f} 秒")
return result
return wrapper
@timer
def example_function(a, b):
"""这是一个示例函数,计算 a +b。"""
time.sleep(0.05)
return a + b
print(example_function.__name__) # 输出: example_function
print(example_function.__doc__) # 输出: 这是一个示例函数,计算 a +b。
进阶之路:带参数的装饰器
前面的装饰器是固定的,每次使用都执行相同的逻辑。但很多时候,我们希望能够在使用装饰器时传入一些配置参数,例如,一个日志装饰器可能希望指定日志级别,或者一个权限装饰器需要指定所需的角色。这就是“带参数的装饰器”派上用场的时候了。
带参数的装饰器实际上是一个“装饰器工厂函数”,它接受装饰器的参数,然后返回一个真正的装饰器函数。这个真正的装饰器函数再接受被装饰的函数,并返回 wrapper 函数。听起来有点绕?看代码就明白了。
from functools import wraps
def log_decorator(level):
"""一个带参数的装饰器,用于根据指定的级别记录函数调用。"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{level}] 调用函数: {func.__name__},参数: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"[{level}] 函数 {func.__name__} 执行完毕,返回值: {result}")
return result
return wrapper
return decorator
@log_decorator(level="INFO")
def add(a, b):
return a + b
@log_decorator(level="DEBUG")
def subtract(a, b):
return a - b
@log_decorator(level="WARNING")
def divide(a, b):
if b == 0:
print(f"[WARNING] 尝试除以零!")
return None
return a / b
print("n--- 调用 add 函数 ---")
add(10, 5)
print("n--- 调用 subtract 函数 ---")
subtract(20, 7)
print("n--- 调用 divide 函数 ---")
divide(10, 2)
divide(10, 0)
解析带参数装饰器的工作原理:
log_decorator(level="INFO"):当你写@log_decorator(level="INFO")时,Python 首先会调用log_decorator函数,并将"INFO"作为level参数传入。log_decorator函数执行后,它会返回内部的decorator函数。- 此时,
@语法糖实际上变成了@decorator。也就是说,add = decorator(add)。 decorator函数接收add作为func参数,并返回wrapper函数。- 最终,
add被替换为这个wrapper函数。
这个多层嵌套的结构(装饰器工厂 -> 装饰器 -> 包装器)是实现带参数装饰器的关键。
灵活应用:装饰器的多个参数与使用场景
带参数的装饰器可以接受任意数量的参数,这使得它们在多种场景下都非常有用。
示例:一个权限检查装饰器
from functools import wraps
def require_permission(permission_level):
"""
一个权限检查装饰器,根据用户权限决定是否执行函数。假设有一个全局的用户权限字典。"""
def decorator(func):
@wraps(func)
def wrapper(user_id, *args, **kwargs):
# 这是一个简化的权限检查逻辑
user_permissions = {
1: "admin",
2: "editor",
3: "viewer"
}
if user_id not in user_permissions:
print(f"错误:用户 {user_id} 不存在。")
return None
user_level = user_permissions[user_id]
# 简单比较权限级别,实际应用中可能需要更复杂的权限树或角色管理
permission_levels = {"viewer": 1, "editor": 2, "admin": 3}
if permission_levels.get(user_level, 0) >= permission_levels.get(permission_level, 0):
print(f"用户 {user_id} (权限: {user_level}) 拥有足够权限 ({permission_level})。")
return func(user_id, *args, **kwargs)
else:
print(f"拒绝访问:用户 {user_id} (权限: {user_level}) 不具备'{permission_level}'权限。")
return None
return wrapper
return decorator
@require_permission("admin")
def delete_data(user_id, item_id):
print(f"用户 {user_id} 删除了数据项 {item_id}。")
return True
@require_permission("editor")
def edit_document(user_id, doc_id, content):
print(f"用户 {user_id} 编辑了文档 {doc_id},内容: {content[:20]}...")
return True
@require_permission("viewer")
def view_report(user_id, report_name):
print(f"用户 {user_id} 查看了报告 {report_name}。")
return True
print("n--- 测试 delete_data ---")
delete_data(1, "item_ABC") # 管理员
delete_data(2, "item_DEF") # 编辑,权限不足
print("n--- 测试 edit_document ---")
edit_document(2, "doc_123", "新的文档内容...") # 编辑
edit_document(3, "doc_456", "查看者尝试编辑...") # 查看者,权限不足
print("n--- 测试 view_report ---")
view_report(3, "monthly_sales") # 查看者
view_report(1, "confidential_report") # 管理员
其他常见的使用场景:
- 缓存 (
@cache): 可以指定缓存的过期时间 (TTL)。 - 重试机制 (
@retry): 可以指定重试次数、重试间隔。 - 频率限制 (
@rate_limit): 限制在一定时间内函数调用的次数。 - 数据验证 (
@validate): 验证传入参数的类型、范围等。 - 数据库事务 (
@transaction): 确保一组操作在一个事务中执行。
突破界限:使用类实现装饰器
到目前为止,我们都使用函数来实现装饰器。然而,在某些情况下,使用类来实现装饰器会更加清晰和强大,特别是当装饰器需要维护状态时。
要使用类实现装饰器,这个类需要具备两个关键方法:
__init__(self, func):构造函数,在装饰器被应用到函数上时调用,通常接收被装饰的函数作为参数。__call__(self, *args, **kwargs):使类的实例像函数一样可以被调用。当被装饰的函数被调用时,实际上是调用了类的实例的__call__方法。
让我们用类来实现一个简单的计数装饰器,统计函数被调用的次数:
from functools import wraps
class CallCounter:
"""一个类装饰器,用于统计函数被调用的次数。"""
def __init__(self, func):
wraps(func)(self) # 同样使用 wraps 来保留被装饰函数的元信息
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)
@CallCounter
def increment(x):
return x + 1
@CallCounter
def decrement(x):
return x - 1
print("n--- 调用 increment ---")
print(increment(10))
print(increment(20))
print(increment(30))
print("n--- 调用 decrement ---")
print(decrement(5))
print(decrement(10))
# 访问装饰器实例的状态
print(f"increment 函数总共调用了 {increment.count} 次。")
print(f"decrement 函数总共调用了 {decrement.count} 次。")
类装饰器的工作原理:
- 当你写
@CallCounter时,Python 会创建一个CallCounter的实例,并将被装饰的函数(例如increment)作为参数传递给__init__方法。increment = CallCounter(increment)
- 现在,
increment变量不再指向原始函数,而是指向CallCounter类的一个实例。 - 当你调用
increment(10)时,实际上是调用了CallCounter实例的__call__方法。
类装饰器相比函数装饰器的优势在于:它们可以更容易地维护状态(如 self.count),并且可以将相关的功能和数据封装在一个类中,提高代码的组织性。
类装饰器的进阶:带参数的类装饰器
将带参数的装饰器与类装饰器结合起来,可以创建功能更强大、更灵活的装饰器。这种情况下,类装饰器的实现方式会有所不同:
__init__(self, *decorator_args, **decorator_kwargs):构造函数此时接收的是装饰器本身的参数,而不是被装饰的函数。__call__(self, func):这个方法现在负责接收被装饰的函数func,并返回一个真正的wrapper函数(或可调用对象)。这个wrapper函数才是执行原始逻辑并添加额外功能的。
让我们用带参数的类装饰器来实现一个通用的重试机制:
import time
import random
from functools import wraps
class Retry:
"""
一个带参数的类装饰器,用于重试函数。参数:
attempts (int): 最大重试次数。delay (int): 每次重试之间的等待秒数。exceptions (tuple): 需要捕获并重试的异常类型。"""
def __init__(self, attempts=3, delay=1, exceptions=(Exception,)):
self.attempts = attempts
self.delay = delay
self.exceptions = exceptions
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(1, self.attempts + 1):
try:
return func(*args, **kwargs)
except self.exceptions as e:
print(f"函数 {func.__name__} 失败 (第 {i}/{self.attempts} 次尝试 ): {e}")
if i < self.attempts:
time.sleep(self.delay)
print(f"函数 {func.__name__} 在 {self.attempts} 次尝试后仍失败。")
raise # 重新抛出最后一次异常
return wrapper
@Retry(attempts=5, delay=2, exceptions=(ConnectionError, TimeoutError))
def unstable_network_call(url):
"""模拟一个可能失败的网络请求。"""
if random.random() < 0.6: # 60% 的几率失败
if random.random() < 0.5:
raise ConnectionError(f"连接到 {url} 失败!")
else:
raise TimeoutError(f"请求 {url} 超时!")
print(f"成功获取 {url} 的数据。")
return "Data from" + url
@Retry(attempts=3, delay=1) # 使用默认异常类型 (Exception)
def might_fail_division(a, b):
result = a / b
print(f"{a} / {b} = {result}")
return result
print("n--- 测试 unstable_network_call ---")
try:
unstable_network_call("http://example.com/api/data")
except (ConnectionError, TimeoutError) as e:
print(f"最终捕获到异常: {e}")
print("n--- 测试 might_fail_division ---")
try:
might_fail_division(10, random.choice([0, 2])) # 有一半概率除以零
might_fail_division(10, 2)
except Exception as e:
print(f"最终捕获到异常: {e}")
带参数的类装饰器工作原理:
- 当你写
@Retry(attempts=5, delay=2, ...)时,Python 首先调用Retry类的__init__方法,并将5,2等参数传递给它。decorator_instance = Retry(attempts=5, delay=2, ...)
__init__方法创建并初始化一个Retry类的实例,将这些参数存储在实例的属性中 (self.attempts,self.delay)。- 现在,
@语法糖会把这个decorator_instance实例作为装饰器来处理。也就是说,unstable_network_call = decorator_instance(unstable_network_call)。 - Python 会调用
decorator_instance的__call__方法,并将原始的unstable_network_call函数作为func参数传递给它。 __call__方法内部定义并返回了wrapper函数,这个wrapper函数封装了重试逻辑,并利用了self存储的attempts和delay参数。
这种模式的类装饰器非常强大,它允许我们定义一个具有配置参数的装饰器,并且这个装饰器可以维护自己的状态,同时又能灵活地包装任何函数。
装饰器实战:将所学付诸实践
至此,我们已经全面探索了 Python 装饰器,从最简单的无参数装饰器,到带参数的函数装饰器,再到功能强大的类装饰器及其带参数的变体。这些技术是 Python 编程中实现代码复用、关注点分离和增强功能的重要工具。
在实际项目中,装饰器无处不在:
- Web 框架 (Flask/Django):路由定义 (
@app.route()), 权限验证 (@login_required), 视图缓存 (@cache_page)。 - ORM 库 (SQLAlchemy):事件监听,自定义类型。
- 并发编程 :线程池 / 进程池管理,异步函数的包装。
- API 开发 :限流、认证、请求 / 响应日志。
掌握了这些,你将能够:
- 提高代码复用性 :将通用逻辑抽象成装饰器,避免重复代码。
- 增强代码可读性 :通过
@语法糖,清晰地表达函数的额外行为。 - 实现关注点分离 :将业务逻辑与非业务逻辑(如日志、计时、权限)分离开来。
总结与展望
Python 装饰器是 Python 语言的一颗璀璨明珠。从函数装饰器的基本用法,到通过多层嵌套实现带参数装饰器,再到利用类实现具有状态的装饰器,以及最终结合两者的带参数类装饰器,我们看到了装饰器强大的表达能力和灵活性。
理解这些概念并加以实践,将极大提升你的 Python 编程能力,让你能够编写出更优雅、更健壮、更易于维护的代码。在你的日常开发中,不妨多多思考哪些通用逻辑可以通过装饰器来抽象,你会发现一片新天地。继续探索 functools 模块中的其他工具,比如 lru_cache,它们也是装饰器强大应用的最佳实践。