Python has a set of magic methods that can be used to enrich data classes; they are special in the way they are invoked. These methods are also called “dunder methods” because they start and end with double underscores. Dunder methods allow developers to emulate built-in methods, and it’s also how operator overloading is implemented in Python. For example, when we add two integers together, 4 + 2
, and when we add two strings together, “machine” + “learning”
, the behaviour is different. The strings get concatenated while the integers are actually added together.
The “Essential” Dunder Methods
If you have ever created a class of your own, you already know one of the dunder methods, __init__()
. Although it’s often referred to as the constructor, it’s not the real constructor; the __new__()
method is the constructor. The superclass’s __new__()
, super().__new__(cls[, ...])
, method is invoked, which creates an instance of the class, which is then passed to the __init__()
along with other arguments. Why go through the ordeal of creating the __new__()
method? You don’t need to; the __new__()
method was created mainly to facilitate the creation of subclasses of immutable types (such as int, str, list) and metaclasses.
class Vector(): def __new__(cls, x, y): print("__new__ was invoked") instance = object.__new__(cls) return instance def __init__(self, x, y): print("__init__ was invoked") self.x = x self.y = y
vector1 = Vector(12, 8) -----------------------------Output----------------------------- __new__ was invoked __init__ was invoked
In addition to __init__()
there are two dunder methods that you should always implement: __repr__()
and __str__()
.
__repr__()
defines the “official” string representation of the object. Ideally, it should output a string that is a valid Python statement and can be used to recreate the object. It is mainly used for debugging.
def __repr__(self): return f"Vector({self.x}, {self.y})"
The __str__()
method also return a string representation of the object; however, this representation doesn’t need to be a valid Python statement. It is used by built-in functions like format()
and print()
, so the string representation should be readable for the end-user. If __str__()
method is not defined it invokes the __repr__()
method.
def __str__(self): return f"{self.x}x + {self.y}y"
# before implementing __str__ print(vector1) #or print(repr(vector1)) # __str__ implemented print(vector1) -----------------------------Output----------------------------- Vector(12, 8) 12x + 8y
Emulating Built-in Functions
__len__()
is used to implement the built-in len()
method. It should return the length of the object. For the vector example, it makes sense if len()
returns the magnitude of the vector, but the return type of len()
is restricted to integers.
def __len__(self): return int((self.x*self.x +self.y*self.y)**(1/2))
The __getitem__()
and __setitem__()
methods are used to implement the built-in functionality of using [index/key]
to read and edit elements of a sequence object like a list
or a mapping object like dict
. For the vector class example, you can use the object[index]
to read and edit the variables instead of creating individual getter and setter methods for both instance variables.
def __getitem__(self, key): if key < 0 or key > 1: raise IndexError("Index out of range! Should either be 0 or 1.") elif key: return self.y else: return self.x def __setitem__(self, key, val): if key < 0 or key > 1: raise IndexError("Index out of range! Should either be 0 or 1.") elif key: self.y = val else: self.x = val
Like functions, Python objects are callable, the __call__()
method defines what happens when an object is called. You can use this to overcome the type restriction of the len()
method and return the magnitude of the vector in float.
def __call__(self): print(f"Vector({self.x}, {self.y}) was called.") return (self.x*self.x +self.y*self.y)**(1/2)
# invoke __len__ print(len(vector1)) # use [] to edit x component of the vector vector1[0] = 9 print(vector1) # call the Vector object mod = vector1() print(mod) -----------------------------Output----------------------------- 12 9x + 8y Vector(9, 8) was called. 12.041594578792296
Operator Overriding Using Dunder Methods
Dunder methods like __add__(self, other)
, __sub__(self, other)
, __mul__(self, other)
, __mod__(self, other)
, etc are used to implement binary arithmetic operations. Let’s say you’re interested in supporting the addition, subtraction and multiplication(dot) operations on two vectors:
def __add__(self, other): if type(other) is not Vector: raise TypeError('other should be an object of class Vector') return Vector(self.x + other.x, self.y + other.y) def __sub__(self, other): if type(other) is not Vector: raise TypeError('other should be an object of class Vector') return Vector(self.x - other.x, self.y - other.y) def __mul__(self, other): if type(other) is not Vector: raise TypeError('other should be an object of class Vector') return self.x * other.x + self.y * other.y
vector2 = Vector(8, -1) print(vector1 + vector2) print(vector1 - vector2) print(vector1 * vector2) -----------------------------Output----------------------------- 17x + 7y 1x + 9y 64
Python also has a set of “rich comparison” dunder methods that are used to override the behaviour of conditional operators such as <, >, ==, <=, etc. Continuing the vector example, let’s say you want to support the less than, greater than and equal to operators on a pair of vectors:
def __lt__(self, other): if type(other) is not Vector: raise TypeError('other should be an object of class Vector') return self() < other() def __gt__(self, other): if type(other) is not Vector: raise TypeError('other should be an object of class Vector') return self() > other() def __eq__(self, other): if type(other) is not Vector: raise TypeError('other should be an object of class Vector') return self.x == other.x and self.y == other.y
print(vector1 > vector2) print(vector3 < vector2) print(vector1 == vector3) -----------------------------Output----------------------------- True False True
The consolidated Vector class can be found in a gist here.
Last Epoch
This article discussed Python dunder methods. Although it didn’t go through all of the dunder methods Python offers, the ones discussed should be enough to enable you to write better, more sophisticated classes in Python and ease the process of understanding (the source code of) built-in and third-party modules. To learn more about dunder methods and the Python data model, refer to the official documentation.
Want to learn more about the ins and outs of Python? Check out these articles: