BishopPhillips
BishopPhillips
BPC Home BPC Python Topic Home BPC RiskManager BPC SurveyManager BPC RiskWiki Learn HTML 5 and CSS Enquiry

Python Techniques - Method Overloading

An Introduction.

Author: Jonathan Bishop
AI Revolution

While it does support method overriding, unlike many object-oriented languages, Python does not directly support method overloading.  To a C++ or Delphi programmer this realisation can come as a bit of a surprise.  On the face of it, it may seem a strange omission, but when you think about it a bit more you can start to realise why.  In this article we will explore this Python restriction and consider how you can achieve something similar, and why you don't need overloading in Python.

Function overloading is the facility whereby you can declare multiple methods of the same name in a class or at the global level as functions that are distinguished from one another by their parameter types, order and count.  Function overriding, by contrast is where you declare a method (usually in a derived class) that replaces (or overrides) a method of the same name in an inherited class.  Overriding is supported but means that the redeclared function or method completely replaces the original method, whereas overloaded methods exist side by side with the original method: either can be called depending on the parameter list used by the caller.  In, python with its dynamic typing, variable parameter lists and optional parameters, the ability to reliably distinguish two functions of the same name so they can overload each other is contra-indicated, hence it is not directly supported. 

Pseudo Overloading

While function overloading per-se is not supported there are a number of ways to achieve a similar capability.  Key to these approaches is the fact that while Python lacks static typing (declaring a parameter of a particular type, and no - type hints do not count in this regard as they are not enforceable), it does support type testing and parameter count checking so dynamic typing does not mean there is no typing at all.  Let us consider a few approaches.

Default Arguments

This approach allows you to provide different initialization options by using default values for parameters. A default argument is one where the parameter is declared with a value that is used if the function or method is called without that positional or named parameter:

class MyClass:
    def __init__(self, a=None, b=None):
        if a is not None and b is not None:
            print(f"Initialized with a={a} and b={b}")
        elif a is not None:
            print(f"Initialized with a={a}")
        else:
            print("Initialized with default values")

# Usage examples:
obj1 = MyClass(a=10, b=20)  # Initialized with a=10 and b=20
obj2 = MyClass(a=5)         # Initialized with a=5
obj3 = MyClass()            # Initialized with default values

Using *args and **kwargs

This approach is more flexible and allows you to process different sets of arguments dynamically:

class MyClass:
    def __init__(self, *args, **kwargs):
        if len(args) == 2:  # Check if two positional arguments were provided
            print(f"Initialized with args: {args[0]} and {args[1]}")
        elif 'x' in kwargs and 'y' in kwargs:
            print(f"Initialized with x={kwargs['x']} and y={kwargs['y']}")
        else:
            print("Initialized with default or unexpected arguments")

# Usage examples:
obj1 = MyClass(1, 2)                # Initialized with args: 1 and 2
obj2 = MyClass(x=10, y=20)          # Initialized with x=10 and y=20
obj3 = MyClass()                    # Initialized with default or unexpected arguments

Using @classmethod for Alternate Constructors:

Another way to simulate "overloading" is by using class methods to provide alternate constructors. This keeps the primary __init__ simple while still offering multiple ways to initialize the class.

class MyClass:
    def __init__(self, value):
        self.value = value

    @classmethod
    def from_two_values(cls, a, b):
        return cls(a + b)  # Example: combine two values during initialization

# Usage examples:
obj1 = MyClass(10)                 # Initialize directly
obj2 = MyClass.from_two_values(5, 15)  # Initialize using alternate constructor
print(obj1.value)  # Output: 10
print(obj2.value)  # Output: 20

Use Variable-Length Arguments (*args or **kwargs):

You can use *args to accept a variable number of positional arguments or **kwargs for keyword arguments. Example with *args:

#Example with *args
class MyClass:
    def add(self, *args):
        return sum(args)

# Usage:
obj = MyClass()
print(obj.add(5))             # Output: 5
print(obj.add(5, 10, 15))     # Output: 30


#Example with **kwargs:
class MyClass:
    def add(self, **kwargs):
        return sum(kwargs.values())

# Usage:
obj = MyClass()
print(obj.add(a=5, b=10))  # Output: 15

Check Parameter Types or Count

You can manually inspect the arguments passed to the method to implement behavior based on their number or type.

#Checking the number of args
class MyClass:
    def add(self, *args):
        if len(args) == 2:  # Two arguments
            return args[0] + args[1]
        elif len(args) == 3:  # Three arguments
            return args[0] + args[1] + args[2]
        else:
            raise TypeError("add() requires 2 or 3 arguments")

# Usage:
obj = MyClass()
print(obj.add(5, 10))       # Output: 15
print(obj.add(5, 10, 15))   # Output: 30

Python provides several ways to check types, which allow us to test the type of a parameter provided to the function and perform appropriate actions based thereon. Of these type(), isinstance(), issubclass(), callable() and a range of "behaviours" checks available in collections.abc are perhaps the most useful for this purpose. (See our article on type and behaviours checking in python - here). For example:

We can check the type directly using type() and whether something is a member of a class (and inherited class) or has a behavior (like iterable) using isinstance():

from collections.abc import Iterable

if type(x) is int:
    print("x is an integer")

if isinstance(x, (int, float)):
    print("x is a number")

if isinstance(x, Iterable)
    print("x is iterable")
 

Use Method Overloading Libraries

Although Python doesn't natively support method overloading, we can use third-party libraries like singledispatch from the functools module to achieve this. Example with @singledispatch:

from functools import singledispatchmethod

class MyClass:
    @singledispatchmethod
    def add(self, a):
        raise NotImplementedError("Unsupported type")

    @add.register
    def _(self, a: int, b: int):
        return a + b

    @add.register
    def _(self, a: list):
        return sum(a)

# Usage:
obj = MyClass()
print(obj.add(5, 10))        # Output: 15
print(obj.add([5, 10, 15]))  # Output: 30

Conclusion

While python does not support method or function overloading directly, there are a number of approaches to making a single declaration of a function perform differing actions based on its parameter count and types.