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.
- 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. ↩︎
Leave a Reply