Attribute Interception and Dynamic Attribute Access in Python: __getattr__, __getattribute__, and __setattr__
In Python, attribute interception refers to the ability to customize the handling of accessing, setting, or deleting object attributes through special methods. These three core methods form the cornerstone of dynamic attribute access in Python.
1. The Difference Between __getattr__ and __getattribute__
__getattr__: Triggered only when normal attribute lookup fails. When accessing a non-existent attribute, Python calls this method as a fallback mechanism.__getattribute__: Triggered on every attribute access, regardless of whether the attribute exists. It completely takes over the attribute lookup process.
Example Demonstration:
class DynamicClass:
def __init__(self):
self.existing_attr = "I am an existing attribute"
def __getattr__(self, name):
print(f"__getattr__ triggered, accessing non-existent attribute: {name}")
return f"Dynamically created attribute: {name}"
def __getattribute__(self, name):
print(f"__getattribute__ triggered, accessing attribute: {name}")
return super().__getattribute__(name) # Must call parent method to avoid recursion
obj = DynamicClass()
print(obj.existing_attr) # Triggers __getattribute__
print(obj.non_existing) # First triggers __getattribute__, then triggers __getattr__
Output:
__getattribute__ triggered, accessing attribute: existing_attr
I am an existing attribute
__getattribute__ triggered, accessing attribute: non_existing
__getattr__ triggered, accessing non-existent attribute: non_existing
Dynamically created attribute: non_existing
2. How __setattr__ Works
__setattr__ is triggered every time an attribute is set, including initialization assignments in __init__. Special care must be taken to avoid recursive calls:
class SafeSetAttr:
def __init__(self):
self._data = {} # Use a special name to avoid recursion
self.value = 10 # This will trigger __setattr__
def __setattr__(self, name, value):
if name == "_data": # Allow _data initialization
super().__setattr__(name, value)
else:
print(f"Setting attribute {name} = {value}")
self._data[name] = value # Store in internal dictionary
obj = SafeSetAttr()
obj.new_attr = "hello" # Triggers __setattr__
print(obj._data) # View stored data
4. Practical Application Scenario: Dynamic Proxy Pattern
Combining these methods enables powerful dynamic proxy functionality:
class AttributeProxy:
"""Forwards attribute access to another object"""
def __init__(self, target):
super().__setattr__('_target', target) # Bypass __setattr__
def __getattr__(self, name):
return getattr(self._target, name) # Forward to target object
def __setattr__(self, name, value):
setattr(self._target, name, value)
class RealClass:
def __init__(self):
self.x = 100
real = RealClass()
proxy = AttributeProxy(real)
print(proxy.x) # Access attribute through proxy
proxy.y = 200 # Set attribute through proxy
5. Considerations and Best Practices
- Must use
super()in__getattribute__and__setattr__to avoid recursion. __getattr__should handle non-existent attributes; for known attributes, it should raise anAttributeError.- Performance considerations:
__getattribute__affects all attribute accesses, so use it with caution.
By combining these three special methods, advanced functionalities such as attribute validation, lazy loading, and dynamic creation can be achieved, fully demonstrating Python's dynamic nature.