作者: andylin02
学习章节: 第 23 章 属性描述符
关键词: 描述符|描述符协议|__get__|__set__|__delete__|数据描述符|非数据描述符|覆盖性描述符|属性访问优先级|验证描述符|property实现|类装饰器
一、本章概述
描述符(Descriptor)是 Python 中一个强大但常被误解的特性。它允许开发者定义可重用的属性访问逻辑,是许多高级特性(如 @property、@classmethod、@staticmethod 以及 __slots__)的底层实现机制。理解描述符将使你对 Python 的对象模型有更深层次的掌握。
描述符是一个实现了描述符协议(__get__、__set__、__delete__ 中的一个或多个)的类。描述符的实例可以作为其他类的类属性,从而控制对被管理属性的访问。
本章的核心议题包括:
- 描述符协议:__get__、__set__、__delete__ 方法的作用与签名。
- 覆盖性描述符 vs 非覆盖性描述符:数据描述符与非数据描述符的本质区别。
- 属性访问的优先级规则:实例属性、类属性、描述符在查找过程中的先后顺序。
- 描述符的应用:通过 Quantity 验证描述符(来自前一章的 LineItem 示例),以及更通用的验证框架。
- 描述符与 Python 内置特性的关系:@property 是如何用描述符实现的;@classmethod 和 @staticmethod 的描述符本质。
- 描述符与 __slots__:如何利用描述符实现 __slots__ 的内存优化。
- 描述符的继承与组合:避免常见陷阱,编写可复用的描述符类。
前置知识:第 22 章中 property 和动态属性的内容是对描述符的铺垫。描述符提供了比 property 更底层的控制。
二、描述符协议(Descriptor Protocol)
描述符协议包含三个特殊方法。一个类只要实现其中至少一个方法,其实例就是描述符:
| __get__(self, instance, owner) | def __get__(self, instance, owner=None): | 获取属性值时调用。instance 是访问的实例(若通过类访问则为 None),owner 是所属类。返回属性值。 |
| __set__(self, instance, value) | def __set__(self, instance, value): | 设置属性值时调用。 |
| __delete__(self, instance) | def __delete__(self, instance): | 删除属性时调用。 |
如果一个类实现了 __set__ 或 __delete__,它被称为数据描述符(data descriptor) 或覆盖性描述符(overriding descriptor)。如果只实现了 __get__,则称为非数据描述符(non-data descriptor) 或非覆盖性描述符(non-overriding descriptor)。
重要:描述符必须作为类属性定义,而不是实例属性。只有将描述符实例赋值给类的属性,Python 才会自动触发描述符协议。如果把描述符实例赋值给实例属性,它的特殊方法不会被调用。
2.1 最简单的描述符示例
class RevealAccess:
"""一个简单的描述符,在每次属性访问时打印日志"""
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, instance, owner):
print(f"Retrieving {self.name}…")
return self.val
def __set__(self, instance, value):
print(f"Updating {self.name} to {value!r}")
self.val = value
class MyClass:
x = RevealAccess(10, 'var "x"')
y = 5
if __name__ == '__main__':
m = MyClass()
print(m.x) # Retrieving var "x"… \\n 10
m.x = 20 # Updating var "x" to 20
print(m.x) # Retrieving var "x"… \\n 20
print(m.y) # 5(普通类属性)
此例完整展示了描述符的 __get__ 和 __set__ 的工作过程。描述符实例 x 作为 MyClass 的类属性,控制了对 m.x 的读写。
三、覆盖性描述符与非覆盖性描述符
3.1 覆盖性描述符(数据描述符)
实现 __set__ 或 __delete__ 的描述符称为覆盖性描述符。即使实例字典中有同名属性,访问时也会优先使用描述符的 __get__ 和 __set__。
class Overriding:
"""覆盖性描述符:实现了 __set__ 和 __get__"""
def __get__(self, instance, owner):
print("Overriding __get__")
def __set__(self, instance, value):
print(f"Overriding __set__ with {value!r}")
class Managed:
attr = Overriding()
obj = Managed()
obj.attr = 100 # 输出:Overriding __set__ with 100
obj.attr # 输出:Overriding __get__
obj.__dict__['attr'] = 99 # 直接赋值给实例字典会被忽略?不,实例字典中会有 'attr': 99
print(vars(obj)) # {'attr': 99}
obj.attr # 仍然输出 Overriding __get__(描述符仍拦截)
# 注意:实例字典中的 attr 被隐藏了,因为描述符优先。
3.2 非覆盖性描述符
只实现 __get__ 的描述符。如果实例字典中有同名属性,则实例属性会覆盖描述符;否则描述符生效。
class NonOverriding:
"""非覆盖性描述符:只实现 __get__"""
def __get__(self, instance, owner):
print("NonOverriding __get__")
class Managed:
attr = NonOverriding()
obj = Managed()
obj.attr # 输出:NonOverriding __get__
obj.attr = 100 # 设置实例属性
obj.attr # 输出:100(实例属性屏蔽了描述符)
del obj.attr # 删除实例属性
obj.attr # 再次输出:NonOverriding __get__
3.3 仅实现 __set__ 的描述符
这种行为比较特殊,如果只实现 __set__ 而不实现 __get__,那么读取属性会回到默认的实例属性查找,但赋值被描述符拦截。这可以用于设置时验证,但取值时允许实例动态赋值。
class SetOnly:
def __set__(self, instance, value):
print(f"SetOnly setting {value!r}")
instance.__dict__['_setonly'] = value # 手动存到实例字典
class Managed:
attr = SetOnly()
obj = Managed()
obj.attr = 42 # 输出:SetOnly setting 42
print(obj.attr) # AttributeError: 'Managed' object has no attribute 'attr' —— 因为没实现 __get__
# 实际上,可以通过 instance.__dict__['_setonly'] 访问,但这不是常规方式。
3.4 描述符访问优先级规则图
#mermaid-svg-5vO91m6tCrfI1MFE{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5vO91m6tCrfI1MFE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5vO91m6tCrfI1MFE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5vO91m6tCrfI1MFE .error-icon{fill:#552222;}#mermaid-svg-5vO91m6tCrfI1MFE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5vO91m6tCrfI1MFE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5vO91m6tCrfI1MFE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5vO91m6tCrfI1MFE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5vO91m6tCrfI1MFE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5vO91m6tCrfI1MFE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5vO91m6tCrfI1MFE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5vO91m6tCrfI1MFE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5vO91m6tCrfI1MFE .marker.cross{stroke:#333333;}#mermaid-svg-5vO91m6tCrfI1MFE svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5vO91m6tCrfI1MFE p{margin:0;}#mermaid-svg-5vO91m6tCrfI1MFE .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-5vO91m6tCrfI1MFE .cluster-label text{fill:#333;}#mermaid-svg-5vO91m6tCrfI1MFE .cluster-label span{color:#333;}#mermaid-svg-5vO91m6tCrfI1MFE .cluster-label span p{background-color:transparent;}#mermaid-svg-5vO91m6tCrfI1MFE .label text,#mermaid-svg-5vO91m6tCrfI1MFE span{fill:#333;color:#333;}#mermaid-svg-5vO91m6tCrfI1MFE .node rect,#mermaid-svg-5vO91m6tCrfI1MFE .node circle,#mermaid-svg-5vO91m6tCrfI1MFE .node ellipse,#mermaid-svg-5vO91m6tCrfI1MFE .node polygon,#mermaid-svg-5vO91m6tCrfI1MFE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5vO91m6tCrfI1MFE .rough-node .label text,#mermaid-svg-5vO91m6tCrfI1MFE .node .label text,#mermaid-svg-5vO91m6tCrfI1MFE .image-shape .label,#mermaid-svg-5vO91m6tCrfI1MFE .icon-shape .label{text-anchor:middle;}#mermaid-svg-5vO91m6tCrfI1MFE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5vO91m6tCrfI1MFE .rough-node .label,#mermaid-svg-5vO91m6tCrfI1MFE .node .label,#mermaid-svg-5vO91m6tCrfI1MFE .image-shape .label,#mermaid-svg-5vO91m6tCrfI1MFE .icon-shape .label{text-align:center;}#mermaid-svg-5vO91m6tCrfI1MFE .node.clickable{cursor:pointer;}#mermaid-svg-5vO91m6tCrfI1MFE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5vO91m6tCrfI1MFE .arrowheadPath{fill:#333333;}#mermaid-svg-5vO91m6tCrfI1MFE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5vO91m6tCrfI1MFE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5vO91m6tCrfI1MFE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5vO91m6tCrfI1MFE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5vO91m6tCrfI1MFE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5vO91m6tCrfI1MFE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5vO91m6tCrfI1MFE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5vO91m6tCrfI1MFE .cluster text{fill:#333;}#mermaid-svg-5vO91m6tCrfI1MFE .cluster span{color:#333;}#mermaid-svg-5vO91m6tCrfI1MFE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5vO91m6tCrfI1MFE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5vO91m6tCrfI1MFE rect.text{fill:none;stroke-width:0;}#mermaid-svg-5vO91m6tCrfI1MFE .icon-shape,#mermaid-svg-5vO91m6tCrfI1MFE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5vO91m6tCrfI1MFE .icon-shape p,#mermaid-svg-5vO91m6tCrfI1MFE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5vO91m6tCrfI1MFE .icon-shape .label rect,#mermaid-svg-5vO91m6tCrfI1MFE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5vO91m6tCrfI1MFE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5vO91m6tCrfI1MFE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5vO91m6tCrfI1MFE :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
是
否
是
否
是
否
获取 obj.attr
在 obj 的类或父类中有名为 attr 的数据描述符?
调用数据描述符的 __get__
obj.__dict__ 中有 'attr'?
返回 obj.__dict__['attr']
在类或父类中有名为 attr 的非数据描述符?
调用非数据描述符的 __get__
在类或父类中查找普通属性
找到属性?
返回普通属性值
抛出 AttributeError
优先级总结(从高到低):
四、描述符的应用:数据验证(Quantity 示例)
第 22 章中我们用特性工厂实现了验证。现在用描述符重构,使验证逻辑可复用:定义一个 Quantity 描述符类。
4.1 基础验证描述符
class Quantity:
def __init__(self, storage_name):
self.storage_name = storage_name # 存储位置(实例字典的键)
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError(f'{self.storage_name} must be > 0')
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.storage_name)
class LineItem:
weight = Quantity('weight')
price = Quantity('price')
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
item = LineItem('apple', 1.5, 10.0)
print(item.subtotal()) # 15.0
# item.weight = -1 # ValueError: weight must be > 0
注意:描述符必须将值存储在实例的 __dict__ 中,键名称为 storage_name。如果描述符直接使用 self.val 存储,则所有实例会共享同一个值(因为描述符是类属性)。
4.2 使用描述符的 __set_name__(Python 3.6+)
Python 3.6 中增加了 __set_name__ 钩子,它会在类创建时自动调用,允许描述符获取被赋值的属性名。这样就不需要手动传递 storage_name 了。
class Quantity:
def __set_name__(self, owner, name):
# owner 是所属类,name 是类属性名(如 weight)
self.storage_name = name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.storage_name] = value
else:
raise ValueError(f'{self.storage_name} must be > 0')
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.storage_name)
class LineItem:
weight = Quantity()
price = Quantity()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
__set_name__ 让描述符的使用更加简洁,这也是现代 Python 中定义描述符的推荐方式。
4.3 更通用的验证描述符
可以通过继承 Quantity 并覆盖验证逻辑来创建多种验证器:
class Validated:
def __set_name__(self, owner, name):
self.storage_name = name
def __set__(self, instance, value):
self.validate(value)
instance.__dict__[self.storage_name] = value
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.storage_name)
def validate(self, value):
"""子类应实现此方法"""
pass
class NonNegative(Validated):
def validate(self, value):
if value < 0:
raise ValueError(f'{self.storage_name} must be non-negative')
class Positive(Validated):
def validate(self, value):
if value <= 0:
raise ValueError(f'{self.storage_name} must be positive')
这样我们可以为不同的属性配置不同的验证逻辑,且代码高度复用。
五、描述符在内置 Python 特性中的角色
5.1 @property 的实现
property 是一个内置的类,它实现了描述符协议。当我们在类中定义 @property 装饰的方法时,实际上创建了一个 property 实例,它作为类属性,并通过描述符机制拦截访问。
# 模拟 property 的简化实现
class Property:
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, instance, owner):
if instance is None:
return self
if self.fget is None:
raise AttributeError('unreadable attribute')
return self.fget(instance)
def __set__(self, instance, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(instance, value)
def setter(self, fset):
self.fset = fset
return self
5.2 @classmethod 和 @staticmethod
classmethod 和 staticmethod 也是通过描述符实现的。
- classmethod:在 __get__ 中返回一个绑定了类的方法(而不是实例)。
- staticmethod:在 __get__ 中直接返回原始函数,不进行额外的绑定。
# 模拟 staticmethod 描述符
class StaticMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
return self.func
# 模拟 classmethod 描述符
class ClassMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
return self.func.__get__(owner, type(owner))
5.3 描述符与 __slots__
__slots__ 内部也使用了描述符。当定义 __slots__ 类的属性时,Python 会为每个槽位创建描述符,用于管理从实例的固定数组(而不是字典)中获取和设置数值。
class WithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# 实际上,x 和 y 是两个描述符,它们从 C 级数组读取数据。
六、描述符的继承与组合
6.1 描述符的继承
如果父类定义了描述符,子类会继承该描述符。子类可以覆盖描述符(在子类中重新赋值类属性),或者通过 __set_name__ 动态修改行为。
6.2 避免描述符中的共享状态
如果在描述符的 __init__ 中直接将值存储在 self.val,所有实例将共享该值。除非明确意图,否则应该将值存储在实例的 __dict__ 中。
# 错误示例:所有实例共享同一个值
class BadDescriptor:
def __init__(self):
self.val = None
def __get__(self, instance, owner):
return self.val
def __set__(self, instance, value):
self.val = value
class Test:
x = BadDescriptor()
t1 = Test()
t2 = Test()
t1.x = 10
print(t2.x) # 10 —— 不是期望的行为!
正确做法是使用 instance.__dict__ 存储(如 Quantity 示例)。
6.3 描述符与类装饰器
描述符常与类装饰器组合使用,以简化类的创建或增强功能。
七、综合案例:类型检查与验证描述符
以下实现一个支持类型检查(int, float, str 等)和值范围验证的描述符。
import numbers
class TypedProperty:
def __init__(self, expected_type, validator=None):
self.expected_type = expected_type
self.validator = validator # 可调用,用于额外验证
self.storage_name = None
def __set_name__(self, owner, name):
self.storage_name = f'_{name}'
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.storage_name, None)
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f'Expected {self.expected_type.__name__}, got {type(value).__name__}')
if self.validator and not self.validator(value):
raise ValueError(f'Value {value!r} failed validation')
setattr(instance, self.storage_name, value)
class Person:
name = TypedProperty(str, lambda s: len(s) > 0)
age = TypedProperty(int, lambda a: 0 <= a <= 150)
def __init__(self, name, age):
self.name = name
self.age = age
p = Person("Alice", 30)
print(p.name, p.age) # Alice 30
# p = Person("", -5) # 会触发验证错误
八、本章思维导图
#mermaid-svg-ZbMxZTtnZ4ymKzpQ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .error-icon{fill:#552222;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .marker.cross{stroke:#333333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ p{margin:0;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .cluster-label text{fill:#333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .cluster-label span{color:#333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .cluster-label span p{background-color:transparent;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .label text,#mermaid-svg-ZbMxZTtnZ4ymKzpQ span{fill:#333;color:#333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node rect,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node circle,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node ellipse,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node polygon,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .rough-node .label text,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node .label text,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .image-shape .label,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .rough-node .label,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node .label,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .image-shape .label,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .icon-shape .label{text-align:center;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node.clickable{cursor:pointer;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .arrowheadPath{fill:#333333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .cluster text{fill:#333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .cluster span{color:#333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .icon-shape,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .icon-shape p,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .icon-shape .label rect,#mermaid-svg-ZbMxZTtnZ4ymKzpQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZbMxZTtnZ4ymKzpQ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
第23章 属性描述符
描述符协议
__get__
__set__
__delete__
描述符类型
数据描述符覆盖性:有__set__或__delete__
非数据描述符非覆盖性:仅__get__
访问优先级
1. 数据描述符
2. 实例字典
3. 非数据描述符
4. 类属性
用法示例
验证描述符:Quantity
__set_name__ 简化
继承与组合
内置实现
@property
@classmethod
__slots__
常见陷阱
描述符实例共享状态
忘记 __set_name__ 导致键名冲突
九、总结
第 23 章深入讲解了描述符这一高级工具,它是 Python 元编程的重要基石。核心收获如下:
- 描述符协议:理解 __get__、__set__ 和 __delete__ 的用法。描述符必须作为类属性存在。
- 覆盖性 vs 非覆盖性:数据描述符优先于实例属性,而非数据描述符会被实例属性覆盖。掌握这一点对于编写描述符至关重要。
- __set_name__ 钩子(Python 3.6+):让描述符自动获取管理属性名,简化代码。
- 实战应用:数据验证(如 Quantity、Positive)、类型检查、属性委托。
- Python 内部机制:@property、@classmethod、@staticmethod 和 __slots__ 都基于描述符实现。
- 注意事项:避免在描述符中直接存储值导致共享状态;留意继承行为;正确使用 instance is None 来支持类级别访问。
掌握描述符之后,你不仅能够阅读和调试框架级别的代码,还能设计出灵活、可重用、且易于维护的属性管理系统。
十、思考题
如果描述符同时实现了 __get__ 和 __set__,但 __set__ 中把值存入了 instance.__dict__,而 __get__ 也从 instance.__dict__ 读取。那么当实例字典中有同名属性时,obj.attr 会返回描述符计算的值还是实例字典中的值?为什么?
编写一个 LazyProperty 描述符,第一次访问时调用一个指定的函数计算值,然后将值缓存到实例字典中,后续访问直接返回缓存值。与 @cached_property 类似。
如果父类和子类都定义了相同名称的描述符,Python 会按什么顺序解析?请编写代码验证。
如何让描述符支持在类级别访问(MyClass.attr)时返回描述符本身而不是 None?在你的 Quantity 描述符中修改 __get__ 来实现。
阅读 functools.cached_property 的源代码,它本质上是一个非覆盖性描述符。思考为什么它没有实现 __set__?
十一、下一章预告
第 24 章《类元编程》
描述符是元编程的入门砖,而第 24 章将深入探讨类元编程的终极武器:元类(metaclass)和类装饰器。
- 类创建机制:Python 如何通过 type 构造类,__new__ 与 __init__ 的顺序。
- 元类基础:自定义元类,通过继承 type 来控制类的创建过程。
- 类装饰器 vs 元类:何时使用类装饰器,何时需要元类。
- 实战案例:使用元类实现 ORM 中的模型声明(如 Django 或 SQLAlchemy 的风格)。
- __init_subclass__:Python 3.6 中引入的轻量级替代元类的方法。
学习元编程后,你将能够编写出优雅的 DSL 和框架级的代码,真正掌握 Python 的终极控制力。
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

