一、描述符概述
1. 什么是描述符?
描述符是实现了特定协议的类,可以拦截对另一个类的属性的访问。描述符提供了一种强大的方式来自定义属性访问行为。
2. 描述符协议方法
| | |
|---|
__get__ | | |
__set__ | | |
__delete__ | | |
__set_name__ | | |
3. 描述符类型
# 1. 数据描述符(同时实现 __get__ 和 __set__)class DataDescriptor: def __get__(self, obj, objtype=None): pass def __set__(self, obj, value): pass# 2. 非数据描述符(只实现 __get__)class NonDataDescriptor: def __get__(self, obj, objtype=None): pass
二、基础描述符实现
1. 最简单的描述符
class RevealAccess: """显示访问信息的描述符""" def __init__(self, initial_value=None, name=None): self.value = initial_value self.name = name def __get__(self, obj, objtype=None): if obj is None: return self print(f"获取 {self.name}: {self.value}") return self.value def __set__(self, obj, value): print(f"设置 {self.name}: {self.value} -> {value}") self.value = value def __delete__(self, obj): print(f"删除 {self.name}") del self.valueclass MyClass: attr = RevealAccess(initial_value=10, name="attr") def __init__(self): self.normal_attr = "普通属性"# 使用obj = MyClass()print(obj.attr) # 触发 __get__obj.attr = 20 # 触发 __set__del obj.attr # 触发 __delete__
2. 带类型检查的描述符
class Typed: """类型检查描述符""" def __init__(self, name, expected_type): self.name = name self.expected_type = expected_type def __get__(self, obj, objtype=None): if obj is None: return self return obj.__dict__.get(self.name) def __set__(self, obj, value): if not isinstance(value, self.expected_type): raise TypeError(f"属性 {self.name} 必须是 {self.expected_type.__name__} 类型") obj.__dict__[self.name] = value def __delete__(self, obj): if self.name in obj.__dict__: del obj.__dict__[self.name]class Person: name = Typed("name", str) age = Typed("age", int) salary = Typed("salary", float) def __init__(self, name, age, salary): self.name = name self.age = age self.salary = salary# 使用try: p = Person("Alice", 25, 5000.0) # 正确 print(f"{p.name}, {p.age}, {p.salary}") p2 = Person("Bob", "二十", 6000.0) # 错误:age 必须是 intexcept TypeError as e: print(f"错误: {e}")
三、__set_name__ 方法(Python 3.6+)
1. 自动获取属性名
class ValidatedAttribute: """自动获取属性名的描述符""" def __set_name__(self, owner, name): """当描述符被赋值给类属性时调用 owner: 拥有这个描述符的类 name: 描述符被赋值的属性名 """ self.name = f"_{name}" self.public_name = name print(f"描述符被绑定到 {owner.__name__}.{name}") def __get__(self, obj, objtype=None): if obj is None: return self return getattr(obj, self.name, None) def __set__(self, obj, value): self.validate(value) setattr(obj, self.name, value) def validate(self, value): """子类重写这个方法进行验证""" passclass PositiveNumber(ValidatedAttribute): def validate(self, value): if value <= 0: raise ValueError(f"{self.public_name} 必须为正数")class Range(ValidatedAttribute): def __init__(self, min_val, max_val): self.min_val = min_val self.max_val = max_val def validate(self, value): if not (self.min_val <= value <= self.max_val): raise ValueError(f"{self.public_name} 必须在 {self.min_val}-{self.max_val} 之间")class Product: price = PositiveNumber() quantity = Range(0, 100) discount = Range(0, 1) def __init__(self, name, price, quantity, discount=0): self.name = name self.price = price self.quantity = quantity self.discount = discount# 使用product = Product("手机", 2999, 50, 0.1)print(f"{product.name}: 价格={product.price}, 数量={product.quantity}, 折扣={product.discount}")try: product.price = -100 # 触发验证except ValueError as e: print(f"错误: {e}")try: product.quantity = 200 # 超出范围except ValueError as e: print(f"错误: {e}")
四、数据描述符 vs 非数据描述符
1. 数据描述符优先级
class DataDescriptor: """数据描述符(实现 __set__)""" def __init__(self): self.value = "描述符值" def __get__(self, obj, objtype=None): print("数据描述符 __get__ 被调用") return self.value def __set__(self, obj, value): print("数据描述符 __set__ 被调用") self.value = valueclass NonDataDescriptor: """非数据描述符(只实现 __get__)""" def __init__(self): self.value = "非数据描述符值" def __get__(self, obj, objtype=None): print("非数据描述符 __get__ 被调用") return self.valueclass TestClass: data_desc = DataDescriptor() non_data_desc = NonDataDescriptor() def __init__(self): self.instance_attr = "实例属性"# 演示优先级obj = TestClass()print("=== 数据描述符 ===")print(obj.data_desc) # 调用描述符 __get__obj.data_desc = "新值" # 调用描述符 __set__print(obj.data_desc) # 仍然调用描述符print("\n=== 非数据描述符 ===")print(obj.non_data_desc) # 调用描述符 __get__obj.non_data_desc = "实例属性覆盖" # 在实例上设置属性print(obj.non_data_desc) # 返回实例属性(不再调用描述符)print(obj.__dict__) # 查看实例字典
2. 属性访问优先级
class PriorityDemonstration: """演示属性访问优先级""" class Descriptor: def __get__(self, obj, objtype=None): print("描述符 __get__") return "描述符值" def __set__(self, obj, value): print("描述符 __set__") class NonDataDesc: def __get__(self, obj, objtype=None): print("非数据描述符 __get__") return "非数据描述符值" # 类属性 data_desc = Descriptor() non_data_desc = NonDataDesc() class_attr = "类属性值" def __init__(self): # 实例属性 self.instance_attr = "实例属性值" self.non_data_desc = "实例覆盖的非数据描述符" # 会覆盖非数据描述符# 演示obj = PriorityDemonstration()print("1. 实例属性:", obj.instance_attr) # 直接返回实例属性print("2. 类属性:", obj.class_attr) # 返回类属性print("3. 数据描述符:", obj.data_desc) # 返回描述符值obj.data_desc = "尝试修改" # 触发描述符 __set__print("4. 非数据描述符(被覆盖):", obj.non_data_desc) # 返回实例属性print("\n属性查找顺序:")print("实例字典 → 数据描述符 → 非数据描述符 → 类属性 → 父类属性")
五、最佳实践和注意事项
1. 描述符选择指南
2. 性能考虑
import timeclass DescriptorPerformance: """描述符性能测试""" class FastDescriptor: """快速描述符(使用实例字典)""" def __set_name__(self, owner, name): self.name = f"_{name}" def __get__(self, obj, objtype=None): if obj is None: return self return getattr(obj, self.name) def __set__(self, obj, value): setattr(obj, self.name, value) class SlowDescriptor: """慢速描述符(每次都处理)""" def __init__(self): self.values = {} def __get__(self, obj, objtype=None): if obj is None: return self # 每次都做一些处理 result = self.values.get(id(obj)) return result def __set__(self, obj, value): # 每次都做一些处理 self.values[id(obj)] = value class TestClass: fast = FastDescriptor() slow = SlowDescriptor() def __init__(self): self.normal = 0# 性能测试obj = DescriptorPerformance.TestClass()# 测试快速描述符start = time.perf_counter()for _ in range(1000000): obj.fast = 1 x = obj.fastfast_time = time.perf_counter() - start# 测试慢速描述符start = time.perf_counter()for _ in range(1000000): obj.slow = 1 x = obj.slowslow_time = time.perf_counter() - startprint(f"快速描述符: {fast_time:.4f}秒")print(f"慢速描述符: {slow_time:.4f}秒")print(f"慢速比快速慢 {slow_time/fast_time:.1f} 倍")
3. 注意事项
class DescriptorPitfalls: """描述符使用陷阱""" # 陷阱1:忘记处理 obj 为 None 的情况 class BadDescriptor: def __get__(self, obj, objtype=None): return self.value # 如果 obj 为 None,这里会出错 class GoodDescriptor: def __get__(self, obj, objtype=None): if obj is None: return self # 正确处理 return self.value # 陷阱2:在多个实例间共享状态 class SharedStateDescriptor: def __init__(self): self.value = {} # 所有实例共享! def __get__(self, obj, objtype=None): return self.value.get(id(obj)) def __set__(self, obj, value): self.value[id(obj)] = value # 陷阱3:忘记调用 super() class InheritanceDescriptor: def __set__(self, obj, value): # 应该调用父类的 __set__ obj.__dict__[self.name] = value # 跳过可能的父类逻辑 # 陷阱4:循环引用 class CircularDescriptor: def __get__(self, obj, objtype=None): return getattr(obj, 'circular') # 可能导致无限递归
4. 描述符 vs 属性
class Comparison: """描述符和@property的比较""" # 使用 @property(简单场景) class WithProperty: def __init__(self): self._value = 0 @property def value(self): return self._value @value.setter def value(self, new_value): if new_value < 0: raise ValueError("不能为负数") self._value = new_value # 使用描述符(可复用场景) class PositiveNumber: def __set_name__(self, owner, name): self.name = f"_{name}" def __get__(self, obj, objtype=None): if obj is None: return self return getattr(obj, self.name) def __set__(self, obj, value): if value < 0: raise ValueError(f"{self.name[1:]} 不能为负数") setattr(obj, self.name, value) class WithDescriptor: value = PositiveNumber() def __init__(self, value): self.value = value# 使用场景对比print("=== @property ===")prop_obj = Comparison.WithProperty()prop_obj.value = 10print(prop_obj.value)print("\n=== 描述符 ===")desc_obj = Comparison.WithDescriptor(10)print(desc_obj.value)# 描述符可复用class AnotherClass: price = Comparison.PositiveNumber() quantity = Comparison.PositiveNumber() def __init__(self, price, quantity): self.price = price self.quantity = quantityanother = AnotherClass(100, 5)print(f"价格: {another.price}, 数量: {another.quantity}")
5. 总结要点
数据描述符优先级高于实例属性
非数据描述符优先级低于实例属性
始终处理 obj 为 None 的情况
使用 __set_name__ 自动获取属性名
避免在多个实例间共享状态
考虑性能影响,特别是频繁访问的属性
描述符最适合需要复用的属性行为
对于单一属性,@property 可能更简单
描述符是Python中非常强大的特性,它们构成了property、classmethod、staticmethod等装饰器的基础。合理使用描述符可以创建出优雅、可复用的代码。