Detailed Explanation of Attribute Interception and Management in Python (__getattr__, __getattribute__, __setattr__, __delattr__)
Although this topic has been covered in the list of previously discussed subjects, I will approach it from a different angle, focusing on practical application scenarios and pitfalls to avoid.
1. Basic Concept Review
Attribute interception is a core mechanism of Python's object model, implemented through four special methods:
__getattr__: Called when normal attribute lookup fails__getattribute__: The first entry point for all attribute access__setattr__: Called when setting an attribute__delattr__: Called when deleting an attribute
2. In-depth Analysis of __getattr__ vs __getattribute__
The key differences lie in their invocation timing and risk control:
__getattr__acts as a "safety net," triggered only when an attribute does not exist__getattribute__serves as a "master gate," intercepting all attribute access
class SafeDict:
def __init__(self):
self._data = {}
def __getattr__(self, name):
# Called only when normal lookup fails
if name in self._data:
return self._data[name]
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
def __getattribute__(self, name):
# All attribute access passes through here first
if name.startswith('_'):
# Avoid recursion: Use object.__getattribute__ to access real attributes
return object.__getattribute__(self, name)
return super().__getattribute__(name)
3. Recursion Pitfalls and Solutions
__setattr__ and __getattribute__ can easily cause recursion:
class RecursionSafe:
def __init__(self):
# Wrong way: self.value = 0 leads to infinite recursion
object.__setattr__(self, 'value', 0) # Correct way
def __setattr__(self, name, value):
# Must avoid directly using self.name = value
print(f"Setting attribute {name} = {value}")
object.__setattr__(self, name, value) # Avoid recursion via base class method
4. Practical Application: Lazy Attribute Loading
Combining attribute interception for performance optimization:
class LazyLoader:
def __init__(self):
self._cache = {}
def __getattr__(self, name):
if name not in self._cache:
# Simulate time-consuming initialization
print(f"Lazily loading {name}...")
self._cache[name] = f"Value_{name}"
return self._cache[name]
5. Advanced Patterns for Attribute Access Control
Implementing read-only attributes or validation logic:
class ValidatedObject:
def __setattr__(self, name, value):
if name == 'age' and not isinstance(value, (int, float)):
raise TypeError("Age must be a number")
if name == 'email' and '@' not in value:
raise ValueError("Invalid email format")
super().__setattr__(name, value)
6. Collaboration with the Descriptor Protocol
Attribute interception methods have lower priority than descriptors:
class Descriptor:
def __get__(self, instance, owner):
return "Descriptor has priority"
class TestClass:
attr = Descriptor()
def __getattr__(self, name):
return "Interceptor fallback"
7. Best Practices Summary
- When accessing existing attributes in
__getattribute__, always useobject.__getattribute__() - When setting attributes in
__setattr__, useobject.__setattr__()orsuper().__setattr__() - Clearly distinguish the handling logic between data attributes and method attributes
- Consider the priority relationship with mechanisms like property and descriptors
This deep understanding helps you correctly implement advanced features like dynamic attributes, proxy patterns, ORM mapping, and more in real-world development.