In my previous post we talked about Python’s __getitem__
and __setitem__
methods. These methods are called magic methods or special methods or dunder methods. Well what is the magic about these methods? This is exactly what we are going to see today.
P.S: You will fall in love with Python language (again! ;) )
And it’s gonna be a long post. So, let’s get started.
Magic methods or Dunder methods are just normal methods but with special powers. These magic methods in Python, help you to define the magic to your classes. These magic methods are defined by adding double underscores (__) as prefix and suffix to the method name. To be frank, there isn’t any magic going on. These methods are not well documented in the Python docs and hence we will be seeing these in detail today.
All Python developers are initially taught that __init__
is the first method (or the constructor) that is called after a class is created. But __init__
is not the first method that is been called. Its actually the __new__
method that is been called initially.
__new__
takes arguments as class_name, args, and kwargs. __new__
then passes these args and kwargs to the class_name’s __init__
method
__new__(class_name, args, kwargs)
__init__
is the initializer for the class. This is almost globally used for initialization purposes
__init__(self, args, kwargs)
__del__
is the destructor of the class. Just note that this does not define the behaviour of del x
. It defines the behaviour for when an object is garbage collected
__del__(self)
Let’s look at an example:
class SimpleInit(object):
'''
Class to initialize a list with a value
'''
def __init__(self, value=10):
self._list = [value]
def __del__(self):
del self._list
Arithmetic operations are very common and their magic methods are really handy if you want to create your own data structures. For example, list
in python concatenate if we do some_list + some_list2
. Such kind of behaviour can be defined using magic methods for arithmetic operators.
__add__(self, other)
defines addition (+
)
__sub__(self, other)
defines subtraction (-
)
__mul__(self, other)
defines multiplication (*
)
__floordiv__(self, other)
defines integer division (//
)
__div__(self, other)
defines floating division (/
)
__mod__(self, other)
defines modulo function (%
)
__and__(self, other)
defines bitwise and (&
)
__or__(self, other)
defines bitwise or (|
)
__xor__(self, other)
defines bitwise xor (^
)
__pow__(self, other)
defines exponents (**
)
__lshift__(self, other)
defines bitwise left shift (<<
)
__rshift__(self, other)
defines bitwise right shift (>>
)
Example:
class SimpleAdder(object):
def __init__(self, elements=[]):
self._list = elements
def __add__(self, other):
return self._list + other._list
def __str__(self):
return str(self._list)
a = SimpleAdder(elements=[1,2,3,4])
b = SimpleAdder(elements=[2, 3, 4])
print(a + b) # [1, 2, 3, 4, 2, 3, 4]
Python not only allows us to define custom arithmeic operations but also provide the methods for augmented assignment too! For those who don’t know what augmented assignment is, let us see that with a simple example. Suppose
x = 5
x += 1 # This first adds 5 and 1 and then assigns it back to 'x'
Hence there might be a situation where you want to write some custom logic for augmented assignment operators. The supported operations for the same are:
__iadd__(self, other)
defines addition (+=
)
__isub__(self, other)
defines subtraction (-=
)
__imul__(self, other)
defines multiplication (*=
)
__ifloordiv__(self, other)
defines integer division (//=
)
__idiv__(self, other)
defines floating division (/=
)
__imod__(self, other)
defines modulo function (%=
)
__iand__(self, other)
defines bitwise and (&=
)
__ior__(self, other)
defines bitwise or (|=
)
__ixor__(self, other)
defines bitwise xor (^=
)
__ipow__(self, other)
defines exponents (**=
)
__ilshift__(self, other)
defines bitwise left shift (<<=
)
__irshift__(self, other)
defines bitwise right shift (>>=
)
Python has an extensive set of magic methods for comparisons. We can override the default behaviour of comparison operators to make them work with object references. Here’s the list of comparison magic methods:
__eq__(self, other)
helps to check the equality of two objects. It defines the behaviour of equality operator (==
)
__ne__(self, other)
helps to define the inequality (!=
) operator
__lt__(self, other)
defines the less-than (<
) operator
__gt__(self, other)
defines the greater-than (>
) operator
__le__(self, other)
defines the less-than-or-equal-to (<=
) operator
__ge__(self, other)
defines the greater-than-or-equal-to (>=
) operator
Example:
class WordCounter(object):
'''
Simple class to count number of words in a sentence
'''
def __init__(self, sentence):
# split the sentence on ' '
if type(sentence) != str:
raise TypeError('The sentence should be of type str and not {}'.format(type(sentence)))
self.sentence = sentence.split(' ')
self.count = len(self.sentence)
def __eq__(self, other_class_name):
'''
Check the equality w.r.t length of the list with other class
'''
return self.count == other_class_name.count
def __lt__(self, other_class_name):
'''
Check the less-than w.r.t length of the list with other class
'''
return self.count < other_class_name.count
def __gt__(self, other_class_name):
'''
Check the greater-than w.r.t length of the list with other class
'''
return self.count > other_class_name.count
word = WordCounter('Omkar Pathak')
print(word.count) # 2
print(word == WordCounter("Omkar")) # False
print(word < WordCounter("Omkar")) # False
print(word > WordCounter("Omkar")) # True
Many-a-times developers need to typecast their variables to grab the desired results. Python being a dynamically typed language takes care of your data types internally. But hey, Python cares for you too. If you want you can define custom behaviour while casting using these methods:
__int__(self)
defines type conversion to int
__long__(self)
defines type conversion to long
__float__(self)
defines type conversion to float
__complex__(self)
defines type conversion to complex
__oct__(self)
defines type conversion to octal
__hex__(self)
defines type conversion to hexadecimal
__index__(self)
defines type conversion to an int when the object is used in a slice expression
These are some of the magic methods that you will come across frequently:
__str__(self)
defines behaviour for when str() is called. For example, when you call print (object_name) whatever defined under __str__()
is executed
__repr__(self)
defines behaviour for when repr() is called. This is very much similar to __str__()
. Major difference between these two is that str() is mainly human-readable and repr() is machine-readable
__hash__(self)
defines behaviour when hash() is called
__len__(self)
returns the length of the container
__getitem__(self)
and __setitem__(self)
. For more info on them, visit my previous blog post
__delitem__(self, key)
defines behaviour when an item is deleted. Example, del _list[3]
__iter__(self)
returns an iterator for the container
class CustomList(object):
def __init__(self, elements=0):
self.my_custom_list = [0] * elements
def __str__(self):
return str(self.my_custom_list)
def __setitem__(self, index, value):
self.my_custom_list[index] = value
def __getitem__(self, index):
return "Hey you are accessing {} element whose value is: {}".format(index, self.my_custom_list[index])
def __iter__(self):
return iter(self.my_custom_list)
obj = CustomList(12)
obj[0] = 1
print(obj[0])
print(obj)
So these where some of the magic methods that Python gifts you with. There are many many more and you should research about them as and when required. There is a similar blog post I came across when I was researching some of the magic methods, and I highly recommend all to read this post.
If anyone of you know more methods please do mention them in comments or you can directly open a Pull request here if you want to contribute :)
All content is licensed under the CC BY-SA 4.0 License unless otherwise specified