Magic or dunder(double underscore) methods are special pre-defined methods that allow us to use operators, builtin functions and other features in a special way on our custom objects.
One defining features of these methods is that they start and end with two underscores. The most obvious one and which you have most likely interacted with is the __init__()
method which is called when objects are being instantiated.
By implementing dunder methods, we can make our custom objects behave similar to the built-in types.
Note: The dunder methods are not just normal methods, they are called magic methods for a reason. Python recognizes them and knows exactly what special task each one of them performs .
In these article we will look at various dunder methods, their tasks and how we can incorporate them in our custom classes.
Dunder methods are everywhere
Have you ever wondered what happens when you use some standard operators or call some bulitin functions such as len(), str(), print(), etc?. In reality Python actually calls a corresponding dunder method. This allows Python to provide a consistent and predictable interface no matter what type of object is being worked with.
Consider the following examples:
#create a list
l = [1, 2, 3, 4, 5]
#print l
print(l)
print(l.__str__())
#print the length of list l
print(len(l))
print(l.__len__())
What is going on in the above example is that when you call the print()
function with an object, python actually calls the __str__()
method of that object. In the case with len()
, python calls the __len__()
method.
various dunder methods
The following table summarizes some common dunder methods and their usage.
method | description |
---|---|
__init__ | Initializes class objects. |
__str__ | Returns a string representation of an object. It is called when we are call the builtin str function as well as the print() function in some cases. |
__eq__ | Checks whether two objects are equal. It is called when we use the equality(==) operator. |
__ne__ | Checks whether two objects are not equal. It is called when the not equal (!=) operator is called. |
__add__ | Adds/concatenates two objects together. It is called when we call the addition(+) operator. |
__del__ | Deletes an object from memory. It is called when we use the del statement. |
__call__ | Enables on object to be called like a function. It is called when an objects is being called i.e using parenthesis. |
__repr__ | Returns the 'official' string representation of an object. |
__bool__ | Returns the boolean value of an object. It is called when we call the bool() function on an object. |
Note: The above list is far from exhaustive, there are many more dunder methods not included.
Dunder method with custom objects
The dunder methods allows custom objects to define how the object will behave with standard operators, builtin functions etc.
Let us now see the magic with custom objects.
Basic Example
The __repr__()
method is called by the print() function. The following example shows how we can define this method on our custom object.
#define the class to demonstrate the __repr__ dunder method. This method is called by the print function.
class Example:
#define the __repr__ dunder method
def __repr__(self):
return "I am an example object"
e = Example()
print(e)
A stack data structure
In this part we will create a class for a stack data structure to demonstrate the various magic methods. In a stack data structure objects are stored in a specific order so that the most recently added object is the first one to be removed. It is also known as a Last In First Out (LIFO) data structure. It is an analogous to a stack of plates where the last plate added is the first one to be removed.
The methods relevant to a stack are:
Push | Adds elements onto the top of the stack. |
Pop | Removes the top element from the stack. |
Top | Returns the top element from the stack without removing it. |
isEmpty | Checks if the stack is empty or not |
clear | Removes all items from the stack |
#define the Stack class
class Stack:
#define some dunder methods
def __init__(self):
#define some private attribute
self._data = []
self._size = 0
def __len__(self):
return self._size
def __str__(self):
return("Stack(%s)"%self._data)
def __repr__(self):
return("Stack(%s)"%self._data)
def __bool__(self):
return bool(self.is_empty())
#Define the methods relevant to a stack data type
def push(self, v):
self._size += 1
self._data.append(v)
def pop(self):
if self.is_empty():
raise(Exception("Stack Is Empty!"))
self._size -= 1
return self._data.pop()
def top(self):
return self._data[-1]
def is_empty(self):
return self._size == 0
def clear(self):
self._data.clear()
self.size = 0
#Create a stack object
S = Stack()
#push items to the stack
S.push(10)
S.push(20)
S.push(30)
#print the stack
print(S)
#Add more data to the stack
S.push(100)
S.push(200)
S.push(300)
S.push(400)
print(S)
print(len(S))
S.pop()
S.pop()
print(S)
print(len(S))
#clear the stack
S.clear()
print(S)
print(len(S))
print(bool(S))
Make the stack support relevant standard operators
We can make the stack that we defined above support Python's standard operators such as + to concatenate two stack and == to tell whether to stacks are equal.
#define the Stack class
class Stack:
#define some dunder methods
def __init__(self):
#define some private attribute
self._data = []
self._size = 0
def __len__(self):
return self._size
def __str__(self):
return("Stack(%s)"%self._data)
def __repr__(self):
return("Stack(%s)"%self._data)
def __bool__(self):
return bool(self.is_empty())
#Add the corresponding dunder methods for the relevant operators
def __eq__(self, s):
"""This returns true if the current stack is equal to stack s, else False"""
return self._data == s._data
def __ne__(self, s):
"""This returns true if the current stack is NOT EQUAL to stack s, else False"""
return self._data != s._data
def __add__(self, s):
""""Returns a new stack with items of the current stack combined with those of stack s"""
new_stack = Stack()
for i in self._data:
new_stack.push(i)
for i in s._data:
new_stack.push(i)
return new_stack
#Define the methods relevant to a stack data type
def push(self, v):
self._size += 1
self._data.append(v)
def pop(self):
if self.is_empty():
raise(Exception("Stack Is Empty!"))
self._size -= 1
return self._data.pop()
def top(self):
return self._data[-1]
def is_empty(self):
return self._size == 0
def clear(self):
self._data.clear()
self.size = 0
S1 = Stack()
S2 = Stack()
S3 = Stack()
S1.push(10)
S1.push(20)
S2.push(100)
S2.push(200)
S3.push(10)
S3.push(20)
print(S1 == S2) #Check whether S1 is equal to S2
print(S1 == S3) #Check whether S1 is equal to S3
#add S1 and S2
S4 = S1 + S2
print(S4)
print(len(S4))