Attribute Interception and Dynamic Attribute Access in Python: `__getattr__`, `__getattribute__`, and `__setattr__`

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 an AttributeError.
  • 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.