• Skip to primary navigation
  • Skip to main content
bucket-of-data-transparent

bucketofdata.com

Pouring Data into Actionable Insights

  • Show Search
Hide Search

Operators Are Overloadable Functions That Enable Objects to Behave Like Built-in Types

Silver Spade · December 3, 2024 · Leave a Comment

In the context of real-life, objects come with attributes. Some of these attributes can be changed through abilities.

A car is an object that comes with the capacity to carry gasoline. The amount of gasoline in a car is an attribute because the car comes with a gas tank. The ability to fill the tank changes the amount of gas.

In Python and the abstract concept of object-oriented programming, objects are types of code structures that have attributes, and methods to act on those attributes. Some of these built-in methods include the addition operator, +.

Rather than using a fill function to reload the car’s gas tank, we can perform car + gas_amount: This will refill the tank in the same way!

The behavior known as operator overloading allows us to manipulate the behavior of the default addition operator. Operations alter the behavior of attributes through methods.

We cannot expect Python to know how to add custom types with each other, much like we cannot find the derivative of a partial differential equation unless we took a few calculus courses…

Therefore, we program the behavior into the class:

# car.py
# Describes the behavior of a car, the fill function and 
# overloading the addition (+) operator to promote built-in behavior.

class Car():
    def __init__(self, amount_of_gas, tank_size):
    
        self.tank_size = tank_size
        
        if self.tank_size < amount_of_gas:
            self.gas = tank_size # You cannot overfill the tank!
        else:
            self.gas = amount_of_gas
        
    def fill_tank(self, amount_of_gas):
        if self.tank_size < self.gas + amount_of_gas:
            self.gas = tank_size # You cannot overfill the tank!
        else:
            self.gas += amount_of_gas 

One can always use the fill_tank function to fill a car, but it is more intuitive to call upon the overloaded + operator much like you would use it to add two numbers. It is almost never the case when someone says add(5,2) to add numbers because it is also much less intuitive.1

The use of overloaded operators allows a Pythonista to design code with more flow and allow any developer the same pleasure of reading an understandable file without hiccups.

Basic Behavior of Operator Overloading

I used to wonder why Python included function names surrounded with double scores, such as __lt__(item1, item2). Besides not knowing what the lt meant (it means less than), I would just rather try item1 < item2 and move on with my day.

But __lt__(item1, item2) is a built-in function that provides default behavior for the < operator! Without it, < doesn’t know what to do.

Operator overloading works under the hood by re-writing the built-in functions that are responsible for performing the built-in operations:

  • The __add__(item1, item2) adds two objects, such as numbers
  • The __eq__(item1, item2) tests equality between two objects, such as strings
  • The __concat__(item1, item2) glues two objects, such as lists

The key observation is that these functions always operate on objects. Remember that numbers and strings, although they are built-in types, are also objects in Python!

Name-Mangling to Hide Implementation of the Overloaded Operator

Now that operator overloading is understood, let’s implement the behavior with code:

# car.py
# Describes the behavior of a car, the fill function and 
# overloading the addition (+) operator to promote built-in behavior.

class Car():
    def __init__(self, amount_of_gas, tank_size):
    
        self.__tank_size = tank_size
        
        if self.__tank_size < amount_of_gas:
            self.__gas = tank_size # You cannot overfill the tank!
        else:
            self.__gas = amount_of_gas
        
    # This will be name-mangled to promote encapsulation
    def __fill_tank(self, amount_of_gas):
        if self.__tank_size < self.__gas + amount_of_gas:
            self.__gas = self.__tank_size # You cannot overfill the tank!
        else:
            self.__gas += amount_of_gas 
            
    # Here we now overload the + operator by overloading __add__():
    def __add__(self, amount_of_gas):
        self.__fill_tank(amount_of_gas)
        
        # We want to modify the original object,
        # not create and return a new one.
        return self
        
    # Defining the string representation of the object
    # so print() returns a valid, human-readable description
    # as an f-string
    def __str__(self):
        return (f'This is a car with {self.__gas} units for gas and '
        f'{self.__tank_size} units of capacity for gas.')
        
if __name__ == '__main__':
    my_car = Car(5, 10)
    my_car += 3
    print(my_car)
    
    # This will raise an error
    # my_car.fill_tank(6)
    
    # This is OK, but no one would write this.
    # This is the same as my_car += 6
    # or my_car = my_car + 6
    # or, in non-name-mangled programs, my_car.fill_tank(6)
    my_car._Car__fill_tank(6)

By name-mangling the instance attributes and methods, the user must use the addition operator to add gas to the car. He or she would be unable to manipulate the car’s tank capacity or the amount of gas added otherwise, which would be ghastly and eerie.

The __init()__ Method Is an Overloaded Function

This basic method to construct an instance of a class is __init__(). The default behavior is to initialize an instance with no attributes. This occurs if no __init__() constructor is defined within the class.

This method is overloaded when we implement it and initialize our attributes in the form of self.x = x (such as in the Car class shown above).

In fact, this __init__() method is called on after creation of the instance with __new__(), which runs in the background after the class is called (such as in my_instance = MyClass(). The __init__() is called whether it’s overloaded or not.

The __init__() Method Is Not Name-Mangled

Something to note is that __init__() starts with double-underscores. However, it is not name-mangled because that behavior only applies to custom- or user-defined attributes and methods.

The __init__() function is recognized by Python as built-in and therefore does not undergo name-mangling.

Calling car = Car().__init__() will not raise an error, but __init__() returns None by default, as it is __new__() that creates the instance; the point is that __init__() is not name-mangled.

The Purpose of Operator Overloading

Operator overloading is implemented on user-defined, custom objects so that they behave more like built-in types.

This can be useful when the class is meant to be used with other built-in types such as lists, tuples and other sequences. Concatenating lists with the custom object is possible if the addition or multiplication operator is overloaded.

By implementing this behavior, programs become more intuitive to understand and work with.

  1. In other cases for different objects, it is more intuitive to use the named function rather than the overloaded operator. The use of overloading should be applied when employing the built-in operator is more understandable. ↩︎

Reader Interactions

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

bucketofdata.com

Copyright © 2025 · Monochrome Pro on Genesis Framework · WordPress