Dynamic Attributes
Control how attributes are accessed and assigned in your classes.
Python provides special methods to customize attribute access and assignment behavior. These methods allow you to implement advanced patterns like lazy loading, validation, and dynamic attribute creation.
Understanding Dynamic Attributes
Section titled “Understanding Dynamic Attributes”__getattr__(self, name): Called when accessing a non-existent attribute__setattr__(self, name, value): Called every time an attribute is assigned
These act as hooks for attribute access and assignment.
Basic Example
Section titled “Basic Example”class DynamicAttributes: def __init__(self): self.existing_attr = "I exist"
def __getattr__(self, name: str): """Called only for missing attributes""" return f"{name} not found, but here's a default value!"
def __setattr__(self, name: str, value): """Called for every attribute assignment""" print(f"Setting attribute {name} to {value}") super().__setattr__(name, value) # Important: use super()
obj = DynamicAttributes()print(obj.existing_attr) # "I exist" (doesn't trigger __getattr__)print(obj.missing_attr) # "missing_attr not found..." (triggers __getattr__)
obj.new_attr = "New Value" # Prints: "Setting attribute new_attr to New Value"print(obj.new_attr) # "New Value"Real-World Use Cases
Section titled “Real-World Use Cases”1. Lazy Loading
Section titled “1. Lazy Loading”Load expensive data only when accessed:
class LazyLoader: """Load expensive data only when accessed""" def __init__(self): self._data = None self._config = None
def __getattr__(self, name: str): if name == "data": if self._data is None: print("Loading data from database...") self._data = self._load_data() return self._data elif name == "config": if self._config is None: print("Loading configuration...") self._config = self._load_config() return self._config raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
def _load_data(self): # Simulate expensive operation return {"users": 1000, "posts": 5000}
def _load_config(self): return {"theme": "dark", "language": "en"}
loader = LazyLoader()print(loader.data) # Prints: "Loading data..." then returns dataprint(loader.data) # Returns cached data (no loading message)print(loader.config) # Prints: "Loading configuration..." then returns config2. Attribute Validation
Section titled “2. Attribute Validation”Validate attributes before setting:
class ValidatedUser: """Validate attributes before setting""" def __init__(self): self._age = None self._email = None
def __setattr__(self, name: str, value): if name == "age": if not isinstance(value, int) or value < 0: raise ValueError("Age must be a non-negative integer") elif name == "email": if "@" not in str(value): raise ValueError("Invalid email address") super().__setattr__(name, value)
user = ValidatedUser()user.age = 25 # Validuser.email = "user@example.com" # Valid# user.age = -5 # ValueError# user.email = "invalid" # ValueError3. Attribute Logging
Section titled “3. Attribute Logging”Log all attribute access and changes:
class LoggedAttributes: """Log all attribute access and changes""" def __init__(self): self._attributes = {} self._access_log = []
def __getattr__(self, name: str): """Log attribute access""" if name.startswith("_"): raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'") self._access_log.append(f"Accessed: {name}") return self._attributes.get(name, None)
def __setattr__(self, name: str, value): """Log attribute changes""" if name.startswith("_"): super().__setattr__(name, value) else: self._access_log.append(f"Set: {name} = {value}") if not hasattr(self, '_attributes'): super().__setattr__('_attributes', {}) self._attributes[name] = value
def get_log(self): """Get access log""" return self._access_log
obj = LoggedAttributes()obj.name = "Alice"obj.age = 25print(obj.name) # "Alice"print(obj.get_log())# ['Set: name = Alice', 'Set: age = 25', 'Accessed: name']4. Proxy Pattern
Section titled “4. Proxy Pattern”Create a proxy that forwards attribute access:
class Proxy: """Proxy that forwards attribute access to another object""" def __init__(self, target): self._target = target
def __getattr__(self, name: str): """Forward attribute access to target""" return getattr(self._target, name)
def __setattr__(self, name: str, value): """Handle our own attributes, forward others""" if name.startswith("_"): super().__setattr__(name, value) else: setattr(self._target, name, value)
class User: def __init__(self, name: str): self.name = name
def greet(self): return f"Hello, I'm {self.name}"
# Usageuser = User("Alice")proxy = Proxy(user)
print(proxy.name) # "Alice" - forwarded to userprint(proxy.greet()) # "Hello, I'm Alice" - forwarded to userproxy.name = "Bob" # Forwarded to userprint(user.name) # "Bob"Important Notes
Section titled “Important Notes”Using super() in __setattr__
Section titled “Using super() in __setattr__”Always use super().__setattr__() to avoid infinite recursion:
class CorrectSetattr: def __setattr__(self, name: str, value): # Use super() to avoid infinite recursion super().__setattr__(name, value)
class WrongSetattr: def __setattr__(self, name: str, value): # WRONG! Causes infinite recursion self.name = value # This calls __setattr__ again!__getattr__ vs __getattribute__
Section titled “__getattr__ vs __getattribute__”class Example: def __init__(self): self.existing = "I exist"
def __getattr__(self, name: str): """Only called for missing attributes""" return f"{name} not found"
def __getattribute__(self, name: str): """Called for EVERY attribute access""" print(f"Accessing: {name}") return super().__getattribute__(name)
obj = Example()print(obj.existing) # Prints "Accessing: existing" then "I exist"print(obj.missing) # Prints "Accessing: missing" then "missing not found"