Method and Function Overriding in Python

Posted on May 02, 2021 in Tips & Tricks

Method and Function Overriding in Python

Using @functools.singledispatch and type annotations.

Photo by ThisIsEngineering from Pexels

Method and Function Overriding in Python

Using @functools.singledispatch and type annotations.

Method and Function overriding is an extremely useful technique. It allows you to define the same method multiple times in your code — but with each method taking parameters of a different type.

A Note on Terminology, for Those who Care!

Methods are defined on classes. Overloading is what happens when a child class redefines the method available in a parent class. Therefore we call this method overloading. I won’t be discussing method overloading in this article.

Functions are not defined on class objects (normally). When we define multiple versions of a function, it is called function overriding. This article is about function overriding in Python.

An Example without Function Overriding

Let’s use the iconic example of a circle class and a square class.

Nothing much to see here. The square has an instance property: the length of its sides, and the circle defines a radius property. The __init__ method of each instance takes an initialization value, and 0 is the default. This is object-oriented Python 101, nothing special so far. We’ll need this code later.

A Function to Calculate the Area of the Object.

The area of a square is calculated differently from the area of a circle. My function, area, can take either a square or a circle object and calculate its area. It knows the different ways to calculate the are of these two geometric shapes. In the future, I might want to add support for other shapes.

This function uses an if … elif construct to identify if the object is either a circle or a square, and then use a different calculation. This function does not use overriding. It accepts parameters of any type, and figures out how to deal with them.

This is the driver for the function:

And here is the output in my terminal:

113.09733552923255
144.0

You can find the example here as a Gist on Github.


An Example using Function Overriding

The area function works just fine so far — there is nothing much wrong with it. However, it will become bloated as I add support for more and more geometric shapes. It will also be harder to test as it grows.

Let’s break down the function into more specific variants, using overriding. Replace the previous area function with the following code:

Here’s what’s going on:

  • Line #1: Import singledispatch from the functools module. This is where the ‘magic’ happens.
  • Line #2: Use the decorator on a function named area.
  • Line #3-#4: The base function, area, is not implemented and returns an error.
  • Line #7: Register an override for thearea function which takes a Circle parameter
  • Line #12: Register an override for thearea function which takes a Square parameter
  • Lines #8 and #9: The registered functions are simply named _ because they won’t be called directly.

As you can see, we have registered two specific handlers to dispatch (or handle) variations in a single parameter to the function. That’s why the decorator is called singledispatch. This was introduced in Python version 3.4.

Why Raise a NotImplementedError on the Base Function?

This is a bit of convention, but is very useful. If I’m writing a library for other people to use, then they may define shapes such as a pentagram, which I don’t know about. Instead of silently returning 0 we choose to raise an exception for the client to handle. We’ve explicitly defined that area does not know how to return a calculated value for the object passed in as a parameter.

Running this code I get exactly the same output as before! You can see the full example as Gist on Github.


Using Type Annotations Instead

Up until now, we’ve been registering specific variants of the base method using the following decorators, specifying the type explicitly:

@area.register(Circle)
@area.register(Square)

There is a smarter way, using type annotations! Since version 3.7, Python is very happy to infer the correct type fora dispatch function using the type annotation on the function’s parameter.

Here is an example of the same code, updated to use type annotations:

As you can see in lines #7 and #12, we’re not specifying a type to register the function with. Instead, the correct type is inferred from the annotations on lines #8 and #13.

def _(any_object: Circle): ...
def _(any_object: Square): ...

Using annotations when registering a dispatch handler makes the code self-documenting, and therefore easier to maintain and understand.


Understanding Which Dispatcher will be Called

It’s sometimes useful to figure out which dispatcher will be called if different types are passed to the function.

Here is an example showing how different functions are registered as the dispatcher for a base function:

This is the result in my terminal — you’ll get different addresses.

<function _ints at 0x10b2a0dc0>
<function _lists at 0x10b2a0e50>
<function _floats at 0x10b2a0ee0>
<function fancy_print at 0x10b2a0af0>

Overriding Instance Methods using @singledispatchmethod

Instance methods always have an additional, first parameter, usually named self. The functools module provides a replacement for singledispatch called singledispatchmethod, which is used for instance methods. It can deal with the initial self parameter.

Here is a contrived example with a Dog class. It can bark, with different qualities. Sending an integer value will make the dog bark multiple times. Sending a string will also change the quality of the barking.

This is the output in my terminal:

BARK BARK BARK BARK BARK 
quiet bark

As you can see from this example, overriding of a class’ instance methods is possible by using the @singledispatchmethod decorator.

Conclusion and Caveat

Within the functools module, Python since version 3.4 has offered two decorators: @singledispatchmethod for use on instance methods and @singledispatch for use on functions. Since version 3.7, these have been able to use type annotations.

This is a fantastic capability — but it has its limitations. These decorators can only be used for overriding the first argument in a method or function signature. If you need to be able to override multiple arguments, you’ll have to look elsewhere. Perhaps the PyPi module multipledispatch is what you need!

If you’d like to know more about object-oriented programming in Python, take a look at some of my other articles!