作为Python的高级特性之一,装饰器(Decorator)不仅可以用来修改或增强函数的行为,更强大的功能在于通过使用类来实现装饰器,从而实现状态的保持(Stateful Closure)和类方法的属性拦截(Attribute Interception)。
1. 为什么需要带状态的装饰器?
传统的函数式装饰器在每次函数调用之间不会保留状态。如果我们需要记录某个函数被调用的次数、计算平均执行时间,或者实现简单的限流逻辑,我们就需要一个可以持久化状态的机制。实现这一点的最佳方式是使用一个类作为装饰器。
2. 实现状态保持的装饰器 (Call Counter)
我们将创建一个名为 CallTracker 的类,它将记录被装饰函数或方法被调用的总次数。
class CallTracker:
def __init__(self, func):
# 存储原始函数
self.func = func
# 状态:记录调用次数
self.call_count = 0
print(f"[初始化]: {func.__name__} 的追踪器已创建")
def __call__(self, *args, **kwargs):
# 每次调用时更新状态
self.call_count += 1
print(f"--- {self.func.__name__} (总调用次数: {self.call_count}) ---")
# 执行原始函数
return self.func(*args, **kwargs)
# 示例:装饰普通函数
@CallTracker
def add(a, b):
return a + b
print(f"结果: {add(1, 2)}")
print(f"结果: {add(3, 4)}")
print(f"总调用次数(通过访问装饰器实例属性): {add.call_count}")
输出结果:
[初始化]: add 的追踪器已创建
--- add (总调用次数: 1) ---
结果: 3
--- add (总调用次数: 2) ---
结果: 7
总调用次数(通过访问装饰器实例属性): 2
3. 如何解决类方法拦截与绑定问题 (描述符协议)
当我们将上述 CallTracker 应用到类方法上时,会遇到一个问题:类方法需要正确地“绑定”到实例(即自动接收 self 参数)。
当一个装饰器类被用作方法时,它实际上成为了一个描述符(Descriptor)。要让它正确地工作并接收实例的引用,我们需要实现描述符协议中的核心方法 __get__。
__get__(self, instance, owner) 方法的作用是定义当属性被访问时应该返回什么。
- instance: 访问属性的那个对象实例(如果通过实例访问)。
- owner: 拥有该属性的类。
我们修改 CallTracker,使其支持方法绑定:
class MethodCallTracker:
def __init__(self, func):
self.func = func
# 使用字典存储每个实例的调用计数,实现“状态分层”
self.counts = {}
def __get__(self, instance, owner):
if instance is None:
# 通过类访问时,返回自身
return self
# 通过实例访问时,返回一个“绑定”到该实例的新函数闭包
def bound_method(*args, **kwargs):
# 初始化或更新该实例的调用计数
instance_id = id(instance)
self.counts[instance_id] = self.counts.get(instance_id, 0) + 1
count = self.counts[instance_id]
print(f"[实例ID:{instance_id}] 方法 {self.func.__name__} 被调用 {count} 次")
# 执行原始方法,确保将 instance (即 self) 作为第一个参数传入
return self.func(instance, *args, **kwargs)
return bound_method
# 使用这个进阶装饰器
class DatabaseConnection:
def __init__(self, name):
self.name = name
@MethodCallTracker
def fetch_data(self, query):
print(f"-> {self.name} 正在执行查询: {query}")
return [1, 2, 3]
# 创建两个独立的实例
db1 = DatabaseConnection("DB_Prod")
db2 = DatabaseConnection("DB_Test")
db1.fetch_data("SELECT A")
db2.fetch_data("SELECT B")
db1.fetch_data("SELECT C")
输出结果分析:
[实例ID:4728519632] 方法 fetch_data 被调用 1 次
-> DB_Prod 正在执行查询: SELECT A
[实例ID:4728520240] 方法 fetch_data 被调用 1 次
-> DB_Test 正在执行查询: SELECT B
[实例ID:4728519632] 方法 fetch_data 被调用 2 次
-> DB_Prod 正在执行查询: SELECT C
通过实现 __get__ 方法,我们成功地将状态追踪逻辑(self.counts)绑定到了类实例的生命周期上,从而实现了基于装饰器的类方法属性拦截和状态分离。
汤不热吧