第 14 章 描述符
描述符是 Python 属性访问机制的底层实现,理解它就理解了 property、classmethod、staticmethod 的工作原理。
14.1 描述符协议
描述符是实现了以下任一方法的类:
class Descriptor:
def __get__(self, obj, objtype=None):
"""obj.attr 时调用"""
...
def __set__(self, obj, value):
"""obj.attr = value 时调用"""
...
def __delete__(self, obj):
"""del obj.attr 时调用"""
...
一个简单的描述符:
class Verbose:
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f"_verbose_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self # 通过类访问时返回描述符本身
value = getattr(obj, self.storage_name, None)
print(f"获取 {self.name} = {value}")
return value
def __set__(self, obj, value):
print(f"设置 {self.name} = {value}")
setattr(obj, self.storage_name, value)
class MyClass:
x = Verbose()
y = Verbose()
obj = MyClass()
obj.x = 10 # 设置 x = 10
obj.y = 20 # 设置 y = 20
print(obj.x) # 获取 x = 10 → 10
14.2 数据描述符 vs 非数据描述符
- 数据描述符:定义了
__set__和/或__delete__的描述符 - 非数据描述符:只定义了
__get__的描述符
区别在于属性查找优先级:
数据描述符 > 实例 __dict__ > 非数据描述符
class DataDescriptor:
"""数据描述符 — 有 __get__ 和 __set__"""
def __get__(self, obj, objtype=None):
return "来自数据描述符"
def __set__(self, obj, value):
print(f"数据描述符拦截了设置: {value}")
class NonDataDescriptor:
"""非数据描述符 — 只有 __get__"""
def __get__(self, obj, objtype=None):
return "来自非数据描述符"
class MyClass:
data = DataDescriptor()
nondata = NonDataDescriptor()
obj = MyClass()
# 数据描述符优先级高于实例 __dict__
obj.__dict__["data"] = "实例值"
print(obj.data) # "来自数据描述符" — 描述符胜出
# 非数据描述符优先级低于实例 __dict__
obj.__dict__["nondata"] = "实例值"
print(obj.nondata) # "实例值" — 实例字典胜出
完整的属性查找链:
1. 数据描述符(类.__mro__ 中找到的、有 __set__ 或 __delete__ 的描述符)
2. 实例.__dict__
3. 非数据描述符(类.__mro__ 中找到的、只有 __get__ 的描述符)/ 类属性
4. __getattr__(如果定义了)
5. 抛出 AttributeError
14.3 描述符与 property、classmethod、staticmethod 的关系
这三个内置装饰器本质都是描述符:
用纯 Python 实现 property
class MyProperty:
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc or (fget.__doc__ if fget else None)
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):
return type(self)(fget, self.fset, self.fdel)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel)
用纯 Python 实现 staticmethod
class MyStaticMethod:
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
return self.func # 直接返回原函数,不绑定任何东西
用纯 Python 实现 classmethod
class MyClassMethod:
def __init__(self, func):
self.func = func
def __get__(self, obj, objtype=None):
if objtype is None:
objtype = type(obj)
# 返回一个绑定了 cls 的函数
def wrapper(*args, **kwargs):
return self.func(objtype, *args, **kwargs)
return wrapper
函数本身也是描述符
Python 的普通函数实现了 __get__,这就是方法绑定的秘密:
class MyClass:
def method(self):
pass
obj = MyClass()
# 通过类访问 — 得到函数
print(MyClass.__dict__["method"]) # <function MyClass.method at 0x...>
# 通过实例访问 — 函数的 __get__ 返回绑定方法
print(obj.method) # <bound method MyClass.method of <MyClass object>>
# 手动调用函数的 __get__
func = MyClass.__dict__["method"]
bound_method = func.__get__(obj, MyClass)
print(bound_method) # <bound method MyClass.method of <MyClass object>>
14.4 用描述符实现 ORM 字段验证
class Field:
"""ORM 字段基类"""
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage_name, self.default)
def __set__(self, obj, value):
value = self.validate(value)
setattr(obj, self.storage_name, value)
def validate(self, value):
return value # 子类重写
class StringField(Field):
def __init__(self, min_length=0, max_length=None, default=""):
self.min_length = min_length
self.max_length = max_length
self.default = default
def validate(self, value):
if not isinstance(value, str):
raise TypeError(f"{self.name} 必须是字符串")
if len(value) < self.min_length:
raise ValueError(f"{self.name} 长度不能少于 {self.min_length}")
if self.max_length and len(value) > self.max_length:
raise ValueError(f"{self.name} 长度不能超过 {self.max_length}")
return value
class IntField(Field):
def __init__(self, min_value=None, max_value=None, default=0):
self.min_value = min_value
self.max_value = max_value
self.default = default
def validate(self, value):
if not isinstance(value, int):
raise TypeError(f"{self.name} 必须是整数")
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}")
return value
class EmailField(StringField):
def validate(self, value):
value = super().validate(value)
if "@" not in value:
raise ValueError(f"{self.name} 不是有效的邮箱地址")
return value
# 使用
class User:
name = StringField(min_length=1, max_length=50)
age = IntField(min_value=0, max_value=150)
email = EmailField(min_length=5)
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
def __repr__(self):
return f"User(name={self.name!r}, age={self.age}, email={self.email!r})"
user = User("Alice", 30, "alice@example.com")
print(user) # User(name='Alice', age=30, email='alice@example.com')
# 验证生效
try:
user.name = "" # ValueError: name 长度不能少于 1
except ValueError as e:
print(e)
try:
user.age = -1 # ValueError: age 不能小于 0
except ValueError as e:
print(e)
try:
user.email = "invalid" # ValueError: email 不是有效的邮箱地址
except ValueError as e:
print(e)
本章小结:描述符是 Python 属性访问机制的核心,是
property、classmethod、staticmethod和方法绑定的底层实现。理解数据描述符和非数据描述符的优先级差异,就理解了 Python 的属性查找链。描述符在实现 ORM、验证框架等场景中非常强大。