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: 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