Method delegation in Python

Jul 11th, 2018 in  by Michael Cho

Using Python, a short example on how to delegate specified methods to another object, as compared to composition and inheritance.

A common code organization problem I've run into is how to delegate specific methods to another class, where Class A is the commonly available class (think of a User class, for example) but it frequently needs to call a related Class B to do some specific work.

It's a bit difficult to explain in the abstract, so hopefully this contrived example will demonstrate what we had been doing previously.

Option 1 : Call the related class directly


class Parent:
  def __init__(self):
     self.child = Child()

class Child:
   def take_out_the_trash(self):
        ... do some work ...

# Then everywhere in our code...
parent = Parent()
parent.child.take_out_the_trash()

Here, everytime you want to take_out_the_trash(), you have to call the child directly. Nothing wrong with this, it's just more verbose than I would like.

Option 2 : Write a wrapper method in the host class

This technique is less verbose everywhere else, but essentially duplicates many method names and just feels very un-DRY (calling this "WET" seems wrong...).

Here's what it looks like:


class Parent:
  def __init__(self):
     self.child = Child()

  def take_out_the_trash(self):
      return self.child.take_out_the_trash()

class Child:
   def take_out_the_trash(self):
        ... do some work ...

# Then everywhere in our code...
parent = Parent()
parent.take_out_the_trash()

Option 3 : Delegator mixin

To get around this, I've started to use a Delegator class as a mixin, which lets me define methods which the host class will delegate to some other class.

It does this by overriding the __getattr__() method, which is what each instance calls when there is a non-existent method / attribute defined by the class. You might have seen this if you called an invalid method and see an AttributeError saying 'Parent' object has no attribute 'take_out_the_trash'.

Here's the Delegator class I use:


class Delegator(object):
    def __getattr__(self, called_method):
        def __raise_standard_exception():
            raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, called_method))

    def wrapper(*args, **kwargs):
        delegation_config = getattr(self, 'DELEGATED_METHODS', None)
        if not isinstance(delegation_config, dict):
            __raise_standard_exception()
 
        for delegate_object_str, delegated_methods in delegation_config.items():
            if called_method in delegated_methods:
                break
        else:
            __raise_standard_error()

        delegate_object = getattr(self, delegate_object_str, None)

        return getattr(delegate_object, called_method)(*args, **kwargs)

    return wrapper

class Parent(Delegator, Human):
    DELEGATED_METHODS = {
        'child': [
           'take_out_the_trash',
           'do_the_dishes'
        ],
        'spouse': [
            'cook_dinner'
        ]
    }
    def __init__(self):
        self.child = Child()
        self.spouse = Spouse()

# I can now use the following in my codebase:
parent = Parent()
parent.take_out_the_trash()  # Will get done by the Child instance
parent.do_the_dishes()       # Same here
parent.cook_dinner()         # the Spouse instance cooks!

Let's take a look at some of the advantages and disadvantages of this Delegator class.

Delegator Class - Advantages

Once the Delegator class is mixed in and the DELEGATED_METHODS dict is populated, it is very easy to delegate methods to any related classes. This reduces the rest of your codebase, especially if you find these methods being used very often.

If the delegate class also does not have a particular method/attribute defined, then the standard AttributeError is raised - just as if you never mixed in the Delegator class.

Delegator Class - Disadvantages

There's 2 niggling issues I have with the use of this technique.

Firstly, it's not intuitive where you would find the method definition for `take_out_the_trash()` for example. A new engineer would see your code littered with `parent.take_out_the_trash()` and be surprised that there is no such method defined in the Parent class.

Secondly, this Delegator class as it is written would behave unexpectedly if called with an attribute which is not defined. Unlike a method which is not defined (which would raise an AttributeError, as expected), an undefined attribute will actually return the Delegator wrapper function. That is:


parent.do_flying_kick()  # ==> raises AttributeError, as expected
parent.do_flying_kick    # ==> returns a reference to the wrapper function, ie <function Delegator.__getattr__.<locals>.wrapper at 0x107575158>

So long as you are aware of this, it's manageable but inelegant...

Comparison with Composition and Inheritance

Lastly, let's take a few minutes to discuss why this Delegation pattern may be more suitable than Composition and/or Inheritance in some cases.

If we think about Composition as the has-a design pattern and Inheritance as the is-a pattern, Delegation is (in layman's terms) a "asks-somebody-else-to-do" pattern.

What does this mean?

I tend to use Composition to pull in functionality from a separate class which would rarely (never?) exist without the host class, and we would never call without the host class. For example:


class Parent:
   def __init__(self):
       self.hand = Hand()

   def scratch_head(self):
       self.hand.scratch(body_part='head')

# I would expect to call this often:
parent.scratch_head()

# I wouldn't expect to call this disembodied object at all:
hand.scratch()

In contrast, I use Inheritance for functionality which overrides or enhances a base class. This implies a tree hierarchy which is usually missing from Composition. Here's an example:


class Human:
   def leave_house(self):
       self.close_door()
       self.go_party()

class Parent(Human):
    def go_party(self):
        if now().hour > 21:
            return False
        return True

# I can now call:
parent.leave_house()  # this instance will close_door() and go_party() only under certain circumstances, ie a different implementation of the Human class.

# You could then easily have another class which inherits from Human, eg
teenager.leave_house()  # and this instance will behave differently!

I find myself using the Delegation pattern for functionality where the delegate class is also called independently. This is in contrast to Composition where the related class rarely exists without the host class.


class Child:
   def do_chores(self):
      self.take_out_the_trash()
      self.clean_room()

# Both of the following methods would be called just as frequently:
parent.take_out_the_trash()  # Delegate this to the Child
child.do_chores()            # But sometimes the Child instance needs to call the same method too!

This pattern was more common when I was working in Ruby on Rails than in Python, but I've found this a useful abstraction to use judiciously.


Other articles you may like

Using Python enums in SQLAlchemy models
May 16th, 2018
Python command-line scripts with argparse
Feb 15th, 2018
SQLAlchemy commit(), flush(), expire(), refresh(), merge() - what's the difference?
Nov 2nd, 2017
Prioritized Code Review Checklist - what to look for first, second, and last
Sep 21st, 2017
Many to many relationships in SQLAlchemy models (Flask)
Jul 28th, 2017