一、属性访问概述
Python 的属性访问控制是其元编程能力的核心基石。与 Java 或 C++ 的访问修饰符(public / protected / private)不同,Python 提供了一套基于特殊方法(dunder methods) 的拦截机制,允许开发者精确控制对实例属性的每一次读取、写入和删除操作。
核心特殊方法一览
__getattr__(self, name): 仅在正常属性查找失败 后被调用(即属性不存在时)
__getattribute__(self, name): 每次访问属性时无条件 调用(优先级最高)
__setattr__(self, name, value): 每次设置属性值时调用(如 obj.x = val)
__delattr__(self, name): 每次删除属性时调用(如 del obj.x)
__slots__: 类级别的声明,限制实例允许的属性名称集合
理解这套协议对于编写框架、ORM 系统、代理模式、惰性求值、数据验证等场景至关重要。下面我们将逐一深入每个特殊方法,并通过丰富的代码示例展示其行为与陷阱。
二、__getattr__ 与 __getattribute__
2.1 __getattr__:属性缺失时的兜底
__getattr__ 是属性查找链中的最后一环 。当通过正常的属性查找路径(后文详述)无法找到属性时,Python 解释器会自动调用 __getattr__,并将属性名作为参数传入。如果该方法内部也未找到该属性,则抛出 AttributeError。
class DynamicAttr :
def __init__ (self ):
self .existing = "I exist"
def __getattr__ (self , name):
# 访问不存在的属性时返回一个动态值
return f"Dynamic value for ' { name} '"
obj = DynamicAttr ()
print (obj.existing) # 输出: I exist
print (obj.undefined_attr) # 输出: Dynamic value for 'undefined_attr'
这个模式在实现动态代理 、虚拟属性 、REST 客户端字段懒加载 时特别有用。
2.2 __getattribute__:每一次访问都拦截
__getattribute__ 是属性访问的第一道关卡 。无论属性是否存在,只要访问 obj.xxx,就会调用该方法。它的优先级高于 __getattr__,也高于实例的 __dict__ 字典查找。
class LoggedAccess :
def __init__ (self ):
self .value = 42
def __getattribute__ (self , name):
print (f"[LOG] Accessing attribute ' { name} '" )
return super ().__getattribute__ (name)
obj = LoggedAccess ()
print (obj.value)
# 输出:
# [LOG] Accessing attribute 'value'
# 42
无限递归陷阱
在 __getattribute__ 内部,绝对不能使用 self.xxx 来访问属性,因为这将再次触发 __getattribute__,导致无限递归直至栈溢出。正确的做法是调用 super().__getattribute__(name) 或 object.__getattribute__(self, name) 来绕过自身的拦截。
class RecursiveTrap :
def __getattribute__ (self , name):
# 错误!self.__dict__ 会再次触发 __getattribute__
return self .__dict__[name] # RecursionError!
2.3 __getattr__ 与 __getattribute__ 的协作顺序
当两者同时定义时,完整的调用流程如下:
__getattribute__ 被无条件首先调用
如果 __getattribute__ 正常返回结果,流程结束
如果 __getattribute__ 抛出 AttributeError,则解释器转而去调用 __getattr__
如果 __getattr__ 返回结果,则使用该结果;如果它也抛出 AttributeError,则最终抛出异常
class CooperativeDemo :
def __init__ (self ):
self .real_attr = "real"
def __getattribute__ (self , name):
print (f"[getattribute] called with ' { name} '" )
return super ().__getattribute__ (name)
def __getattr__ (self , name):
print (f"[getattr] called with ' { name} '" )
if name == "computed" :
return 999
raise AttributeError (name)
obj = CooperativeDemo ()
print ("--- Accessing real_attr ---" )
print (obj.real_attr)
print ("\n--- Accessing computed attr ---" )
print (obj.computed)
print ("\n--- Accessing missing attr ---" )
try :
obj.missing
except AttributeError as e:
print (f"AttributeError: {e}" )
运行结果:
--- Accessing real_attr ---
[getattribute] called with 'real_attr'
real
--- Accessing computed attr ---
[getattribute] called with 'computed'
[getattr] called with 'computed'
999
--- Accessing missing attr ---
[getattribute] called with 'missing'
[getattr] called with 'missing'
AttributeError: missing
黄金规则
__getattribute__ 是全能拦截器,__getattr__ 是兜底处理器。绝大多数场景只需要 __getattr__(例如 Django ORM 的动态字段访问)。只在需要监控或修改每一次 属性访问时才用 __getattribute__,且必须谨慎避免递归。
三、__setattr__:属性设置的守门员
每当执行 obj.attr = value 时,Python 会调用 __setattr__ 方法。这为我们提供了拦截和修改属性赋值行为的机会,适用于数据验证、日志记录、只读属性等场景。
3.1 类型验证模式
class TypedField :
def __init__ (self , name: str , age: int ):
self .name = name
self .age = age
def __setattr__ (self , name, value):
if name == "name" :
if not isinstance (value, str ):
raise TypeError ("name must be a string" )
if len (value) == 0 :
raise ValueError ("name cannot be empty" )
elif name == "age" :
if not isinstance (value, int ):
raise TypeError ("age must be an integer" )
if value < 0 or value > 150 :
raise ValueError ("age must be between 0 and 150" )
# 关键:调用父类的 __setattr__ 来真正设置属性
super ().__setattr__ (name, value)
p = TypedField ("Alice" , 30 ) # OK
try :
p.age = "thirty" # TypeError
except TypeError as e:
print (e)
try :
p.age = 200 # ValueError
except ValueError as e:
print (e)
3.2 实现只读属性
class ReadOnlyAfterInit :
def __init__ (self , x, y):
# __init__ 中也触发 __setattr__,所以需要先设置初始化的标记
self ._initialized = False
self .x = x
self .y = y
self ._initialized = True
def __setattr__ (self , name, value):
if getattr (self , '_initialized' , False ):
raise AttributeError (f"Cannot modify read-only attribute '{name}'" )
super ().__setattr__ (name, value)
obj = ReadOnlyAfterInit (10 , 20 )
print (obj.x, obj.y) # 10 20
try :
obj.x = 99 # AttributeError!
except AttributeError as e:
print (e)
重要的初始化陷阱
在 __init__ 中通过 self.x = value 赋值时,也会触发 __setattr__!这意味着如果 __setattr__ 写得太死(例如完全禁止赋值),连 __init__ 中的初始化都会失败。上述代码通过一个 _initialized 标志位来解决这个问题。
四、__delattr__:删除属性的拦截
__delattr__ 在 del obj.attr 时被调用,允许我们控制属性的删除行为。最常见的用途是禁止删除 某些关键属性,或在删除前执行清理逻辑。
class ProtectedNamespace :
def __init__ (self ):
self .public_attr = "can be deleted"
self ._protected = "protected value"
self .__private = "truly private"
def __delattr__ (self , name):
if name.startswith ("__" ):
raise AttributeError (f"Deleting private attribute '{name}' is not allowed" )
super ().__delattr__ (name)
ns = ProtectedNamespace ()
del ns.public_attr # OK
try :
del ns._ProtectedNamespace__private # Python 名称修饰
except AttributeError as e:
print (e)
注意:Python 的双下划线命名修饰(name mangling)会将 self.__private 转换为 self._ClassName__private,这一点在使用 __delattr__ 时也需要考虑。
五、__slots__:限制实例属性的声明式方法
__slots__ 是类级别的一个特殊属性,用于声明该类的实例允许拥有的全部属性名称。它通过禁止创建 __dict__ (除非显式包含 '__dict__' 在 slots 中)来实现内存优化和属性白名单约束。
5.1 基本用法与内存优化
class PointWithSlots :
__slots__ = ('x' , 'y' )
def __init__ (self , x, y):
self .x = x
self .y = y
def __repr__ (self ):
return f"Point({self.x}, {self.y})"
p = PointWithSlots (3 , 4 )
print (p.x, p.y) # 3 4
try :
p.z = 5 # AttributeError: 'PointWithSlots' object has no attribute 'z'
except AttributeError as e:
print (e)
# 验证没有 __dict__
print (hasattr (p, '__dict__' )) # False
内存对比
使用 __slots__ 的类,每个实例节省一个字典的开销 (约 50-80 字节)。对于需要创建数百万个小对象的场景(如粒子系统、游戏实体、科学计算),__slots__ 可以显著减少内存占用。
5.2 __slots__ 的局限性
# __slots__ 仅对当前类生效,子类仍需显式定义
class Base :
__slots__ = ('a' ,)
class Derived (Base ):
pass # 没有定义 __slots__,所以派生类会有 __dict__
d = Derived ()
d.a = 1 # OK
d.b = 2 # OK! 因为 Derived 有 __dict__
print (d.__dict__) # {'b': 2}
何时使用 __slots__
创建大量小对象(数万到数百万级别)
需要严格控制实例属性的集合(类似白名单)
与 __setattr__ 配合提供双重保险
不宜使用 的场景:类的属性在运行时动态添加、需要 __dict__ 进行自省或序列化。
六、完整属性查找链
理解 Python 属性解析的完整顺序是掌握属性控制协议的前提。当访问 obj.attr 时,Python 按照以下优先级查找:
属性查找链(从高到低优先级)
__getattribute__: 全局拦截器,如果定义则被第一个调用
数据描述符(data descriptor): 定义了 __get__ 和 __set__ 的类属性(如 property、@classmethod)
实例 __dict__: 实例自身的属性字典
类字典及父类字典: 类及所有基类中定义的非描述符属性
非数据描述符(non-data descriptor): 只定义了 __get__ 的类属性(如普通方法、@staticmethod)
__getattr__: 以上全部失败时调用的兜底方法
这个顺序可以用一个简化的代码模型来演示:
class DemonstrateLookupChain :
# 模拟数据描述符(定义了 __get__ 和 __set__)
class DataDescriptor :
def __get__ (self , instance, owner):
return "from data descriptor"
def __set__ (self , instance, value):
print (f"Data descriptor set: {value}" )
desc = DataDescriptor () # 类属性 - 数据描述符
def __init__ (self ):
self .desc = "instance override" # 尝试通过实例覆盖
obj = DemonstrateLookupChain ()
print (obj.desc) # "from data descriptor" —— 数据描述符优先于实例 __dict__
6.1 描述符协议的优先级
特别值得注意的是:数据描述符优先于实例 __dict__ 。这意味着即使实例的 __dict__ 中有一个同名属性,数据描述符仍然会拦截读取操作。这是一个容易让人困惑但极为重要的细节。相反,非数据描述符(如普通函数/方法)的优先级低于实例 __dict__ ,因此可以为方法赋一个实例属性来"覆盖"它。
class MethodOverrideDemo :
def greet (self ):
return "Hello from method"
obj = MethodOverrideDemo ()
obj.greet = "Hello from instance attr" # 覆盖方法(因为方法是 non-data descriptor)
print (obj.greet) # "Hello from instance attr"
这种查找链设计体现了 Python 的"一致性"与"灵活性"的平衡:数据描述符提供强制约束,非数据描述符允许灵活覆盖。
七、实战应用模式
7.1 惰性求值(Lazy Evaluation)
利用 __getattr__ 实现属性的按需计算和结果缓存:
class LazyProperty :
def __init__ (self , func):
self .func = func
self .name = func.__name__
def __get__ (self , instance, owner):
if instance is None :
return self
value = self .func(instance)
# 缓存结果到实例 __dict__ 中,下次直接使用
instance.__dict__ [self .name] = value
return value
class DataProcessor :
def __init__ (self , data):
self .data = data
@LazyProperty
def expensive_computation (self ):
print ("Performing expensive computation..." )
return sum (self .data) * 2
dp = DataProcessor ([1 , 2 , 3 , 4 , 5 ])
print (dp.expensive_computation) # 第一次:计算并缓存
print (dp.expensive_computation) # 第二次:直接返回缓存值,不打印
7.2 ORM 风格的动态字段代理
class DynamicModelProxy :
"""类似 ORM 模型中根据数据库字段动态生成属性访问"""
def __init__ (self , data: dict ):
# 存储原始数据,但不直接设为属性以避免污染
object .__setattr__ (self , '_data' , data)
def __getattr__ (self , name):
if name.startswith ('_' ):
raise AttributeError (name)
if name in self ._data:
return self ._data[name]
raise AttributeError (f"Field '{name}' not found" )
def __setattr__ (self , name, value):
if name == '_data' :
super ().__setattr__ (name, value)
else :
self ._data[name] = value
def __repr__ (self ):
return f"DynamicModelProxy( { self ._data} )"
# 模拟从数据库查询结果创建
record = DynamicModelProxy ({"id" : 1 , "name" : "Alice" , "email" : "alice@example.com" })
print (record.name) # Alice
print (record.email) # alice@example.com
record.email = "new@example.com"
print (record.email) # new@example.com
7.3 链式配置系统
class ChainConfig :
"""支持链式调用的配置对象"""
def __init__ (self , **kwargs ):
object .__setattr__ (self , '_config' , kwargs)
def __getattr__ (self , name):
if name in self ._config:
value = self ._config[name]
# 如果值是 dict,将其包装为新的 ChainConfig 实例
if isinstance (value, dict ):
return ChainConfig (**value )
return value
raise AttributeError (f"Config key '{name}' not found" )
def __setattr__ (self , name, value):
self ._config[name] = value
# 使用示例
config = ChainConfig (
database={
"host" : "localhost" ,
"port" : 5432 ,
"credentials" : {"user" : "admin" , "password" : "secret" }
},
debug=True
)
print (config.database.host) # localhost
print (config.database.port) # 5432
print (config.database.credentials.user) # admin
print (config.debug) # True
7.4 带有访问审计的属性代理
class AuditedObject :
"""记录所有属性访问和修改的审计包装器"""
def __init__ (self , wrapped):
object .__setattr__ (self , '_wrapped' , wrapped)
object .__setattr__ (self , '_access_log' , [])
def __getattribute__ (self , name):
if name.startswith ('_' ):
return super ().__getattribute__ (name)
wrapped = super ().__getattribute__ ('_wrapped' )
log = super ().__getattribute__ ('_access_log' )
log.append (f"GET {name}" )
return getattr (wrapped, name)
def __setattr__ (self , name, value):
if name.startswith ('_' ):
super ().__setattr__ (name, value)
else :
wrapped = super ().__getattribute__ ('_wrapped' )
log = super ().__getattribute__ ('_access_log' )
log.append (f"SET {name} = {value!r}" )
setattr (wrapped, name, value)
def show_log (self ):
for entry in self ._access_log:
print (entry)
# 使用示例
class User :
def __init__ (self , name):
self .name = name
self .score = 0
user = User ("Bob" )
audited = AuditedObject (user)
audited.score = 100
print (audited.name)
print (audited.score)
audited.show_log ()
# 输出:
# SET score = 100
# GET name
# GET score
八、常见陷阱与最佳实践
陷阱 1:__getattribute__ 中的无限递归
永远不要在 __getattribute__ 中使用 self.xxx 来获取属性。始终使用 super().__getattribute__(name) 或 object.__getattribute__(self, name)。
陷阱 2:__setattr__ 中的无限递归
类似地,在 __setattr__ 中不要使用 self.xxx = value 来设置属性。使用 super().__setattr__(name, value) 或 object.__setattr__(self, name, value)。
陷阱 3:__init__ 与 __setattr__ 的交互
__init__ 方法中的 self.x = value 赋值也会触发 __setattr__。如果你在 __setattr__ 中做了严格的校验,需要确保初始化阶段也能通过,或者使用 object.__setattr__ 绕过。
陷阱 4:__slots__ 与继承
__slots__ 不是继承的。如果子类没有定义 __slots__,它会自动获得一个 __dict__,从而使得父类的 __slots__ 约束在子类中失效。
陷阱 5:__slots__ 与弱引用
使用 __slots__ 的类默认不支持 weakref。如果需要弱引用支持,必须在 __slots__ 中包含 '__weakref__'。
最佳实践总结
优先使用 __getattr__ :除非你必须拦截每一次属性访问,否则优先选择 __getattr__,它更安全、更可预测
始终调用 super() :在所有 dunder 方法中记得委托给父类实现,否则属性将无法正常工作
考虑用 property/描述符替代 :对于简单的只读或验证需求,@property 比自定义 __setattr__ 更简单、更优雅
使用 object.__setattr__ 绕过 :在 __init__ 中需要直接设置内部属性时,使用 object.__setattr__(self, name, value) 可以安全绕过
九、核心要点总结
__getattr__ vs __getattribute__: __getattr__ 是兜底机制(属性不存在时调用),__getattribute__ 是全局拦截器(每次访问都调用)。两者通过 AttributeError 异常协作。
__setattr__: 负责拦截所有属性赋值操作,可用于类型验证、只读保护、日志审计等场景。注意避开 __init__ 中的递归陷阱。
__delattr__: 控制属性删除操作,可用于保护私有属性或触发清理逻辑。
__slots__: 声明式限制实例属性集合,带来内存优化和约束能力,但需注意继承和弱引用等问题。
完整属性查找链: __getattribute__ → 数据描述符 → 实例 __dict__ → 类/父类字典 → 非数据描述符 → __getattr__ → AttributeError。
描述符协议优先级: 数据描述符(有 __set__)优先于实例 __dict__;非数据描述符(仅 __get__)优先级低于实例 __dict__。
实战模式: 惰性求值、ORM 动态字段、链式配置系统、访问审计代理等都是属性控制协议的经典应用。
十、进一步思考与练习
思考题
如果同时定义了 __getattribute__ 和 __getattr__,并且在 __getattribute__ 中手动捕获 AttributeError 而不抛给上层,__getattr__ 还会被调用吗?
为什么 Python 的 property 描述符可以覆盖实例字典中的同名属性?这与属性查找链的哪一条规则有关?
使用 __slots__ 的类能否被 pickle 序列化?如果不能,应如何解决?
实现一个 DefaultAttr 类,使其在访问任何不存在的属性时返回一个默认值(如 None),而不是抛出 AttributeError。
编码练习
练习1:不可变对象 — 实现一个 ImmutablePoint 类,其属性在 __init__ 后不可修改也不可删除。
练习2:类型约束描述符 — 实现一个描述符 TypedField(type),自动验证赋值的类型。例如 name = TypedField(str) 确保只有字符串可以被赋值。
练习3:属性变更通知 — 实现一个 Observable 类,在任意属性被修改时自动调用一个注册的回调函数 on_change(name, old_value, new_value)。
推荐学习路径
Python 官方文档:Customizing Attribute Access
《Fluent Python》第22章:Attribute Descriptors and Property
《Python Cookbook》第8章:Classes and Objects
阅读 CPython 源码中 Objects/object.c 的 _PyObject_GenericGetAttrWithDict 实现
本笔记为 Python 进阶编程系列学习资料,深入解析属性访问控制协议
本学习笔记为本人学习资料,不得转载
免责声明: 本学习笔记只供学习使用,不构成任何形式的编程指南或生产环境建议。实际项目中请结合 PEP 8 规范和团队编码标准进行决策。