getattr/setattr & Proxy Objects¶
Overview¶
Dynamic attribute access enables runtime flexibility: - getattr/setattr/delattr: Get/set attributes dynamically - getattr: Called when attribute not found - setattr: Intercept all attribute assignments - Proxy objects: Transparent wrappers around objects
Built-in Attribute Functions¶
getattr, setattr, hasattr, delattr¶
class Config:
debug = True
timeout = 30
# getattr: Get attribute dynamically
config = Config()
value = getattr(config, 'debug') # True
value = getattr(config, 'unknown', 'default') # 'default' (has default)
# setattr: Set attribute dynamically
setattr(config, 'debug', False)
print(config.debug) # False
# hasattr: Check if attribute exists
if hasattr(config, 'debug'):
print("Has debug attribute")
# delattr: Delete attribute
delattr(config, 'timeout')
# print(config.timeout) # AttributeError
Magic Methods for Attribute Access¶
getattr - Called When Attribute Missing¶
class SmartDict:
"""Dict-like object with attribute access."""
def __init__(self, data):
self._data = data
def __getattr__(self, name):
"""Called when attribute not found."""
if name.startswith('_'):
raise AttributeError(f"No attribute {name}")
if name in self._data:
return self._data[name]
raise AttributeError(f"No key {name}")
def __setattr__(self, name, value):
"""Intercept all attribute assignments."""
if name.startswith('_'):
# Store private attributes normally
super().__setattr__(name, value)
else:
# Store in _data
self._data[name] = value
# Use it
config = SmartDict({'host': 'localhost', 'port': 8000})
print(config.host) # 'localhost'
print(config.port) # 8000
config.debug = True # Stores in _data
print(config.debug) # True
getattribute - Called for All Attributes¶
class TracingObject:
"""Trace all attribute access."""
def __init__(self):
self.value = 42
def __getattribute__(self, name):
"""Called for EVERY attribute access."""
print(f"Accessing: {name}")
return super().__getattribute__(name)
obj = TracingObject()
print(obj.value) # Prints "Accessing: value" then 42
Proxy Objects¶
Transparent Proxy¶
class Proxy:
"""Transparent proxy to another object."""
def __init__(self, obj):
object.__setattr__(self, '_obj', obj)
def __getattr__(self, name):
return getattr(self._obj, name)
def __setattr__(self, name, value):
if name == '_obj':
object.__setattr__(self, name, value)
else:
setattr(self._obj, name, value)
def __call__(self, *args, **kwargs):
return self._obj(*args, **kwargs)
# Use it
class Model:
def forward(self, x):
return x * 2
model = Model()
proxy = Proxy(model)
print(proxy.forward(5)) # 10 (transparent access)
Lazy Loading Proxy¶
class LazyProxy:
"""Lazily load object on first access."""
def __init__(self, loader):
object.__setattr__(self, '_loader', loader)
object.__setattr__(self, '_obj', None)
def _ensure_loaded(self):
if self._obj is None:
print("Loading object...")
object.__setattr__(self, '_obj', self._loader())
def __getattr__(self, name):
self._ensure_loaded()
return getattr(self._obj, name)
def load_model():
"""Simulate expensive model loading."""
import time
time.sleep(2)
return {'weights': [1, 2, 3]}
# Create proxy - doesn't load yet
proxy = LazyProxy(load_model)
# Access - triggers loading
print(proxy['weights']) # "Loading object..." then [1, 2, 3]
print(proxy['weights']) # No loading message (cached)
Real-World ML Example¶
Model Wrapper with Logging¶
class LoggingModel:
"""Wrap model with automatic logging."""
def __init__(self, model):
self._model = model
self._call_count = 0
def __call__(self, x):
self._call_count += 1
print(f"Forward pass #{self._call_count}")
return self._model(x)
def __getattr__(self, name):
"""Proxy other attributes to wrapped model."""
return getattr(self._model, name)
def __setattr__(self, name, value):
if name in ('_model', '_call_count'):
super().__setattr__(name, value)
else:
setattr(self._model, name, value)
class SimpleModel:
def __init__(self):
self.weight = 2.0
def __call__(self, x):
return x * self.weight
model = SimpleModel()
logged_model = LoggingModel(model)
print(logged_model(5)) # "Forward pass #1" then 10
print(logged_model.weight) # 2.0
logged_model.weight = 3.0
print(logged_model(5)) # "Forward pass #2" then 15
Summary: When to Use¶
| Pattern | Use Case |
|---|---|
| getattr/setattr | Dynamic configuration |
| getattr | Computed attributes, fallbacks |
| getattribute | Comprehensive interception |
| Proxy | Wrapping, logging, lazy loading |
Related Topics¶
- 03 Magic Methods - Other magic methods
- 00 Readme - Functional alternatives
- 05 Custom Bytecode & Metaprogramming - Advanced metaprogramming