属性访问控制协议

深入 __getattr__ / __setattr__ / __getattribute__ 协议

核心主题: Python属性访问控制协议详解

主要内容: __getattr__ / __getattribute__ 调用顺序与区别、__setattr__ 属性设置拦截、__delattr__ 属性删除控制、__slots__ 限制、完整属性查找链、描述符协议、实战应用模式

关键词: Python, __getattr__, __setattr__, __getattribute__, __delattr__, __slots__, 属性控制, 描述符协议

一、属性访问概述

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__ 的协作顺序

当两者同时定义时,完整的调用流程如下:

  1. __getattribute__ 被无条件首先调用
  2. 如果 __getattribute__ 正常返回结果,流程结束
  3. 如果 __getattribute__ 抛出 AttributeError,则解释器转而去调用 __getattr__
  4. 如果 __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 按照以下优先级查找:

属性查找链(从高到低优先级)

  1. __getattribute__:全局拦截器,如果定义则被第一个调用
  2. 数据描述符(data descriptor):定义了 __get____set__ 的类属性(如 property@classmethod
  3. 实例 __dict__:实例自身的属性字典
  4. 类字典及父类字典:类及所有基类中定义的非描述符属性
  5. 非数据描述符(non-data descriptor):只定义了 __get__ 的类属性(如普通方法、@staticmethod
  6. __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) 可以安全绕过

九、核心要点总结

十、进一步思考与练习

思考题

  1. 如果同时定义了 __getattribute____getattr__,并且在 __getattribute__ 中手动捕获 AttributeError 而不抛给上层,__getattr__ 还会被调用吗?
  2. 为什么 Python 的 property 描述符可以覆盖实例字典中的同名属性?这与属性查找链的哪一条规则有关?
  3. 使用 __slots__ 的类能否被 pickle 序列化?如果不能,应如何解决?
  4. 实现一个 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 实现