Importance Of Delegation in Python

Shampad Sharkar
AnyMind Group
Published in
4 min readJan 10, 2023

If you want an object to pass attribute access requests to another object it contains, rather than handling them itself or inheriting the attributes from a parent class, you can use delegation. This can be useful for implementing a proxy or for an alternative to inheritance.

Delegation is a design pattern in which an object, called the delegate, is responsible for performing certain tasks on behalf of another object, called the delegator. This can be done by the delegator forwarding method calls and attribute access to the delegate. In its most basic form, delegation can be implemented using the following approach: the delegator passes requests for certain actions to the delegate, which then performs the actions on behalf of the delegator.

In its simplest form, it often looks something like this:


class A:
def spam(self, x):
pass
def foo(self):
pass
class B:
def __init__(self):
self._a = A()
def spam(self, x):
# Delegate to the internal self._a instance
return self._a.spam(x)
def foo(self):
# Delegate to the internal self._a instance
return self._a.foo()
def bar(self):
pass

If there are only a couple of methods to delegate, writing code such as that just given is
easy enough. However, if there are many methods to delegate, an alternative approach
is to define the __getattr__() method, like this:


class A:
def spam(self, x):
pass
def foo(self):
pass
class B:
def __init__(self):
self._a = A()
def bar(self):
pass
# Expose all of the methods defined on class A
def __getattr__(self, name):
return getattr(self._a, name)

The __getattr__() method is kind of like a catch-all for attribute lookup. It’s a method
that gets called if code tries to access an attribute that doesn’t exist. In the preceding
code, it would catch access to undefined methods on B and simply delegate them to A.
For example:


b = B()
b.bar() # Calls B.bar() (exists on B)
b.spam(42) # Calls B.__getattr__(‘spam’) and delegates to A.spam

Another example of delegation is in the implementation of proxies. For example:


# A proxy class that wraps around another object, but
# exposes its public attributes
class Proxy:
def __init__(self, obj):
self._obj = obj
# Delegate attribute lookup to internal obj
def __getattr__(self, name):
print(‘getattr:’, name)
return getattr(self._obj, name)
# Delegate attribute assignment
def __setattr__(self, name, value):
if name.startswith(‘_’):
super().__setattr__(name, value)
else:
print(‘setattr:’, name, value)
setattr(self._obj, name, value)
# Delegate attribute deletion
def __delattr__(self, name):
if name.startswith(‘_’):
super().__delattr__(name)
else:
print(‘delattr:’, name)
delattr(self._obj, name)

To use this proxy class, you simply wrap it around another instance. For example:


class Spam:
def __init__(self, x):
self.x = x
def bar(self, y):
print(‘Spam.bar:’, self.x, y)
# Create an instance
s = Spam(2)
# Create a proxy around it
p = Proxy(s)
# Access the proxy
print(p.x) # Outputs 2
p.bar(3) # Outputs “Spam.bar: 2 3”
p.x = 37 # Changes s.x to 37

By customizing the implementation of the attribute access methods, you could customize the proxy to behave in different ways (e.g., logging access, only allowing read-only access, etc.)

Delegation is sometimes used as an alternative to inheritance. For example, instead of
writing code like this:


class A:
def spam(self, x):
print(‘A.spam’, x)
def foo(self):
print(‘A.foo’)
class B(A):
def spam(self, x):
print(‘B.spam’)
super().spam(x)
def bar(self):
print(‘B.bar’)

A solution involving delegation would be written as follows:


class A:
def spam(self, x):
print(‘A.spam’, x)
def foo(self):
print(‘A.foo’)

class B:
def __init__(self):
self._a = A()
def spam(self, x):
print(‘B.spam’, x)
self._a.spam(x)
def bar(self):
print('B.bar')
def __getattr__(self, name):
return getattr(self._a, name)

This use of delegation is often useful in situations where direct inheritance might not
make much sense or where you want to have more control of the relationship between
objects (e.g., only exposing certain methods, implementing interfaces, etc.).
When using delegation to implement proxies, there are a few additional details to note.
First, the __getattr__() method is actually a fallback method that only gets called when
an attribute is not found. Thus, when attributes of the proxy instance itself are accessed
(e.g., the _obj attribute), this method would not be triggered. Second, the __se
tattr__() and __delattr__() methods need a bit of extra logic added to separate
attributes from the proxy instance inself and attributes on the internal object _obj. A
common convention is for proxies to only delegate to attributes that don’t start with a
leading underscore (i.e., proxies only expose the “public” attributes of the held instance).
It is also important to emphasize that the __getattr__() method usually does not apply
to most special methods that start and end with double underscores. For example, con‐
sider this class:


class ListLike:
def __init__(self):
self._items = []
def __getattr__(self, name):
return getattr(self._items, name)

If you try to make a ListLike object, you’ll find that it supports the common list meth‐
ods, such as append() and insert(). However, it does not support any of the operators
like len(), item lookup, and so forth. For example:


>>> a = ListLike()
>>> a.append(2)
>>> a.insert(0, 1)
>>> a.sort()
>>> len(a)
Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
TypeError: object of type ‘ListLike’ has no len()
>>> a[0]
Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
TypeError: ‘ListLike’ object does not support indexing
>>>

To support the different operators, you have to manually delegate the associated special
methods yourself. For example:

class ListLike:
def __init__(self):
self._items = []
def __getattr__(self, name):
return getattr(self._items, name)
# Added special methods to support certain list operations
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __setitem__(self, index, value):
self._items[index] = value
def __delitem__(self, index):
del self._items[index]

--

--