Python Proxy Objects Using Dunder Methods
Python provides native support for creating proxy objects—wrappers
that intercept, modify, or delegate operations to an underlying
object—by leveraging dynamic attribute-lookup special methods, commonly
called dunder methods. By implementing methods such as
__getattr__, __setattr__, and
__delattr__, a class can seamlessly forward incoming calls
and variable accesses to an encapsulated instance. This mechanism allows
developers to build transparent wrappers for lazy loading, access
control, logging, and remote procedure calls without altering the
interface of the original object.
The Role of
__getattr__ in Delegation
The primary tool for building a proxy in Python is the
__getattr__ method. Python invokes __getattr__
only when an attribute is not found in the object's instance dictionary
(__dict__) or its class hierarchy. This makes it an ideal
fallback for forwarding requests to a wrapped instance.
class Proxy:
def __init__(self, target):
self._target = target
def __getattr__(self, name):
return getattr(self._target, name)In this implementation:
- When a caller accesses an attribute existing on the proxy (like
_target), Python resolves it normally. - When accessing any attribute not defined on the proxy, Python routes
the request to
__getattr__. - The
getattr()built-in retrieves the attribute fromself._target, effectively delegating method calls and property reads to the underlying object.
Intercepting
Mutations with __setattr__ and
__delattr__
While __getattr__ handles attribute retrieval, proxies
often need to forward mutations and deletions as well.
Unlike __getattr__, __setattr__ and
__delattr__ are called unconditionally whenever an
attribute is modified or deleted. To avoid infinite recursion,
operations targeting the proxy's internal state must bypass the standard
setter by writing directly to self.__dict__ or using
object.__setattr__.
class TransparentProxy:
def __init__(self, target):
object.__setattr__(self, "_target", target)
def __getattr__(self, name):
return getattr(self._target, name)
def __setattr__(self, name, value):
if name == "_target":
object.__setattr__(self, name, value)
else:
setattr(self._target, name, value)
def __delattr__(self, name):
if name == "_target":
object.__delattr__(self, name)
else:
delattr(self._target, name)By delegating __setattr__ and __delattr__
to the underlying _target, state changes apply directly to
the encapsulated object.
Universal
Interception with __getattribute__
When a proxy must intercept all attribute accesses—including
those that exist on the proxy itself—__getattribute__ is
used. Because __getattribute__ executes for every single
attribute reference, any reference to self attributes must
call the base class implementation via
super().__getattribute__() to avoid infinite recursion.
class LoggingProxy:
def __init__(self, target):
super().__setattr__("_target", target)
def __getattribute__(self, name):
if name == "_target":
return super().__getattribute__(name)
target = super().__getattribute__("_target")
print(f"Accessing attribute: {name}")
return getattr(target, name)The Limitation with Magic Methods
Standard dunder methods representing protocols (such as
__len__, __iter__, and
__getitem__) bypass __getattr__ and
__getattribute__ during implicit invocation. CPython
optimizes operations like len(proxy) or
for item in proxy: by searching the class's type slots
rather than the instance dictionary.
To forward special operators completely, a proxy must either explicitly define each required magic method on its class or dynamically populate its class namespace with wrapper functions that delegate to the target instance.