Python 描述符与属性访问
描述符协议驱动 property/方法绑定与属性校验;理解 get/set/delete 与查找顺序。
#concept
#growing
#dev
#python
#python
[!info] related notes
Python 描述符与属性访问
这个概念为什么出现
@property、方法绑定、ORM 字段——底层多是描述符。不懂描述符,属性“读的是类上的函数却变成方法”会像魔法。
[!abstract] 一句话理解 描述符是实现
__get__/__set__/__delete__的对象;放在类上时介入属性访问,property 是其应用。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:单价必须非负的属性校验
# price_descriptor_demo.py
# 业务意图:用 property 校验单价。
# 教学点:描述符应用;读写分离。
class LineItem:
def __init__(self, sku: str, unit_price_cents: int) -> None:
self.sku = sku
self.unit_price_cents = unit_price_cents
@property
def unit_price_cents(self) -> int:
return self._unit_price_cents
@unit_price_cents.setter
def unit_price_cents(self, value: int) -> None:
if value < 0:
raise ValueError("price must be >= 0")
self._unit_price_cents = value
def main() -> None:
item = LineItem("SKU-1", 100)
print(item.unit_price_cents)
try:
item.unit_price_cents = -1
except ValueError as exc:
print("blocked:", exc)
if __name__ == "__main__":
main()
建议运行:
python price_descriptor_demo.py
期望输出:
100
blocked: price must be >= 0
结合场景再看三个关注点
- 赋值走 setter。
- 存储用
_unit_price_cents避免递归。 - 函数放在类上经描述符变成绑定方法。
核心概念与准确模型
- 描述符协议由描述符对象的类型实现;类型定义
__set__或__delete__时称为数据描述符,仅定义__get__时为非数据描述符。 - 普通实例读取先按 MRO 找类上的首个同名属性:有
__get__的数据描述符优先,其后是实例 dict,再调用非数据描述符或返回类属性。查找失败时点号访问可调用__getattr__。 property/classmethod/staticmethod/functions
边界情况与反直觉行为
- 只定义
__get__的非数据描述符可被实例 dict 遮蔽 - 重写
__getattribute__可改变查找/描述符行为;需要默认行为时委托object.__getattribute__,直接读self.x容易递归。 - 只读 property 也有拒绝写入的
__set__,仍优先于同名实例 dict 项。slots 通常也由类上的数据描述符访问。 - 本例只检查负值;类型注解不阻止 float、bool 或 NaN。写
_unit_price_cents也可绕过 setter;property 不是安全隔离或完整输入验证。
常见误区
[!warning] 常见误区:为一切字段写 property 错误理解:封装必须样板。
正确模型:有不变量再引入。
工程实践
- 公开不变量用 property/描述符
- 框架作者才常写通用描述符
- 文档化副作用
本节总结
描述符是属性系统的机制核心。日常用 property;读库时能认出协议。
自测题
- property 是什么?
- 为何 setter 写入
_x?
参考答案
- 描述符的便捷封装。
- 避免再次触发 setter 造成递归。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| Descriptor HowTo | HOWTO | 权威 |