描述符协议
描述符协议与属性管理
什么是描述符
描述符是 Python 中一种强大的协议,它允许你自定义属性的访问行为。当你用 obj.attr 访问属性时,如果这个属性是一个描述符对象,Python 就会调用描述符的方法,而不是直接返回属性值。
描述符是 Python 许多底层机制的实现基础。property、classmethod、staticmethod 都是通过描述符实现的。理解描述符能让你写出更优雅、更可复用的属性管理代码。
基础概念
描述符协议
一个描述符是实现以下任意方法的类:
- __get__(self, obj, objtype):当属性被读取时调用
- __set__(self, obj, value):当属性被赋值时调用
- __delete__(self, obj):当属性被删除时调用
数据描述符与非数据描述符
- 数据描述符:同时实现了 __get__ 和 __set__(或 __delete__)的描述符。它的优先级高于实例属性
- 非数据描述符:只实现了 __get__ 的描述符。它的优先级低于实例属性
这个区别很重要,它决定了当实例属性和描述符同名时,哪个优先。
查找顺序
当你访问 obj.attr 时,Python 的查找顺序是:
- 数据描述符(在类中定义的)
- 实例属性(在 __dict__ 中)
- 非数据描述符(在类中定义的)
- 类属性
快速上手
最简单的描述符
class SimpleDescriptor:
"""最简单的描述符:记录属性被访问的次数"""
def __init__(self, name):
self.name = name
self.access_count = 0
def __get__(self, obj, objtype=None):
self.access_count += 1
print(f"{self.name} 被访问了 {self.access_count} 次")
# 从实例的 __dict__ 中获取实际值
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
print(f"{self.name} 被设置为 {value}")
obj.__dict__[self.name] = value
class MyClass:
# 使用描述符
name = SimpleDescriptor("name")
obj = MyClass()
obj.name = "张三" # 输出: name 被设置为 张三
print(obj.name) # 输出: name 被访问了 1 次, 然后输出: 张三
obj.name = "李四" # 输出: name 被设置为 李四
print(obj.name) # 输出: name 被访问了 2 次, 然后输出: 李四
使用 property(内置描述符)
property 是 Python 内置的描述符工厂,是最常用的描述符形式:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
"""获取名字"""
return self._name
@name.setter
def name(self, value):
"""设置名字,自动去除首尾空格"""
if not value:
raise ValueError("名字不能为空")
self._name = value.strip()
@name.deleter
def name(self):
"""删除名字"""
print("名字已被删除")
del self._name
# 使用
p = Person(" 张三 ")
print(p.name) # 张三(自动去除了空格)
p.name = " 李四 "
print(p.name) # 李四
del p.name # 名字已被删除
详细用法
类型验证描述符
创建一个可复用的类型验证描述符:
class ValidatedAttribute:
"""类型验证描述符:确保属性值的类型正确"""
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
self.private_name = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"{self.name} 应该是 {self.expected_type.__name__} 类型,"
f"但传入了 {type(value).__name__}"
)
setattr(obj, self.private_name, value)
class User:
name = ValidatedAttribute('name', str)
age = ValidatedAttribute('age', int)
# 使用
user = User()
user.name = "张三" # 正确
user.age = 25 # 正确
# user.age = "25" # TypeError: age 应该是 int 类型,但传入了 str
范围验证描述符
class RangeValidated:
"""范围验证描述符:确保数值在指定范围内"""
def __init__(self, name, min_value=None, max_value=None):
self.name = name
self.min_value = min_value
self.max_value = max_value
self.private_name = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.private_name, None)
def __set__(self, obj, value):
if self.min_value is not None and value < self.min_value:
raise ValueError(f"{self.name} 不能小于 {self.min_value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"{self.name} 不能大于 {self.max_value}")
setattr(obj, self.private_name, value)
class Student:
age = RangeValidated('age', min_value=0, max_value=150)
score = RangeValidated('score', min_value=0, max_value=100)
student = Student()
student.age = 20 # 正确
student.score = 95 # 正确
# student.age = -1 # ValueError: age 不能小于 0
# student.score = 150 # ValueError: score 不能大于 100
延迟计算描述符
class LazyProperty:
"""延迟计算描述符:第一次访问时计算,之后缓存结果"""
def __init__(self, func):
self.func = func
self.name = func.__name__
def __get__(self, obj, objtype=None):
if obj is None:
return self
# 计算并缓存结果
value = self.func(obj)
obj.__dict__[self.name] = value
return value
class DataProcessor:
def __init__(self, data):
self.data = data
@LazyProperty
def expensive_result(self):
"""耗时的计算,只在第一次访问时执行"""
print("执行耗时计算...")
import time
time.sleep(1)
return sum(x ** 2 for x in self.data)
processor = DataProcessor(range(1000))
print("第一次访问:")
print(processor.expensive_result) # 会执行计算
print("第二次访问:")
print(processor.expensive_result) # 直接返回缓存结果
只读属性描述符
class ReadOnly:
"""只读属性描述符:只能在初始化时设置,之后不可修改"""
def __init__(self, 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, None)
def __set__(self, obj, value):
# 如果已经有值,则不允许修改
if hasattr(obj, self.private_name):
raise AttributeError(f"{self.name} 是只读属性")
setattr(obj, self.private_name, value)
class Config:
api_key = ReadOnly('api_key')
base_url = ReadOnly('base_url')
def __init__(self, api_key, base_url):
self.api_key = api_key # 初始化时可以设置
self.base_url = base_url
config = Config("secret-key", "https://api.example.com")
# config.api_key = "new-key" # AttributeError: api_key 是只读属性
常见场景
实现类似 Django ORM 的字段定义
class Field:
"""数据库字段描述符基类"""
def __init__(self, name=None, nullable=True, default=None):
self.name = name
self.nullable = nullable
self.default = default
self.private_name = None
def __set_name__(self, owner, name):
"""Python 3.6+ 自动调用,获取字段名"""
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, self.default)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
def validate(self, value):
if value is None and not self.nullable:
raise ValueError(f"{self.name} 不能为空")
class CharField(Field):
def __init__(self, max_length=255, **kwargs):
super().__init__(**kwargs)
self.max_length = max_length
def validate(self, value):
super().validate(value)
if value is not None and len(str(value)) > self.max_length:
raise ValueError(f"{self.name} 长度不能超过 {self.max_length}")
class IntegerField(Field):
def validate(self, value):
super().validate(value)
if value is not None and not isinstance(value, int):
raise TypeError(f"{self.name} 必须是整数")
class User:
name = CharField(max_length=100, nullable=False)
age = IntegerField(nullable=True, default=0)
user = User()
user.name = "张三"
user.age = 25
# user.name = "A" * 200 # ValueError: name 长度不能超过 100
实现观察者模式
class ObservableAttribute:
"""可观察属性:值变化时通知观察者"""
def __init__(self, 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, None)
def __set__(self, obj, value):
old_value = getattr(obj, self.private_name, None)
setattr(obj, self.private_name, value)
if old_value != value and hasattr(obj, '_observers'):
for callback in obj._observers:
callback(self.name, old_value, value)
class Model:
status = ObservableAttribute('status')
def __init__(self):
self._observers = []
def observe(self, callback):
self._observers.append(callback)
def on_status_change(attr, old, new):
print(f"状态变化: {old} -> {new}")
model = Model()
model.observe(on_status_change)
model.status = "running" # 输出: 状态变化: None -> running
model.status = "stopped" # 输出: 状态变化: running -> stopped
注意事项与常见错误
描述符必须定义为类属性
描述符必须定义在类中,不能定义在实例中。如果写在 __init__ 中,它就是一个普通实例属性,描述符协议不会生效。
__set_name__ 的作用
Python 3.6 引入了 __set_name__ 方法,当描述符被定义到类中时自动调用,参数是类和属性名。这解决了描述符不知道自己叫什么名字的问题。
避免在 __get__ 中返回 self
当 obj 为 None 时(通过类访问属性,如 MyClass.attr),通常应该返回描述符自身。否则 MyClass.attr 会报错。
数据存储位置
描述符通常不自己存储数据,而是把数据存在实例的 __dict__ 中。如果描述符自己存储数据,所有实例会共享同一个值。
进阶用法
描述符与元类结合
使用元类自动注册描述符字段:
class ModelMeta(type):
"""模型元类:自动收集字段"""
def __new__(mcs, name, bases, namespace):
fields = {}
for key, value in namespace.items():
if isinstance(value, Field):
fields[key] = value
namespace['_fields'] = fields
return super().__new__(mcs, name, bases, namespace)
class Model(metaclass=ModelMeta):
"""模型基类"""
pass
class User(Model):
name = CharField(max_length=100)
age = IntegerField()
# User._fields 自动包含 name 和 age
使用 __delete__ 方法
class CachedProperty:
"""可清除的缓存属性"""
def __init__(self, func):
self.func = func
self.name = func.__name__
def __get__(self, obj, objtype=None):
if obj is None:
return self
value = self.func(obj)
obj.__dict__[self.name] = value
return value
def __delete__(self, obj):
"""删除缓存,下次访问时重新计算"""
obj.__dict__.pop(self.name, None)