Friday, November 27, 2020

Daniel Bader: Function and Method Overloading in Python

Function and Method Overloading in Python

How to “overload” your methods and functions in Python so they can be called with different sets of parameters.

When writing a Python program, sometimes it’s nice to be able to create a function or method that allows you to blindly call it no matter what sort of parameter you’re feeding it.

For example, if you’re writing code that will calculate the volumes of different solids, there are two basic ways you could do this.

The first would be to write a method for each type of solid, like so:

  • cube_volume(args)
  • sphere_volume(args)
  • cylinder_volume(args)

Each method would take in some set of args or parameters and return the appropriate value.

The second way to do it would be to create a series of methods simply called volume(args) and change the type of parameters each method took. For example,

  • volume(int side) → Returns cube volume
  • volume(float radius) → Returns sphere volume
  • volume(int height, float radius) → Returns cylinder volume

Each of these would accept different arguments based on how you called them.

The problem with the first technique is that before you can call the appropriate method, you have to determine which method you have to call.

In the second, you’ve “overloaded” the method so that depending on what parameters you send, the code knows which method to call.

In this tutorial you’ll learn how to overload methods and functions in Python. Because this technique works slightly differently in Python compared to other popular programming languages like C++, C#, or Java, we’ll spend some time going over these differences first.

This should make it easier for you to transfer these concepts if your switching to Python from a different programming language background.

Let’s get started!

What Exactly Is Method or Function Overloading?

Overloading a method or a function is one of the hallmarks of some object oriented programming (OOP) languages, like C#, C++, and Java. It allows you to forgo having to name each specific method something different. As the same method name is used with each call, each overloaded function has to differ in the parameters that are sent.

This can be a difference in the arity—that is, the number of parameters. It can also be a difference in the data types, or a difference in the order of the data types. That is, a method called with the parameters (int, long) is overloading one using the call (long, int).

As you can see in the three volume methods above, each overloaded method calls a different combination of an integer and a long data type. I’m always a fan of showing real code examples when discussing programming topics, so let’s take a look at how a statically typed language like C++ handles this task.

Here’s how you’d implement these three overloaded functions in C++ (don’t worry, we’ll switch to Python in a minute):

// Returns cube volume
int volume(int side) {
     return side * side * side;
}

// Volume of sphere
float volume(float radius) {
     return (4.0 / 3.0) * 3.14159265 * radius * radius * radius;
}

// Returns cylinder volume
float volume(int height, float radius) {
     return static_cast<float>(height) * radius * radius * 3.14159265;
}

The program to use these methods could then calculate the volume of a cylinder by calling volume(4, 1.2), or the volume of a sphere by calling volume(2.3):

#include <stdio.h>

int main() {
    printf("Cube volume: %f", volume(5));
    printf("Sphere volume: %f", volume(2.3));
    printf("Cylinder volume: %f", volume(4, 1.2));
    return 0;
}

Why Overload Your Methods?

The reason that overloading is desirable is because it makes your code cleaner and easier to read. Consider these overloaded methods for the Java Math class:

static double max(double a, double b)
static float max(float a, float b)
static int max(int a, int b)
static long max (long a, long b)

These overloaded implementations of the max method return the greater of the two values passed to them. Without overloading, the methods would look like this:

static double maxDouble(double a, double b)
static float maxFloat(float a, float b)
static int maxInt(int a, int b)
static long maxLong(long a, long b)

The non-overloaded methods are harder to read and don’t look as sharp as the overloaded methods. And remember the first line of The Zen of Python:

“Beautiful is better than ugly.”

And nestled somewhere in there is the second adage:

“Readability counts.”

So, How to Overload a Method in Python

The way to overload a method or function in Python depends on if you’re coding in Python 2.x or Python 3.x. Each has its own specific way to do it. See, for some reason, Python doesn’t include overloading in its library in the way that Java or C++ does. For example, here’s a bit of Python code that looks like it should overload a basic method:

def add_stuff(a, b):
    return a + b

def add_stuff(a, b, c):
    return a + b + c

print add_stuff(1, 2)

That looks like it should return 3, right? Well, no. If you run that code, you’ll get a TypeError exception:

TypeError: add_stuff() takes exactly 3 arguments (2 given).

That’s because methods and functions in Python are limited to one function signature per name. As soon as you define a new method in the code using the same name, Python will take that method and its parameters as the definition to replace the previous one.

Overloading Methods in Python 2.7

Python users in 2.7 learned to code a makeshift method to code an overloaded function by using nested conditional statements.

def addStuff(parameter, *args):
    if parameter == 'int':
        result = 0
    if parameter == 'str':
        result = ' '
    for i in args:
        result = result + i
        return result

If you call the add method by using the following code:

print addStuff('int', 1,2,3)
print addStuff('str', 'this', ' works')

then you will get the following output of

6
this works

The problem with this style of overloading a method in Python is that it violates those two tenets. It’s not very pretty and it certainly doesn’t enhance the readability of the code. Imagine if you wanted to overload six methods. You’d have a huge long method with a tangle of nested conditionals that would be annoying to try to debug. Not to mention the hassle of changing the code if you needed to add in how addStuff would handle a long or a double data type.

Thankfully, the Python developers listened and along came a sweeping change to how method overloading would work in Python 3.

Single Dispatch Method Overloading in Python 3

Python 3 added support for method overloading by adding something called single dispatch to the existing functools standard library module.

Developers recognized that Python didn’t have a way for programmers to write generic functions like other languages, and that in order to do so, everyone was coming up with their own way to do it. Some were defining __special__ methods, and others were monkey patching (replacing existing attributes of classes while the program is running. This can be done because Python’s classes are all mutable).

The Python dev team realized that the existing type inspection for methods wasn’t going to work, so they developed the @singledispatch decorator. In doing so, they established a uniform API that allowed coders everywhere to start making overloaded methods that were pretty, functional and readable.

The way that singledispatch works in Python is this. It allows you to make a generic method that acts as the default method when you make the rest of your overloaded methods. You call singledispatch on the first method.

Once you’ve done that, any overloaded methods use the register() attribute of the first generic method. This decorator takes a specific data type as a parameter and implements the method for that specific type.

Using our addStuff example method, overloading it in Python 3 using singledispatch then looks like this:

from functools import singledispatch
@singledispatch

def addStuff(a, b):
    return a + b

@addStuff.register(float)
def _(a, b, c):
    return a + b + c

@addStuff.register(str)
def _(a, b, c, d):
    return a + b + c + d

The first method accepts a default argument that isn’t a float or a string. The best part about this is you can send it anything that isn’t defined in the overloaded methods. This includes complex, int, or even arrays or tuples.

For example, if you call the methods by using the following calls:

print (addStuff((1, 2, 3), (3, 4, 5)))
print (addStuff(1.0, 2.1, 3.0))
print (addStuff('this', ' is', ' python', ' fun!'))

the output will be the following:

(1, 2, 3, 3, 4, 5)
6.1
this is python fun!

If you need to check what data types your single dispatch methods will use, you can do that by using the following console command

addStuff.registry.keys()

This is a read only attribute that will check and return the dict_keys that are used in these methods. For our addStuff methods, the returned information is:

dict_keys([<class 'object'>, <class 'float'>, <class 'str'>])

That lets you know that the first (and default generic) method will accept any object or data type. The second takes floats and the last takes strings. This is a great way to find out if you’ve already accounted for a specific data type.

Multiple Dispatch for Python

Single dispatch is great when you’re dealing with one data type outside of an arbitrarily determined class. However, real life code is never that easy. Consider our method addStuff. The method that is invoked is determined by the type of object – float, string, or a generic type.

This type of method calling is great until you need to determine how two objects will interact. Consider how a cat reacts to a mouse, a dog, or another cat. The cat’s actions are dependent not only on it being a cat, but also based on what animal it sees. In Python (or any other language), single dispatch won’t be able to handle this.

That’s where multiple dispatch mechanisms come in as a way of overloading a method. What essentially happens is that there are two method calls: one determines whether what the first animal is. The second determines what type of animal that one is seeing. This is more precisely called a double dispatch, but you should see how multiple dispatch extends past this. Your program is going to make multiple method calls to determine the object types involved and then use a polymorphic method call for each.

Python does not include native multiple dispatch support. However, there is a library extension called multimethods.py that allows you to use multiple dispatch support. Using our cat, dog, and mouse example, this is how overloading methods would take place using multiple dispatch

This assumes that we’ve written a class named Animal that defines the objects Cat, Mouse, and Dog as well as a class named Interactions that determines the interactions of each, i.e. CatMouse, MouseCat, CatDog, DogCat, etc.

from multimethods import Dispatch
from animal import Cat, Mouse, Dog
from interactions import (
    CatMouse, MouseCat, CatDog, DogCat, DogMouse,
    MouseDog, CatCat, DogDog, MouseMouse)

reaction = Dispatch()
reaction.add_rule((Cat, Mouse), CatMouse)
reaction.add_rule((Cat, Dog), CatDog)
# etc.

Then you can call the reaction by using the call:

reaction(a, b)

This can also be done using Python 2.7 decorators, as Guido Van Rossum (the Benevolent Dictator For Life of Python) showed.

@multimethod(Cat, Dog)
def reaction(a, b):
    """Interaction when cat sees dog"""
    # defines a new behavior

@multimethod(Dog, Cat)
def reaction(a,b):
    """Interaction when cat sees dog"""
    # defines a new behavior

After all of the reactions are defined, you can define the multimethod decorator through a monkey patch. Van Rossum shows how this can be done in his 2005 article here.

It’s one of Python’s strengths that both multiple dispatch and single dispatch can be extended in the language so simply. This lets you code in overloaded methods no matter which version of the language you’re using.


[ Improve Your Python with Dan's 🐍 Python Tricks 💌 – Get a short & sweet Python Trick delivered to your inbox every couple of days. >> Click here to learn more and see examples ]



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...