Python中的属性描述符优先级与属性访问顺序详解
一、问题描述
在Python中,当我们访问一个对象的属性时(例如obj.attr),Python解释器会按照特定的顺序查找这个属性。这个查找过程涉及到多个环节:实例字典、类字典、父类继承链,以及描述符协议。这些环节之间存在优先级关系,理解这个优先级对于掌握Python面向对象编程、元编程以及高级框架设计至关重要。具体问题包括:当实例属性、类属性、描述符同时存在时,哪个会生效?__getattribute__、__getattr__、描述符的__get__方法之间谁先被调用?
二、核心概念准备
在深入优先级之前,我们需要明确几个核心概念:
- 描述符(Descriptor):是一个实现了
__get__、__set__、__delete__中至少一个方法的类。它允许在访问属性时执行自定义代码。 - 数据描述符:同时定义了
__get__和__set__的描述符。 - 非数据描述符:只定义了
__get__的描述符。 - 实例字典:
obj.__dict__,存储实例自己的属性。 - 类字典:
type(obj).__dict__,存储类定义的属性和方法。 - 属性访问魔法方法:
__getattribute__(self, name):访问任何属性时首先被调用。它是属性查找的入口。__getattr__(self, name):当__getattribute__找不到属性时,作为后备机制被调用。__setattr__(self, name, value):设置属性时被调用。
三、属性访问优先级顺序(完整流程)
Python的属性访问机制是一个多层的“瀑布式”流程。对于obj.attr(读取属性)操作,查找顺序如下:
步骤1:触发__getattribute__方法
任何属性访问(包括方法调用)都首先触发对象所属类的__getattribute__方法。这是Python中最高优先级的属性访问钩子。默认的__getattribute__(来自object类)实现了后续所有查找逻辑。如果我们重写了它,就必须手动实现或调用父类的查找逻辑。
步骤2:数据描述符优先(如果存在)
在默认的__getattribute__逻辑中,首先检查类及其父类中是否存在名为attr的“数据描述符”。
- 查找位置:在
type(obj)及其MRO(方法解析顺序)链上的所有类字典中查找attr。 - 判断标准:如果找到的
attr是一个数据描述符(定义了__get__和__set__),则立即调用其__get__方法,并返回其结果。这一步会跳过实例字典。 - 为什么优先:数据描述符通常用于需要强控制的属性(如验证、计算属性),因此Python赋予其最高查找优先级(仅次于
__getattribute__本身)。
步骤3:查找实例字典(obj.__dict__)
如果在类及其父类中没有找到名为attr的数据描述符,那么接下来会查找实例自身的字典obj.__dict__。
- 如果
obj.__dict__中存在键attr,则直接返回对应的值。 - 这是普通实例属性的存放位置。
步骤4:查找非数据描述符或类属性
如果实例字典中没有attr,则返回到类及其父类的字典中查找。
- 此时,如果找到的
attr是一个非数据描述符(只定义了__get__)或一个普通类属性(如方法、类变量),则:- 如果是非数据描述符,调用其
__get__方法,并返回结果。 - 如果是普通类属性(如函数方法、整数等),直接返回该属性本身。
- 如果是非数据描述符,调用其
- 对于方法:当一个函数(方法)在类字典中被找到时,它本质上是一个非数据描述符。在访问时,其
__get__方法会被调用,返回一个绑定方法对象(bound method),其中自动绑定了实例obj。
步骤5:触发__getattr__(如果定义了)
如果经过步骤2、3、4都没有找到attr,并且类中定义了__getattr__方法,那么__getattribute__会抛出一个AttributeError异常。这个异常会被__getattribute__捕获,然后触发调用__getattr__(self, 'attr')方法,并将其返回值作为属性访问的结果。
步骤6:抛出AttributeError
如果以上所有步骤都失败了(包括没有__getattr__),则最终抛出AttributeError异常。
四、设置属性(obj.attr = value)的优先级
对于属性赋值操作,优先级规则有所不同:
- 数据描述符优先:首先检查类及其父类中是否存在名为
attr的数据描述符。如果存在,则调用其__set__方法,赋值过程结束。实例字典不会被修改。 - 调用
__setattr__:如果没有数据描述符,则调用obj.__setattr__('attr', value)。默认的__setattr__实现会将属性写入obj.__dict__。
关键区别:非数据描述符没有__set__方法,因此在赋值时不会被触发。赋值会直接进入__setattr__,最终写入实例字典。
五、详细示例与逐步分析
让我们通过一个代码示例,并结合打印输出来验证上述流程。
class DataDescriptor:
"""数据描述符(定义了__get__和__set__)"""
def __get__(self, obj, objtype=None):
print(f"数据描述符的 __get__ 被调用, obj={obj}, objtype={objtype}")
return "来自数据描述符的值"
def __set__(self, obj, value):
print(f"数据描述符的 __set__ 被调用, obj={obj}, value={value}")
class NonDataDescriptor:
"""非数据描述符(只定义了__get__)"""
def __get__(self, obj, objtype=None):
print(f"非数据描述符的 __get__ 被调用, obj={obj}, objtype={objtype}")
return "来自非数据描述符的值"
class MyClass:
# 类属性:一个数据描述符实例
data_attr = DataDescriptor()
# 类属性:一个非数据描述符实例
non_data_attr = NonDataDescriptor()
# 普通类属性
normal_class_attr = "普通类属性"
def __init__(self):
# 实例属性
self.instance_attr = "实例属性"
# 尝试在实例字典中设置一个与数据描述符同名的属性(通常无效)
# self.data_attr = "尝试覆盖数据描述符" # 这实际上会触发描述符的__set__
def __getattribute__(self, name):
print(f">>> 进入 __getattribute__,查找属性: {name}")
# 必须调用父类的__getattribute__以维持默认查找链
return super().__getattribute__(name)
def __getattr__(self, name):
print(f">>> 进入 __getattr__,处理未找到的属性: {name}")
return f"动态生成的属性:{name}"
# 创建实例
obj = MyClass()
print("="*60)
# 场景1:访问实例属性(存在实例字典中)
print("1. 访问 instance_attr:")
print(obj.instance_attr)
print("="*60)
# 场景2:访问被数据描述符管理的属性
print("2. 访问 data_attr:")
print(obj.data_attr)
print("="*60)
# 场景3:访问被非数据描述符管理的属性
print("3. 访问 non_data_attr:")
print(obj.non_data_attr)
print("="*60)
# 场景4:访问普通类属性(实例字典中没有,类字典中有)
print("4. 访问 normal_class_attr:")
print(obj.normal_class_attr)
print("="*60)
# 场景5:访问不存在的属性(触发__getattr__)
print("5. 访问不存在的属性 missing_attr:")
print(obj.missing_attr)
print("="*60)
# 场景6:设置属性 - 对数据描述符赋值
print("6. 对 data_attr 赋值:")
obj.data_attr = "新值" # 会触发数据描述符的__set__
print("="*60)
# 场景7:设置属性 - 对非数据描述符赋值
print("7. 对 non_data_attr 赋值:")
obj.non_data_attr = "新值" # 非数据描述符没有__set__,因此会写入实例字典
print(f"赋值后,实例字典中的 non_data_attr: {obj.__dict__.get('non_data_attr')}")
print("="*60)
# 场景8:再次访问 non_data_attr(现在实例字典中有同名属性)
print("8. 再次访问 non_data_attr:")
print(obj.non_data_attr) # 现在会返回实例字典中的值,而不是非数据描述符!
预期输出与分析:
============================================================
1. 访问 instance_attr:
>>> 进入 __getattribute__,查找属性: instance_attr
实例属性
============================================================
2. 访问 data_attr:
>>> 进入 __getattribute__,查找属性: data_attr
数据描述符的 __get__ 被调用, obj=<__main__.MyClass object at 0x...>, objtype=<class '__main__.MyClass'>
来自数据描述符的值
============================================================
3. 访问 non_data_attr:
>>> 进入 __getattribute__,查找属性: non_data_attr
非数据描述符的 __get__ 被调用, obj=<__main__.MyClass object at 0x...>, objtype=<class '__main__.MyClass'>
来自非数据描述符的值
============================================================
4. 访问 normal_class_attr:
>>> 进入 __getattribute__,查找属性: normal_class_attr
普通类属性
============================================================
5. 访问不存在的属性 missing_attr:
>>> 进入 __getattribute__,查找属性: missing_attr
>>> 进入 __getattr__,处理未找到的属性: missing_attr
动态生成的属性:missing_attr
============================================================
6. 对 data_attr 赋值:
数据描述符的 __set__ 被调用, obj=<__main__.MyClass object at 0x...>, value=新值
============================================================
7. 对 non_data_attr 赋值:
>>> 进入 __getattribute__,查找属性: __dict__ # 设置属性也会触发__getattribute__
(省略部分内部细节)
赋值后,实例字典中的 non_data_attr: 新值
============================================================
8. 再次访问 non_data_attr:
>>> 进入 __getattribute__,查找属性: non_data_attr
新值 # 注意:这里返回的是实例字典中的值,非数据描述符被“覆盖”了!
六、优先级总结与记忆口诀
我们可以将读取属性(obj.attr)的优先级简化为一个清晰的链条:
数据描述符 > 实例属性 > 非数据描述符/类属性 > __getattr__
口诀:“数实非类后getattr”
- 数:数据描述符(类中定义,且实现
__set__) - 实:实例属性(
obj.__dict__中) - 非类:非数据描述符 或 类属性(类字典中)
- 后getattr:最后才轮到
__getattr__
七、实际应用场景与注意事项
- @property装饰器:
@property创建的是一个非数据描述符(只有__get__)。@attr.setter会为它添加__set__,使其变成数据描述符。这就是为什么没有setter的property不能被赋值的原因(实例字典可以,但会破坏封装)。 - 方法绑定:类中定义的普通方法也是非数据描述符。访问
obj.method时,描述符的__get__被调用,返回一个绑定了obj的bound method。 - 覆盖行为:实例字典可以“覆盖”非数据描述符和类属性(如场景8),但不能覆盖数据描述符。因为查找时数据描述符优先级更高。
- 性能考虑:
__getattribute__会被每次属性访问调用,即使属性存在。因此重写它时要格外小心,避免性能瓶颈。 __dict__访问:即使是访问obj.__dict__本身,也会触发__getattribute__。在重写__getattribute__时,如果要访问__dict__,必须使用super().__getattribute__('__dict__')或直接操作object.__getattribute__(self, '__dict__'),否则会引发递归调用。
理解这个优先级链条,是掌握Python属性访问控制、实现高级框架(如ORM、验证库)和进行有效元编程的基础。