描述符
Python描述符协议详解:__get__、__set__、__delete__。
概述
描述符(Descriptor)是 Python 中实现属性访问控制的核心协议。任何实现了 __get__、__set__ 或 __delete__ 方法的类都是描述符。描述符是 property、classmethod、staticmethod 等内置功能的底层机制,也是 ORM 框架中字段定义的基础。
基础概念
描述符协议
描述符协议包含以下方法:
__get__(self, obj, objtype=None):访问属性时调用__set__(self, obj, value):设置属性时调用__delete__(self, obj):删除属性时调用
数据描述符与非数据描述符
- 数据描述符(Data Descriptor):实现了
__set__或__delete__的描述符 - 非数据描述符(Non-Data Descriptor):只实现了
__get__的描述符
两者的关键区别在于优先级:数据描述符的优先级高于实例属性,非数据描述符的优先级低于实例属性。
# 数据描述符:优先级高于实例属性
class Validated:
def __get__(self, obj, objtype=None):
return getattr(obj, self.name, None)
def __set__(self, obj, value):
setattr(obj, self.name, value)
# 非数据描述符:优先级低于实例属性
class Lazy:
def __get__(self, obj, objtype=None):
if obj is None:
return self
return self.compute(obj)
def compute(self, obj):
return "计算结果"
属性查找顺序
Python 的属性查找遵循以下优先级:
- 数据描述符(类上的
__get__+__set__) - 实例属性(
obj.__dict__) - 非数据描述符(类上的
__get__) - 类属性
快速上手
第一个描述符
class TypedField:
"""类型检查描述符"""
def __init__(self, expected_type, default=None):
self.expected_type = expected_type
self.default = default
def __set_name__(self, owner, name):
"""Python 3.6+:自动获取属性名"""
self.name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.name, self.default)
def __set__(self, obj, value):
if not isinstance(value, self.expected_type):
raise TypeError(
f"期望 {self.expected_type.__name__},得到 {type(value).__name__}"
)
setattr(obj, self.name, value)
class Person:
name = TypedField(str, "")
age = TypedField(int, 0)
p = Person()
p.name = "Alice" # OK
# p.name = 42 # TypeError: 期望 str,得到 int
p.age = 30 # OK
property 是描述符
property 本质上是一个数据描述符:
# 使用 property
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("半径不能为负")
self._radius = value
# 等价的描述符实现
class Radius:
def __get__(self, obj, objtype=None):
return obj._radius
def __set__(self, obj, value):
if value < 0:
raise ValueError("半径不能为负")
obj._radius = value
详细用法
验证描述符
class Range:
"""范围验证描述符"""
def __init__(self, min_val=None, max_val=None):
self.min_val = min_val
self.max_val = max_val
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 self.min_val is not None and value < self.min_val:
raise ValueError(f"值不能小于 {self.min_val}")
if self.max_val is not None and value > self.max_val:
raise ValueError(f"值不能大于 {self.max_val}")
setattr(obj, self.name, value)
class Student:
score = Range(0, 100)
age = Range(0, 150)
s = Student()
s.score = 95 # OK
# s.score = -1 # ValueError: 值不能小于 0
# s.score = 200 # ValueError: 值不能大于 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)
setattr(obj, self.name, value)
return value
class DataLoader:
def __init__(self, path):
self.path = path
@LazyProperty
def data(self):
"""首次访问时加载数据,之后使用缓存"""
print("正在加载数据...")
with open(self.path) as f:
return f.read()
loader = DataLoader("data.txt")
print(loader.data) # 输出: 正在加载数据... + 数据内容
print(loader.data) # 直接返回缓存,不再加载
委托描述符
class Alias:
"""属性别名描述符"""
def __init__(self, target):
self.target = target
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.target)
def __set__(self, obj, value):
setattr(obj, self.target, value)
class User:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
name = Alias("first_name") # name 是 first_name 的别名
u = User("Alice", "Smith")
print(u.name) # Alice
u.name = "Bob"
print(u.first_name) # Bob
方法描述符
函数本身就是非数据描述符,这就是方法绑定的工作原理:
class MyClass:
def method(self):
return "实例方法"
# 函数的 __get__ 实现了方法绑定
obj = MyClass()
print(type(MyClass.method)) # function(未绑定)
print(type(obj.method)) # method(绑定到 obj)
常见场景
场景一:ORM 字段
class Column:
"""数据库列描述符"""
def __init__(self, col_type, primary_key=False, nullable=True):
self.col_type = col_type
self.primary_key = primary_key
self.nullable = nullable
def __set_name__(self, owner, name):
self.name = name
self.storage = f"_{name}_value"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage, None)
def __set__(self, obj, value):
if value is None and not self.nullable:
raise ValueError(f"{self.name} 不能为空")
if value is not None and not isinstance(value, self.col_type):
raise TypeError(f"{self.name} 类型错误")
setattr(obj, self.storage, value)
class User:
id = Column(int, primary_key=True, nullable=False)
name = Column(str, nullable=False)
email = Column(str, nullable=True)
场景二:缓存属性
class Cached:
"""带过期时间的缓存描述符"""
def __init__(self, ttl=60):
self.ttl = ttl
def __set_name__(self, owner, name):
self.name = name
self.cache_key = f"_{name}_cache"
self.time_key = f"_{name}_time"
def __get__(self, obj, objtype=None):
if obj is None:
return self
import time
last_time = getattr(obj, self.time_key, 0)
if time.time() - last_time > self.ttl:
return None # 缓存过期
return getattr(obj, self.cache_key, None)
def __set__(self, obj, value):
import time
setattr(obj, self.cache_key, value)
setattr(obj, self.time_key, time.time())
场景三:只读属性
class ReadOnly:
"""只读描述符:初始化后不可修改"""
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 hasattr(obj, self.name):
raise AttributeError(f"属性 {self.name[1:]} 是只读的")
setattr(obj, self.name, value)
class Config:
host = ReadOnly()
port = ReadOnly()
c = Config()
c.host = "localhost" # 首次设置 OK
# c.host = "other" # AttributeError: 属性 host 是只读的
注意事项
- 描述符必须定义为类属性,定义在实例上不会生效
__set_name__在 Python 3.6+ 可用,之前版本需要手动传递属性名- 数据描述符的
__get__在访问时总是被调用,即使实例属性存在 - 非数据描述符可以被实例属性覆盖
- 描述符中存储值时不要使用与描述符同名的属性,否则会导致无限递归
- 使用
obj.__dict__或带前缀的属性名存储实际值
进阶用法
描述符与元类配合
class ModelMeta(type):
"""收集所有描述符字段"""
def __new__(mcs, name, bases, namespace):
fields = {}
for key, value in namespace.items():
if isinstance(value, Column):
fields[key] = value
namespace['_fields'] = fields
return super().__new__(mcs, name, bases, namespace)
class Model(metaclass=ModelMeta):
def validate(self):
"""验证所有字段"""
for name, field in self._fields.items():
value = getattr(self, name)
if value is None and not field.nullable:
raise ValueError(f"{name} 不能为空")
描述符链
class Validated:
"""可组合的验证描述符"""
def __init__(self, *validators):
self.validators = validators
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):
for validator in self.validators:
validator(value) # 执行验证
setattr(obj, self.name, value)
def positive(value):
if value <= 0:
raise ValueError("值必须为正数")
def even(value):
if value % 2 != 0:
raise ValueError("值必须是偶数")
class Settings:
count = Validated(positive, even)
s = Settings()
s.count = 4 # OK
# s.count = -2 # ValueError: 值必须为正数
# s.count = 3 # ValueError: 值必须是偶数