Skip to content

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
config = Config()
value = getattr(config, 'debug') # True
value = getattr(config, 'unknown', 'default') # 'default' (has default)

# setattr
setattr(config, 'debug', False)
print(config.debug) # False

# hasattr
if hasattr(config, 'debug'):
 print("Has debug attribute")

# delattr
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

-

  • 03 Magic Methods - Other magic methods
  • 00 Readme - Functional alternatives
  • [05 Custom Bytecode & Metaprogramming](/05-py3/09-bytecode-and-execution/(05-custom-bytecode-metaprogramming/) - Advanced metaprogramming