← 返回Python进阶编程目录
← 返回学习笔记首页
专题: Python进阶编程系统学习
关键词: Python, @property, 描述符, __get__, __set__, 验证器, property源码, 描述符协议
一、概述
在Python面向对象编程中,属性访问是最基础也最频繁的操作之一。当我们写 obj.attr 时,Python解释器背后经历了一系列精密的查找和调用过程。@property 装饰器和描述符协议(Descriptor Protocol)是Python中控制属性访问的两大核心机制,它们共同构成了Python属性管理的基础设施。
@property 装饰器允许我们将一个方法"伪装"成属性来使用,使得外部代码可以通过 obj.attr 的形式访问经过计算或验证的值,同时内部仍然保持方法的灵活性和控制力。而描述符协议则是一种更加底层和通用的机制,它定义了 __get__ 、__set__ 和 __delete__ 三个特殊方法,任何实现了这些方法的类都可以成为描述符,从而接管对属性的访问控制。
理解这两个概念不仅可以让我们写出更加Pythonic的代码,还能深入理解Python语言本身的设计哲学。事实上,@property 本身就是描述符协议的一个经典实现——property类是一个内置的描述符。本文将依次深入探讨这些机制。
前置知识: 阅读本文需要熟悉Python基础语法、类和对象的概念、装饰器的基本用法。如果对这些基础知识尚不熟悉,建议先回顾相关内容。
二、@property装饰器基础
2.1 为什么需要@property
在传统的面向对象语言(如Java)中,我们通常通过定义getter和setter方法来控制属性访问。Python中当然也可以这样做,但代码会显得冗长且不够优雅:
传统getter/setter(不推荐)
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
def get_name(self):
return self._name
def set_name(self, value):
if not isinstance(value, str):
raise TypeError("name must be str")
self._name = value
def get_age(self):
return self._age
def set_age(self, value):
if not 0 < value < 150:
raise ValueError("invalid age")
self._age = value
p = Person("Alice", 30)
p.set_age(p.get_age() + 1)
print(p.get_name())
使用@property(推荐)
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError("name must be str")
self._name = value
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not 0 < value < 150:
raise ValueError("invalid age")
self._age = value
p = Person("Alice", 30)
p.age += 1
print(p.name)
使用 @property 后,外部代码无需关心属性背后是否有验证逻辑或计算过程,接口保持统一。这在重构时尤为有价值——你可以先将属性公开,后续需要验证时再改为property,而调用方代码无需任何修改。
2.2 @property的完整用法
@property 实际上创建了一个property对象,该对象提供了三个方法:getter 、setter 和 deleter ,分别对应属性的读取、设置和删除操作:
class Temperature :
def __init__ (self, celsius=0):
self._celsius = celsius
@property
def celsius (self):
"""返回摄氏温度"""
return self._celsius
@celsius.setter
def celsius (self, value):
if value < -273.15:
raise ValueError ("温度不能低于绝对零度" )
self._celsius = value
@celsius.deleter
def celsius (self):
print ("删除温度属性" )
del self._celsius
@property
def fahrenheit (self):
"""返回华氏温度(只读属性)"""
return self._celsius * 9 / 5 + 32
# 使用示例
t = Temperature(100 )
print (t.celsius) # 100
print (t.fahrenheit) # 212.0
t.celsius = 0
print (t.fahrenheit) # 32.0
# t.fahrenheit = 50 # AttributeError: can't set attribute
del t.celsius # 删除温度属性
上述代码展示了property的三种典型用法:只读属性(fahrenheit )、读写属性(celsius 带setter)、可删除属性(celsius 带deleter)。特别注意,fahrenheit 没有定义setter,因此它是只读的,任何赋值操作都会引发 AttributeError 。
命名约定: Python中通常使用单下划线前缀(如 _celsius )表示"保护"属性,双下划线前缀(如 __celsius )触发名称改写(name mangling)。property方法名通常是对外公开的属性名,而真正的存储属性使用下划线前缀。这只是一个约定,并非强制。
2.3 使用property实现计算属性
property最常见的用途之一是实现计算属性(computed property),即属性值由其他数据动态计算得出:
class Rectangle :
def __init__ (self, width, height):
self.width = width
self.height = height
@property
def area (self):
return self.width * self.height
@property
def perimeter (self):
return 2 * (self.width + self.height)
r = Rectangle(3 , 4 )
print (r.area) # 12
print (r.perimeter) # 14
r.width = 5
print (r.area) # 20(自动更新)
计算属性带来的一个关键好处是"自动同步"——当依赖的数据发生变化时,计算属性会自动反映最新的值,无需手动调用更新方法。这使得代码更加健壮且不易出错。
三、描述符协议
3.1 什么是描述符
描述符(Descriptor)是Python中一个强大的协议,它允许我们自定义属性访问的行为。一个类只要实现了 __get__ 、__set__ 或 __delete__ 方法中的任意一个,就被称为描述符。具体来说:
数据描述符(Data Descriptor): 同时实现了 __get__ 和 __set__ (或 __delete__ )
非数据描述符(Non-data Descriptor): 只实现了 __get__
描述符协议是Python属性查找机制的核心,许多内置功能都建立在描述符之上,包括 @property 、类方法 、静态方法 、super() 等。
3.2 描述符协议方法详解
描述符协议由三个特殊方法组成:
# 描述符协议方法签名
def __get__ (self, obj, objtype=None ):
"""获取属性值
self: 描述符实例
obj: 所属类的实例(通过实例访问时),None(通过类访问时)
objtype: 所属类
"""
pass
def __set__ (self, obj, value):
"""设置属性值
self: 描述符实例
obj: 所属类的实例
value: 要设置的值
"""
pass
def __delete__ (self, obj):
"""删除属性
self: 描述符实例
obj: 所属类的实例
"""
pass
当我们通过实例访问属性时,Python会检查该属性是否是描述符(即类中是否定义了描述符协议方法),如果是,则调用描述符的相应方法而不是直接返回属性值。
3.3 一个简单的描述符示例
class PositiveNumber :
"""描述符:确保属性值始终为正数"""
def __init__ (self, name):
self.name = name
def __get__ (self, obj, objtype=None ):
if obj is None :
return self
return obj.__dict__.get(self.name)
def __set__ (self, obj, value):
if value <= 0 :
raise ValueError (f"{self.name} 必须为正数" )
obj.__dict__[self.name] = value
def __delete__ (self, obj):
del obj.__dict__[self.name]
class Order :
price = PositiveNumber("price" )
quantity = PositiveNumber("quantity" )
def __init__ (self, price, quantity):
self.price = price
self.quantity = quantity
@property
def total (self):
return self.price * self.quantity
# 使用示例
order = Order(100 , 5 )
print (order.total) # 500
# order.price = -10 # ValueError: price 必须为正数
print (order.price) # 100
关键理解: 描述符的核心在于将属性访问逻辑封装到独立的类中,实现"关注点分离"。在上述例子中,PositiveNumber 描述符负责验证逻辑,而 Order 类只需声明使用该描述符。这与Java中的注解处理器、C#中的属性有异曲同工之妙。
四、数据描述符与非数据描述符
4.1 核心区别
数据描述符和非数据描述符的关键区别在于属性查找时的优先级顺序。Python的属性查找遵循以下规则:
obj.attr 查找顺序:
数据描述符(类中定义)
↓ (未找到 ↑)
实例 __dict__
↓ (未找到 ↑)
非数据描述符 / 类属性
具体来说:
首先检查类及其父类中是否存在该名称的 数据描述符 ,如果有,优先调用描述符的 __get__ 或 __set__
如果没有数据描述符,则检查实例的 __dict__ 中是否有该属性
如果实例字典中也没有,再查找类及其父类中的 非数据描述符 或普通类属性
4.2 优先级对比实验
# 非数据描述符
class NonDataDescriptor :
def __init__ (self, name):
self.name = name
def __get__ (self, obj, objtype=None ):
print (f"NonDataDescriptor.__get__: {self.name}" )
return "desc_value"
# 数据描述符
class DataDescriptor :
def __init__ (self, name):
self.name = name
def __get__ (self, obj, objtype=None ):
print (f"DataDescriptor.__get__: {self.name}" )
return "desc_value"
def __set__ (self, obj, value):
print (f"DataDescriptor.__set__: {self.name} = {value}" )
obj.__dict__[self.name] = value
class Demo :
nd = NonDataDescriptor("nd" )
dd = DataDescriptor("dd" )
d = Demo()
d.__dict__["nd" ] = "instance_value"
d.__dict__["dd" ] = "instance_value"
print ("访问 nd (非数据描述符):" )
print (d.nd) # 实例字典优先,输出: instance_value
print ("\n访问 dd (数据描述符):" )
print (d.dd) # 描述符优先,输出: desc_value
重要提示: 非数据描述符会被实例属性"遮盖"(shadowed),而数据描述符会"遮盖"实例属性。这个区别在实际开发中非常重要——@property 是数据描述符,因此即使实例的 __dict__ 中同名字段也不会绕过property的逻辑;而普通的函数/方法是非数据描述符,这就是为什么实例方法可以被实例属性覆盖(虽然通常不应该这样做)。
4.3 方法也是描述符
值得一提的是,Python中的函数(function)是非数据描述符。当我们通过实例调用方法时,正是描述符协议在幕后工作:
class MyClass :
def method (self):
return "hello"
# 函数是非数据描述符
print (hasattr (MyClass.method, '__get__' )) # True
print (hasattr (MyClass.method, '__set__' )) # False
obj = MyClass()
# obj.method 实际调用的是: type(obj).method.__get__(obj, type(obj))
# 这样就实现了自动绑定 self 参数
print (obj.method()) # hello
每当我们通过实例调用方法时,Python的 __get__ 方法会将函数与实例绑定,生成一个"绑定方法"(bound method),自动将实例作为第一个参数(self )传入。这正是Python方法调用的核心机制。
五、@property的底层实现原理
5.1 property的本质
@property 实际上是一个内置的类(不是普通的装饰器函数)。当我们写 @property 时,实际上创建了一个 property 类的实例。这个类本身实现了描述符协议,是一个数据描述符:
print (type (property )) #
print (issubclass (property , object )) # True
# property 类在 CPython 中的简化版实现
class PropertySimplified :
"""演示 property 的核心工作机制"""
def __init__ (self, fget=None , fset=None , fdel=None , doc=None ):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None :
doc = fget.__doc__
self.__doc__ = doc
def __get__ (self, obj, objtype=None ):
if obj is None :
return self
if self.fget is None :
raise AttributeError ("unreadable attribute" )
return self.fget(obj)
def __set__ (self, obj, value):
if self.fset is None :
raise AttributeError ("can't set attribute" )
self.fset(obj, value)
def __delete__ (self, obj):
if self.fdel is None :
raise AttributeError ("can't delete attribute" )
self.fdel(obj)
# getter/setter/deleter 方法返回新的 property 实例
def getter (self, fget):
return type (self)(fget, self.fset, self.fdel, self.__doc__)
def setter (self, fset):
return type (self)(self.fget, fset, self.fdel, self.__doc__)
def deleter (self, fdel):
return type (self)(self.fget, self.fset, fdel, self.__doc__)
当我们使用 @property 装饰一个方法时:
# 使用 @property 语法糖
class Circle :
def __init__ (self, radius):
self._radius = radius
@property
def radius (self):
"""返回圆的半径"""
return self._radius
# 等价于:
class Circle :
def __init__ (self, radius):
self._radius = radius
def radius (self):
"""返回圆的半径"""
return self._radius
radius = property (radius) # 构建 property 实例
而 @radius.setter 同样如此——它调用 property 实例的 setter 方法,返回一个新的 property 实例(包含了原有的fget和新的fset),然后用这个新实例替换同名的类属性。
深入理解: 每次调用 getter() 、setter() 、deleter() 都会创建一个新的 property 对象。这保证了后续的装饰操作不会影响之前已经定义的 property。property 是数据描述符,这意味着它的 __get__ 和 __set__ 优先级高于实例字典,从而确保属性访问始终经过 getter/setter 逻辑。
六、自定义描述符实现验证器
6.1 类型验证描述符
描述符最常见的实际应用之一是数据验证。通过自定义描述符,我们可以创建可复用的验证逻辑:
class TypedAttribute :
"""描述符:验证属性类型"""
def __init__ (self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __set_name__ (self, owner, name):
"""Python 3.6+ 支持,自动获取属性名"""
self.name = name
def __get__ (self, obj, objtype=None ):
if obj is None :
return self
return obj.__dict__.get(self.name)
def __set__ (self, obj, value):
if not isinstance (value, self.expected_type):
raise TypeError (
f"期望 {self.expected_type.__name__} 类型,"
f"但收到 {type(value).__name__}"
)
obj.__dict__[self.name] = value
class RangeValidator :
"""描述符:验证数值范围"""
def __init__ (self, min_val, max_val):
self.min_val = min_val
self.max_val = max_val
def __set_name__ (self, owner, name):
self.name = name
def __get__ (self, obj, objtype=None ):
if obj is None :
return self
return obj.__dict__.get(self.name)
def __set__ (self, obj, value):
if not self.min_val <= value <= self.max_val:
raise ValueError (
f"{self.name} 必须在 [{self.min_val}, {self.max_val}] 范围内"
)
obj.__dict__[self.name] = value
class NotEmptyString :
"""描述符:验证非空字符串"""
def __set_name__ (self, owner, name):
self.name = name
def __get__ (self, obj, objtype=None ):
if obj is None :
return self
return obj.__dict__.get(self.name)
def __set__ (self, obj, value):
if not isinstance (value, str ) or not value.strip():
raise ValueError (f"{self.name} 不能为空" )
obj.__dict__[self.name] = value
class Product :
name = NotEmptyString()
price = RangeValidator(0.01 , 1000000 )
quantity = RangeValidator(0 , 99999 )
def __init__ (self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
# 使用示例
p = Product("Python编程" , 59.9 , 100 )
print (p.name, p.price, p.quantity)
# p.price = -1 # ValueError
# p.name = "" # ValueError
# p.quantity = 100000 # ValueError
设计模式视角: 使用描述符实现验证器是一种"组合优于继承"的设计实践。相比于创建庞大的基类并在子类中覆写验证方法,描述符允许我们将验证逻辑封装为独立的、可组合的单元。这大大提高了代码的复用性和可测试性。
6.2 延迟计算描述符(Lazy Property)
描述符的另一经典应用是实现延迟计算属性——仅在首次访问时计算结果并缓存:
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 DataAnalyzer :
def __init__ (self, data):
self.data = data
@LazyProperty
def processed_data (self):
"""大量数据处理,仅在首次访问时执行"""
print ("执行耗时数据处理..." )
return [x * 2 for x in self.data if x > 0 ]
@LazyProperty
def statistics (self):
"""统计数据,仅在首次访问时计算"""
print ("计算统计数据..." )
processed = self.processed_data
return {
"sum" : sum (processed),
"avg" : sum (processed) / len (processed),
"max" : max (processed),
"count" : len (processed),
}
da = DataAnalyzer([1 , -2 , 3 , 0 , 5 ])
print (da.processed_data) # 首次触发计算
print (da.processed_data) # 直接返回缓存结果,不会再次计算
print (da.statistics) # 首次触发计算
print (da.statistics) # 返回缓存
延迟计算描述符巧妙地利用了非数据描述符的特性:第一次访问时触发计算并将结果存入实例字典,后续访问时实例字典会"遮盖"非数据描述符,从而直接返回缓存值。这种模式在数据处理、配置文件解析、数据库查询等场景中非常实用。
性能提示: Python 3.9+ 提供了 functools.cached_property ,其功能与上述 LazyProperty 类似但更加完善(包括线程安全考虑)。在 Python 3.12+ 中,推荐优先使用 functools.cached_property 或 @functools.cache 。
七、描述符在ORM中的应用
7.1 模拟字段定义
描述符在ORM(对象关系映射)框架中有着广泛的应用。SQLAlchemy、Django ORM 等都使用类似机制来定义模型字段。下面展示一个简化的ORM字段实现:
class Field :
"""基数字段描述符"""
def __init__ (self, column_type, default=None , nullable=True ):
self.column_type = column_type
self.default = default
self.nullable = nullable
self.name = ""
def __set_name__ (self, owner, name):
self.name = name
def __get__ (self, obj, objtype=None ):
if obj is None :
return self
value = obj.__dict__.get(self.name, self.default)
if value is None and not self.nullable:
raise ValueError (f"字段 {self.name} 不能为空" )
return value
def __set__ (self, obj, value):
if not isinstance (value, self.column_type):
try :
value = self.column_type(value)
except (TypeError , ValueError ):
raise TypeError (
f"字段 {self.name} 期望类型 {self.column_type.__name__}"
)
obj.__dict__[self.name] = value
class IntegerField (Field):
def __init__ (self, default=0 , nullable=False ):
super ().__init__(int , default=default, nullable=nullable)
class StringField (Field):
def __init__ (self, max_length=255 , default="" , nullable=False ):
super ().__init__(str , default=default, nullable=nullable)
self.max_length = max_length
def __set__ (self, obj, value):
super ().__set__(obj, value)
if len (value) > self.max_length:
raise ValueError (
f"字段 {self.name} 长度不能超过 {self.max_length}"
)
class FloatField (Field):
def __init__ (self, default=0.0 , nullable=False ):
super ().__init__(float , default=default, nullable=nullable)
class ModelMeta (type ):
"""元类:自动收集字段信息,模拟ORM行为"""
def __new__ (mcs, name, bases, namespace):
cls = super ().__new__(mcs, name, bases, namespace)
fields = {}
for key, value in namespace.items():
if isinstance (value, Field):
fields[key] = value
cls._fields = fields
cls._table_name = name.lower()
return cls
class Model (metaclass =ModelMeta):
"""所有模型的基类"""
def save (self):
"""模拟保存到数据库"""
columns = []
values = []
for name, field in self._fields.items():
columns.append(name)
values.append(repr (getattr (self, name)))
sql = f"INSERT INTO {self._table_name} ({', '.join(columns)}) "
f"VALUES ({', '.join(values)})"
print (f"[SQL]: {sql}" )
# 使用自定义ORM
class User (Model):
name = StringField(max_length=50 )
age = IntegerField()
score = FloatField()
def __init__ (self, name, age, score):
self.name = name
self.age = age
self.score = score
user = User("张三" , 25 , 95.5 )
print (user.name, user.age, user.score) # 张三 25 95.5
user.save()
# 输出: [SQL]: INSERT INTO user (name, age, score) VALUES ('张三', 25, 95.5)
知识延伸: 上述实现结合了描述符和元类两种Python高级特性。描述符负责字段级别的类型校验和值存取,元类则负责在类创建时自动收集字段信息。这正是SQLAlchemy和Django ORM的简化版思想。在真实的ORM框架中,还会涉及延迟加载、会话管理、关系映射等更复杂的机制。
八、描述符 vs @property装饰器对比
了解了描述符和 @property 之后,我们有必要从多个维度对两者进行对比,以便在实际开发中做出合适的选择:
对比维度
@property
自定义描述符
复用性
低——每个属性需要重复编写类似逻辑
高——描述符类可在多个类、多个属性间复用
代码量
少——单个属性定义简洁直观
多——需要额外创建描述符类,但长期维护更高效
灵活性
中等——适合简单的getter/setter需求
高——可封装任意复杂的属性访问逻辑
学习曲线
低——基础Python即可掌握
高——需要理解描述符协议和属性查找机制
适用场景
单个类中少量属性的快速验证/计算
多模型共享的验证逻辑、ORM字段、懒加载属性
类型信息
不直接支持类型注解关联
可在描述符类中集成类型系统
调试难度
低——堆栈跟踪直观
中等——跨描述符类的追踪稍复杂
选择指南:
1. 如果只有一两个属性需要getter/setter逻辑,并且逻辑很简单——使用 @property 。
2. 如果多个类的多个属性需要相同的验证/转换逻辑——自定义描述符。
3. 如果需要实现类似ORM框架的字段系统——自定义描述符(配合元类效果更佳)。
4. 如果只需要延迟计算——考虑 @functools.cached_property 。
5. 如果需要在类级别和实例级别有不同的访问行为——非数据描述符。
九、进阶用法与注意事项
9.1 描述符的 __set_name__ 协议
Python 3.6 引入了 __set_name__ 方法,它在类创建时自动被调用,将描述符实例所属的类名和属性名传给描述符。这使得描述符无需在构造函数中手动指定属性名:
class AutoNamedDescriptor :
def __set_name__ (self, owner, name):
print (f"在 {owner.__name__} 中注册为 {name}" )
self.name = name
self.private_name = "_" + 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):
setattr (obj, self.private_name, value)
class Demo :
x = AutoNamedDescriptor() # 自动输出: 在 Demo 中注册为 x
y = AutoNamedDescriptor() # 自动输出: 在 Demo 中注册为 y
9.2 属性查找完整流程
理解完整的属性查找流程对于调试描述符相关的问题至关重要:
# obj.attr 的完整查找流程(CPython 实现)
# 1. 在 type(obj).__mro__ 中查找 attr 是否为数据描述符
# (即是否同时定义了 __get__ 和 __set__/__delete__)
# 2. 如果是数据描述符,调用其 __get__
# 3. 否则,在 obj.__dict__ 中查找
# 4. 如果在 type(obj).__mro__ 中找到了非数据描述符或普通属性
# - 非数据描述符:调用 __get__
# - 普通属性:直接返回值
# 5. 如果都没找到,抛出 AttributeError
def attribute_lookup_simulated (obj, name):
"""模拟 CPython 的属性查找过程"""
# 获取对象的类型以及方法解析顺序
obj_type = type (obj)
for cls in obj_type.__mro__:
if name in cls.__dict__:
attr = cls.__dict__[name]
if hasattr (attr, '__set__' ) or hasattr (attr, '__delete__' ):
# 找到数据描述符,无需检查实例字典
if hasattr (attr, '__get__' ):
return attr.__get__(obj, obj_type)
return attr
break
# 检查实例字典——但只有当没有找到数据描述符时
if name in obj.__dict__:
return obj.__dict__[name]
# 检查非数据描述符或普通类属性
for cls in obj_type.__mro__:
if name in cls.__dict__:
attr = cls.__dict__[name]
if hasattr (attr, '__get__' ):
return attr.__get__(obj, obj_type)
return attr
raise AttributeError (f"'{obj_type.__name__}' object has no attribute '{name}'" )
9.3 常见陷阱
使用描述符时需要注意以下几个常见问题:
陷阱一:描述符实例是类属性(共享状态问题)
如果描述符内部使用了 self 来存储状态(而不是通过 obj.__dict__ ),那么所有实例将共享同一个状态,导致数据混乱。正确的做法是在 __set__ 中将数据写入 obj.__dict__ 。
陷阱二:描述符与 __slots__ 的冲突
定义了 __slots__ 的类会限制实例可以拥有的属性。如果描述符使用 obj.__dict__[self.name] 存储数据,但类定义了 __slots__ ,则实例没有 __dict__ ,会导致错误。解决方案是使用 __slots__ 兼容的存储方式(如使用 object.__setattr__ 或提前声明存储槽位)。
陷阱三:property 继承时的意外覆盖
子类中定义同名property会完全覆盖父类的property。如果需要扩展父类的property逻辑,需要在子类中通过父类名显式调用:
class Base :
def __init__ (self):
self._value = 10
@property
def value (self):
return self._value
@value.setter
def value (self, val):
self._value = val
class Derived (Base):
@property
def value (self):
# 覆盖了父类的 property
return super ().value * 2
@value.setter
def value (self, val):
super (Derived, type (self)).value.__set__(self, val)
# 或更简单的方式:直接操作存储属性
# self._value = val
d = Derived()
print (d.value) # 20
d.value = 5
print (d.value) # 10
9.4 描述符的内存管理
使用描述符时需要注意内存泄漏问题。如果描述符内部持有对实例的强引用,将阻止实例被垃圾回收:
import weakref
class SafeDescriptor :
"""使用弱引用避免内存泄漏"""
def __init__ (self):
self._data = weakref .WeakKeyDictionary()
def __get__ (self, obj, objtype=None ):
if obj is None :
return self
return self._data.get(obj)
def __set__ (self, obj, value):
self._data[obj] = value
WeakKeyDictionary 使用弱引用作为键,当实例被销毁时,对应的条目会自动清除,避免了内存泄漏。在需要描述符维护跨实例状态的场景中,这是一个非常重要的考虑因素。
十、总结
核心知识点回顾:
1. @property 装饰器是Python提供的一种优雅方式,用于将方法调用转换为属性访问。它本质上是 property 类的语法糖,而 property 类本身是一个内置的数据描述符。
2. 描述符协议由 __get__ 、__set__ 、__delete__ 三个方法组成,实现了这些方法的类即为描述符。描述符是Python属性访问机制的核心基础设施。
3. 数据描述符(实现了 __set__ )的优先级高于实例字典,而非数据描述符(仅实现 __get__ )会被实例字典遮盖。理解这个优先级是掌握描述符的关键。
4. 自定义描述符适用于需要跨类复用的属性验证逻辑、ORM字段映射、延迟计算等场景。配合 __set_name__ 协议可以自动获取属性名,简化使用。
5. 使用描述符时需要注意共享状态、__slots__ 兼容性、内存泄漏等问题。
描述符协议是Python语言设计精妙之处的绝佳体现。它既提供了底层的灵活性,又通过 @property 这样的语法糖保持了高层次的易用性。掌握描述符不仅能帮助我们编写更高质量的代码,还能加深对Python语言本身的理解,使我们能够更加自信地阅读和修改各种Python框架的源码。
"Python的描述符协议是我见过的设计最优雅的属性管理机制之一。它简单、灵活、强大——这正是Python设计哲学的缩影。"
在后续的学习中,我们还将探讨元类(metaclass)与描述符的协同使用、面向切面编程(AOP)在描述符中的实现、以及描述符在异步编程中的应用等更深入的话题。这些内容将帮助我们构建更为系统和完整的Python知识体系。