3.16 magic methods
Contents
Note
Click here to download the full example code or to run this example in your browser via Binder
3.16 magic methods#
This file describes the so called magic methods in python. Magic methods are
those methods which start with double underscore __ sign. We have already
seen some of the magic methods such as __init__ in 3.5 init method
, __call__ in 3.14 __call__ and about __repr__
and __str__ in 3.6 str and repr methods lessons. Here
we will cover some more.
__add__#
This method determines the behavior when addition is performed on the instance
of its class. Thus, using __add__ method of a class, we can define
how the addition on the instance of this class will work. For example, in
class NonSenseInteger below, we
are defining __add__ method, so any instance of NonSenseInteger class
will behave the way we are defining in __add__ method.
import os
class NonSenseInteger(int):
def __init__(self, value):
self.value = value
def __add__(self, other):
return self.value - other
ns_int = NonSenseInteger(10)
print(ns_int + 5)
5
Above we have defined that addition will work as subtraction.
print(5 + ns_int)
15
However, above, when the instance of class is on right side of addition operation,
the addition did not happened the way we defined in __add__ method.
__radd__#
The __add__ method does not determines the addition behavior of a class
when the instance of the class is on right side of + operator.
In order to overwrite this behavior i.e., the working of addition operation when
the instance of class is on right side of +, we have to write __radd__
method.
class NonSenseInteger(int):
def __init__(self, value):
self.value = value
def __add__(self, other):
return self.value - other
def __radd__(self, other):
return self.value * other
ns_int = NonSenseInteger(10)
print(ns_int + 5)
print(5 + ns_int)
5
50
Above we see that when ns_int was on left side, subtraction was performed
as we defined in __add__ method and when ns_int was on right side,
multiplication was performed as we defined inside __radd__ method.
__mul__#
This method determines the behavior when multiplication is performed on the instance of its class.
class NonSenseInteger(int):
def __init__(self, value):
self.value = value
def __mul__(self, other):
return self.value + other
ns_int = NonSenseInteger(10)
print(ns_int * 5)
15
Although 10 * 5 is 50, but we got 15, because we modified the multiplication behavior of our NoneSenseInteger class.
print(5 * ns_int)
50
__rmul__#
class NonSenseInteger(int):
def __init__(self, value):
self.value = value
def __mul__(self, other):
return self.value + other
def __rmul__(self, other):
return self.value - other
ns_int = NonSenseInteger(10)
print(ns_int * 5)
print(5 * ns_int)
15
5
__sub__#
This method determines the behavior when subtraction operation is performed on the instance of its class.
class NonSenseInteger(int):
def __init__(self, value):
self.value = value
def __sub__(self, other):
return self.value + other
ns_int = NonSenseInteger(10)
print(ns_int - 5)
15
print(5 - ns_int)
-5
__rsub__#
class NonSenseInteger(int):
def __init__(self, value):
self.value = value
def __sub__(self, other):
return self.value + other
def __rsub__(self, other):
return self.value * other
ns_int = NonSenseInteger(10)
print(ns_int - 5)
print(5 - ns_int)
15
50
__truediv__#
This method determines the behavior when division operation is performed on the instance of the class.
class NonSenseInteger(int):
def __init__(self, value):
self.value = value
def __truediv__(self, other):
return self.value * other
ns_int = NonSenseInteger(10)
print(ns_int / 5)
50
print(5 / ns_int)
0.5
__rtruediv__#
class NonSenseInteger(int):
def __init__(self, value):
self.value = value
def __truediv__(self, other):
return self.value * other
def __rtruediv__(self, other):
return self.value + other
ns_int = NonSenseInteger(10)
print(ns_int / 5)
print(5 / ns_int)
50
15
__enter__ and __exit__#
These methods are used by the context
manager i.e. with. They are executed/called when we ‘enter’ and
‘exit’ the context manager.
class Insan:
def __init__(self, name, year, age):
self.name = name
self.year = year
self.age = age
def __enter__(self):
print(f"{self.name} was born in year {self.year}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"{self.name} lived until {self.year + self.age} year")
return
def married(self, spouse_name:str):
print(f"{self.name} married with {spouse_name}")
return
with Insan('Ali', 600, 63) as person:
print("entered")
Ali was born in year 600
entered
Ali lived until 663 year
If you note the print order of strings,
you will find out that __enter__ method was executed before
print() function was called. Similarly, __exit__ method
was executed after print() function was called i.e. at
the time of exiting the context manager.
what if we implment only __exit__ and not __enter__?
class Insan:
def __init__(self, name, year, age):
self.name = name
self.year = year
self.age = age
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"{self.name} lived until {self.year + self.age} year")
return
def married(self, spouse_name:str):
print(f"{self.name} married with {spouse_name}")
return
ali = Insan('Ali', 600, 63)
print(type(ali))
<class '__main__.Insan'>
# uncomment following three lines
# with Insan('Ali', 600, 63) as person:
# print("entered")
# person.married('Falima') # -> AttributeError: __enter__
The error message shows that it is not possible to use Insan
class with context manager without implementing __enter__
method for this class. This is because when we say
with Insan(‘Ali’, 600, 63) as person:, the __enter__ method
of Insan class is called implicitly. When this method does not
exist, we get the error as shown above.
Same is true if we implement only __enter__ method and not __exit__ method.
class Insan:
def __init__(self, name, year, age):
self.name = name
self.year = year
self.age = age
def __enter__(self):
print(f"{self.name} was born in year {self.year}")
return self
def married(self, spouse_name:str):
print(f"{self.name} married with {spouse_name}")
return
# uncomment following three lines
# with Insan('Ali', 600, 63) as person:
# print("entered")
# person.married('Falima') # -> AttributeError: __enter__
Other than that, we can still use this class as normal python class.
ali = Insan('Ali', 600, 63)
print(ali.age)
63
__iter__ and __next__#
The __next__ magic method determines what will happen when we call next function
on the instance of the class.
class Insan:
def __init__(self, num_child):
self.children = [f"child_{i}" for i in range(num_child)]
self.index = 0
def __next__(self):
item = self.children[self.index]
self.index += 1
return item
ali = Insan(2)
print(ali.children)
['child_0', 'child_1']
next(ali)
'child_0'
next(ali)
'child_1'
If we call the next function again on ali, we will get IndexError
due to what is happening inside __next__ method above.
# uncomment following line
# next(ali)
Although, we can apply next function on ali but we can still not
use it in a for loop.
ali = Insan(2)
# uncomment following two lines
# for child in ali:
# print
This is because ali is not an iterable and we can verify it as below
import collections
isinstance(ali, collections.abc.Iterable)
False
Since ali is not an “iterable”, therefore we can not use it in for loop.
The reason is that the for loop requests an iterator from the iterable object, and then calls
__next__ on that iterable until it hits the StopIteration exception.
This happens under the surface which is also the reason why we would want
iterators to implement the __iter__ as well.
Question:
why the code isinstance(ali, collections.abc.Iterator) returns False?
class Insan:
def __init__(self, num_child):
self.children = [f"child_{i}" for i in range(num_child)]
self.index = 0
def __next__(self):
item = self.children[self.index]
self.index += 1
return item
def __iter__(self):
return self
ali = Insan(2)
isinstance(ali, collections.abc.Iterator)
True
isinstance(ali, collections.abc.Iterable)
True
Now we can use ali in a for loop but,
# uncomment following two lines
# ali = Insan(2)
# for child in ali:
# print(child)
but after the last iteration, we will get IndexError because of the way we have
implemented the __next__ method above.
Question: Elaborate the above mentioned reasoning?
class Insan:
def __init__(self, num_child):
self.children = [f"child_{i}" for i in range(num_child)]
self.index = 0
def __next__(self):
try:
item = self.children[self.index]
except IndexError:
raise StopIteration
self.index += 1
return item
def __iter__(self):
return self
ali = Insan(2)
Above we have implemented the __next__ method in a way to raise StopIteration
error instead of IndexError. Since the for loop under the hood runs
until StopIteration and then the for loop just bypasses the StopIteration,
we can now use the ali in for loop safely.
child_0
child_1
However, there is a problem in the above code, if we run the above for loop again, we don’t get any output as shown below,
This is because we are not not resetting self.index to 0 after raising
StopIteration exception.
class Insan:
def __init__(self, num_child):
self.children = [f"child_{i}" for i in range(num_child)]
self.index = 0
def __next__(self):
try:
item = self.children[self.index]
except IndexError:
self.index = 0
raise StopIteration
self.index += 1
return item
def __iter__(self):
return self
ali = Insan(2)
for child in ali:
print(child)
child_0
child_1
child_0
child_1
__len__#
This method determines the output of len function, when applied
on the instance of a class.
class Family:
def __init__(self, num_children):
self.num_children = num_children
def __len__(self):
return 1 + 1 + self.num_children
fam = Family(3)
len(fam)
5
Since fam is instance of Family class, the answer
to len function was same as we determined in __len__ method.
Had we not defined the __len__ method for Family class,
we would have got TypeError if we had applied len function on it.
class Family:
def __init__(self, num_children):
self.num_children = num_children
fam = Family(3)
# uncomment the following line
# len(fam) # -> TypeError: object of type 'Family' has no len()
__getitem__ and __setitem__#
If we define these methods for a class, then we can index the instance of the
class using the slice operator i.e., [].
class Data:
def __init__(self, values):
self.values = values
def __getitem__(self, item):
return self.values[item]
data = Data([1, 2, 3, 4])
print(data[0])
1
print(data[1])
2
# uncomment following two lines
# for idx in range(5):
# print(data[idx]) # -> IndexError: list index out of range
The above example was too simple. Following example shows a more useful case for employment
of __getitem__ method where we would like to index two arrays simultaneously.
class Data:
def __init__(self, x, y):
self.x = x
self.y = y
def __getitem__(self, item):
return self.x[item], self.y[item]
data = Data([1,2,3], [11, 12, 13])
print(data[0])
(1, 11)
1 11
Even the lengths of x and y are not equal in above case, we were still able to slice them. We should have constructed the Data class in such a way to raise the error when the lengths are not equal. Without this, the error message becomes more confusing when the item is present in x but not in y.
# uncomment following line
# print(data[3]) # -> IndexError: list index out of range
class Data:
def __init__(self, x, y):
assert len(x) == len(y), 'length of x and y should be equal'
self.x = x
self.y = y
def __getitem__(self, item):
return self.x[item], self.y[item]
Now,if the lengths of x and y are not equal, we will get more useful error message.
# uncomment following line
# data = Data([1, 2, 3, 4], [11, 12, 13]) # -> AssertionError: length of x and y should be equal
1 11
__del__#
This method determines what will happen to an object (instance of a class) when
del object is executed.
class File:
def __init__(self, name):
self.path = os.path.join(os.getcwd(), name)
with open(self.path, 'w'):
pass
def __del__(self):
print(f"deleting file {self.path}")
os.remove(self.path)
return
f = File("test.txt")
os.path.exists(f.path)
True
del f
deleting file /home/docs/checkouts/readthedocs.org/user_builds/python-seekho/checkouts/dev/scripts/oop/test.txt
__contains__#
This method determines what will happen when we use the instance of a class after in keyword.
class Country:
def __init__(self, provinces:list):
self.provinces = provinces
def __contains__(self, item):
return item in self.provinces
Country is a class which can have provinces.
pak = Country(['balochistan', 'kpk', 'sind', 'punjab', 'gb'])
print('sind' in pak)
True
print('sindh' in pak)
False
For a more comprehensive documentation on magical methods see this
Total running time of the script: ( 0 minutes 0.020 seconds)