Python 描述符详解
描述符的基本概念
描述符是 Python 中实现属性访问控制的强大机制。描述符协议包含三个方法:__get__、__set__ 和 __delete__。任何实现了这些方法的对象都可以作为描述符使用。
描述符协议
pythonclass Descriptor: def __get__(self, obj, objtype=None): """获取属性值""" pass def __set__(self, obj, value): """设置属性值""" pass def __delete__(self, obj): """删除属性""" pass
数据描述符 vs 非数据描述符
数据描述符
实现了 __get__ 和 __set__ 方法的描述符称为数据描述符。
pythonclass DataDescriptor: def __init__(self, initial_value=None): self.value = initial_value def __get__(self, obj, objtype=None): print(f"获取数据描述符值: {self.value}") return self.value def __set__(self, obj, value): print(f"设置数据描述符值: {value}") self.value = value class MyClass: attr = DataDescriptor(42) obj = MyClass() print(obj.attr) # 获取数据描述符值: 42 obj.attr = 100 # 设置数据描述符值: 100 print(obj.attr) # 获取数据描述符值: 100
非数据描述符
只实现了 __get__ 方法的描述符称为非数据描述符。
pythonclass NonDataDescriptor: def __init__(self, initial_value=None): self.value = initial_value def __get__(self, obj, objtype=None): print(f"获取非数据描述符值: {self.value}") return self.value class MyClass: attr = NonDataDescriptor(42) obj = MyClass() print(obj.attr) # 获取非数据描述符值: 42 obj.attr = 100 # 设置实例属性,不调用 __set__ print(obj.attr) # 100(实例属性优先)
数据描述符 vs 非数据描述符的优先级
pythonclass DataDesc: def __get__(self, obj, objtype=None): return "数据描述符" def __set__(self, obj, value): pass class NonDataDesc: def __get__(self, obj, objtype=None): return "非数据描述符" class MyClass: data_desc = DataDesc() non_data_desc = NonDataDesc() obj = MyClass() obj.data_desc = "实例值" obj.non_data_desc = "实例值" print(obj.data_desc) # 数据描述符(数据描述符优先) print(obj.non_data_desc) # 实例值(实例属性优先)
描述符的实际应用
1. 类型检查
pythonclass 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} 类型," f"但得到的是 {type(value)}" ) obj.__dict__[self.name] = value class Person: name = Typed('name', str) age = Typed('age', int) person = Person() person.name = "Alice" # 正常 person.age = 25 # 正常 # person.age = "25" # TypeError: 属性 age 应该是 <class 'int'> 类型,但得到的是 <class 'str'>
2. 值验证
pythonclass Validated: """值验证描述符""" def __init__(self, name, validator): self.name = name self.validator = validator 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 self.validator(value): raise ValueError(f"属性 {self.name} 的值 {value} 无效") obj.__dict__[self.name] = value class Person: age = Validated('age', lambda x: isinstance(x, int) and 0 <= x <= 150) name = Validated('name', lambda x: isinstance(x, str) and len(x) > 0) person = Person() person.age = 25 # 正常 person.name = "Alice" # 正常 # person.age = -5 # ValueError: 属性 age 的值 -5 无效 # person.name = "" # ValueError: 属性 name 的值 无效
3. 延迟计算
pythonclass LazyProperty: """延迟计算属性描述符""" def __init__(self, func): self.func = func self.name = func.__name__ def __get__(self, obj, objtype=None): if obj is None: return self # 检查是否已经计算过 if self.name not in obj.__dict__: print(f"延迟计算 {self.name}") obj.__dict__[self.name] = self.func(obj) return obj.__dict__[self.name] class Circle: def __init__(self, radius): self.radius = radius @LazyProperty def area(self): print("计算面积...") return 3.14159 * self.radius ** 2 @LazyProperty def circumference(self): print("计算周长...") return 2 * 3.14159 * self.radius circle = Circle(5) print(circle.area) # 延迟计算 area, 计算面积..., 78.53975 print(circle.area) # 78.53975(直接返回缓存值) print(circle.circumference) # 延迟计算 circumference, 计算周长..., 31.4159
4. 只读属性
pythonclass ReadOnly: """只读属性描述符""" def __init__(self, name, value): self.name = name self.value = value def __get__(self, obj, objtype=None): if obj is None: return self return self.value def __set__(self, obj, value): raise AttributeError(f"属性 {self.name} 是只读的") class Config: VERSION = ReadOnly('VERSION', '1.0.0') AUTHOR = ReadOnly('AUTHOR', 'Alice') config = Config() print(config.VERSION) # 1.0.0 # config.VERSION = '2.0.0' # AttributeError: 属性 VERSION 是只读的
5. 属性访问日志
pythonclass Logged: """属性访问日志描述符""" def __init__(self, name): self.name = name def __get__(self, obj, objtype=None): if obj is None: return self value = obj.__dict__.get(self.name) print(f"读取属性 {self.name}: {value}") return value def __set__(self, obj, value): print(f"设置属性 {self.name}: {value}") obj.__dict__[self.name] = value def __delete__(self, obj): print(f"删除属性 {self.name}") del obj.__dict__[self.name] class Person: name = Logged('name') age = Logged('age') person = Person() person.name = "Alice" # 设置属性 name: Alice person.age = 25 # 设置属性 age: 25 print(person.name) # 读取属性 name: Alice del person.age # 删除属性 age
6. 缓存属性
pythonclass Cached: """缓存属性描述符""" def __init__(self, func): self.func = func self.name = func.__name__ self.cache = {} def __get__(self, obj, objtype=None): if obj is None: return self # 使用对象 ID 作为缓存键 obj_id = id(obj) if obj_id not in self.cache: print(f"计算并缓存 {self.name}") self.cache[obj_id] = self.func(obj) else: print(f"使用缓存 {self.name}") return self.cache[obj_id] class ExpensiveCalculator: def __init__(self, base): self.base = base @Cached def expensive_operation(self): print("执行耗时操作...") import time time.sleep(1) return self.base ** 2 calc = ExpensiveCalculator(5) print(calc.expensive_operation) # 计算并缓存 expensive_operation, 执行耗时操作..., 25 print(calc.expensive_operation) # 使用缓存 expensive_operation, 25
描述符与 property 的关系
property 本质上是描述符
python# property 实际上是一个描述符类 class MyClass: @property def my_property(self): return "property 值" @my_property.setter def my_property(self, value): print(f"设置 property: {value}") obj = MyClass() print(obj.my_property) # property 值 obj.my_property = "新值" # 设置 property: 新值
自定义 property 类
pythonclass MyProperty: """自定义 property 类""" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError("不可读") return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError("不可写") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError("不可删除") self.fdel(obj) def getter(self, fget): self.fget = fget return self def setter(self, fset): self.fset = fset return self def deleter(self, fdel): self.fdel = fdel return self class Person: def __init__(self): self._name = "" @MyProperty def name(self): return self._name @name.setter def name(self, value): self._name = value person = Person() person.name = "Alice" print(person.name) # Alice
描述符的高级用法
描述符与类方法
pythonclass ClassMethodDescriptor: """类方法描述符""" def __init__(self, func): self.func = func def __get__(self, obj, objtype=None): if objtype is None: objtype = type(obj) return self.func.__get__(objtype, objtype) class MyClass: @ClassMethodDescriptor def class_method(cls): return f"类方法: {cls.__name__}" print(MyClass.class_method()) # 类方法: MyClass
描述符与静态方法
pythonclass StaticMethodDescriptor: """静态方法描述符""" def __init__(self, func): self.func = func def __get__(self, obj, objtype=None): return self.func class MyClass: @StaticMethodDescriptor def static_method(): return "静态方法" print(MyClass.static_method()) # 静态方法
描述符链
pythonclass ValidatedTyped: """验证和类型检查组合描述符""" def __init__(self, name, expected_type, validator=None): self.name = name self.expected_type = expected_type self.validator = validator 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} 类型" ) # 值验证 if self.validator and not self.validator(value): raise ValueError(f"属性 {self.name} 的值 {value} 无效") obj.__dict__[self.name] = value class Person: age = ValidatedTyped( 'age', int, lambda x: 0 <= x <= 150 ) name = ValidatedTyped( 'name', str, lambda x: len(x) > 0 ) person = Person() person.name = "Alice" person.age = 25 # person.age = "25" # TypeError # person.age = -5 # ValueError
描述符的最佳实践
1. 使用 set_name 方法(Python 3.6+)
pythonclass Descriptor: """使用 __set_name__ 的描述符""" def __set_name__(self, owner, name): self.name = name self.private_name = f'_{name}' def __get__(self, obj, objtype=None): if obj is None: return self return getattr(obj, self.private_name) def __set__(self, obj, value): setattr(obj, self.private_name, value) class MyClass: attr = Descriptor() obj = MyClass() obj.attr = 42 print(obj.attr) # 42
2. 避免描述符中的循环引用
pythonimport weakref class WeakRefDescriptor: """使用弱引用的描述符""" def __init__(self): self.instances = weakref.WeakKeyDictionary() def __get__(self, obj, objtype=None): if obj is None: return self return self.instances.get(obj) def __set__(self, obj, value): self.instances[obj] = value class MyClass: attr = WeakRefDescriptor() obj = MyClass() obj.attr = 42 print(obj.attr) # 42
3. 提供清晰的错误信息
pythonclass ValidatedDescriptor: """提供清晰错误信息的描述符""" 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"在类 {obj.__class__.__name__} 中," f"属性 '{self.name}' 应该是 {self.expected_type.__name__} 类型," f"但得到的是 {type(value).__name__}" ) obj.__dict__[self.name] = value class Person: age = ValidatedDescriptor('age', int) person = Person() # person.age = "25" # TypeError: 在类 Person 中,属性 'age' 应该是 int 类型,但得到的是 str
描述符的实际应用案例
1. ORM 模型字段
pythonclass Field: """ORM 字段描述符""" def __init__(self, field_type, primary_key=False): self.field_type = field_type self.primary_key = primary_key self.name = None def __set_name__(self, owner, name): self.name = name def __get__(self, obj, objtype=None): if obj is None: return self return obj.__dict__.get(f'_{self.name}') def __set__(self, obj, value): if not isinstance(value, self.field_type): raise TypeError(f"字段 {self.name} 类型错误") obj.__dict__[f'_{self.name}'] = value class ModelMeta(type): """模型元类""" def __new__(cls, name, bases, attrs): fields = {} for key, value in list(attrs.items()): if isinstance(value, Field): fields[key] = value attrs['_fields'] = fields return super().__new__(cls, name, bases, attrs) class User(metaclass=ModelMeta): id = Field(int, primary_key=True) name = Field(str) age = Field(int) user = User() user.name = "Alice" user.age = 25 print(user.name) # Alice
2. 单位转换
pythonclass Temperature: """温度单位转换描述符""" def __init__(self, name): self.name = name def __get__(self, obj, objtype=None): if obj is None: return self return obj.__dict__.get(f'_{self.name}') def __set__(self, obj, value): if isinstance(value, (int, float)): obj.__dict__[f'_{self.name}'] = value elif isinstance(value, str): if value.endswith('°C'): obj.__dict__[f'_{self.name}'] = float(value[:-2]) elif value.endswith('°F'): obj.__dict__[f'_{self.name}'] = (float(value[:-2]) - 32) * 5/9 else: raise ValueError("无效的温度格式") else: raise TypeError("无效的温度类型") class Weather: celsius = Temperature('celsius') weather = Weather() weather.celsius = "25°C" print(weather.celsius) # 25.0 weather.celsius = "77°F" print(weather.celsius) # 25.0
总结
Python 描述符的核心概念:
- 描述符协议:
__get__、__set__、__delete__三个方法 - 数据描述符:实现了
__get__和__set__,优先级高于实例属性 - 非数据描述符:只实现了
__get__,优先级低于实例属性 - 实际应用:
- 类型检查
- 值验证
- 延迟计算
- 只读属性
- 属性访问日志
- 缓存属性
描述符的优势:
- 强大的属性访问控制
- 可重用的属性逻辑
- 清晰的代码组织
- 与 Python 内置机制集成
描述符的最佳实践:
- 使用
__set_name__方法(Python 3.6+) - 避免循环引用,使用弱引用
- 提供清晰的错误信息
- 理解描述符的优先级规则
- 考虑使用更简单的替代方案(property)
描述符是 Python 中实现高级属性控制的核心机制,理解描述符对于深入掌握 Python 的面向对象编程非常重要。property、classmethod、staticmethod 等都是基于描述符实现的。