Make Python Objects Magical
The very first thing one must learn and believe without a shred of doubt is that In python, everything is an object.
Python offers a host of magic methods
(alternatively called dunder methods
because of the “double underscores”) to make custom objects more intuitive, easy to use and pythonic. Let’s start with a very basic scenario:
string_sum_output = 'hello' + ' world' + '!'
integer_sum_output = 10 + 20
print(string_sum_output)
print(integer_sum_output)o/p1:>> hello world!
o/p2:>> 30
At first glance o/p1
and o/p2
seems super normal. But let me ask you this,
how does python decide when to do arithmetic sum and when to concatenate values if ‘+’ operator is used?
Most obvious answer will be based on data-types, which is not incorrect, but doesn’t reveal the real process of deciding though. That’s where magic
methods come into picture. Certain behaviours of an objects can be controled and dictated by using these, so called, magic methods.
So in this article, we will see how to add certain behavioural traits to our custom classes, so that objects of those classes will have some special functionalities.
Student Class
Following is our simple Student
class:
A very basic class, which takes three parameters to define a student — name
, phone_number
and standard
.
Scenarios
There are numerous magic methods available in python, we will see some basic and most widely used of those, to implement following scenarios:
- What will be shown, when I print an object?
- How can I compare between two objects?
- What will be returned if I apply built-in
length
function on one object? - How to make the object iterable/iterator?
- How to support indexing on the object, i.e.
obj[0]
? - And finally, what will be the result if any arithmetic operator (‘+’, ‘-’, ‘*’ etc) is used with the objects?
Scenario 1: What will be shown, when I print an object?
So if we just create an object of current Student
class, and try to print it something like following will show up:
>>> student_object = Student('saumalya', 1111100000, 'XII', 'A')
>>> print(student_object)----- Output -----<__main__.Student object at 0x10ff67790>
Frankly speaking, this memory location identifier is not that useful to application developer/user, is it!
Let’s say, we want to see student’s name if we print a student object. In that case, we implement __str__
methods in our student class as shown below:
Now when we print the same object, this happens:
>>> print(student_object)----- Output -----saumalya
It is mandatory to return single string data from ‘
__str__'
method, because this method actually decides the output of — ‘str(student_object)’ method call, which is called by ‘print(student_object)’ method implicitly.
Scenario 2: How can I compare between two objects?
When we talk about value comparison, there are total 5 basic types of comparison possible: equality
, greater than
, greater than equal to
, smaller than
andsmaller than equals to
. Python provides magic methods for each of those comparisons: __eq__
, __gt__
, __ge__
, __lt__
and __le__
respectively to support comparison operators for our object comparison, We will do the comparisons based on which standard
the students are in:
See how simply we can compare the objects using comparison operators:
>>> student_1 = Student('magne', 1111100000, 'XI')
>>> student_2 = Student('isolde', 2222200000, 'XII')
>>> student_3 = Student('try', 3333300000, 'ix')
>>> print(student_1 == student_3)
>>> print(student_2 >= student_1)
>>> print(student_3 > student_2)
>>> print(student_1 < student_3)
>>> print(student_2 <= student_2)----- Output -----False
True
False
False
True
Note, we used one private static method ‘_get_standard_weight(std: str)’ to do some custom derivation before comparison. Similarly, we can implement any complex logic based on our requirement, which makes these magic methods so powerful.
House Class:
For rest of the three scenarios, we will use House
class, only because next functionalities make more sense in context of a house
, instead of a student
. Similarly all magic methods will not be useful for each custom classes, user must choose based on relevance.
Again quite a basic class, which takes three parameters to define a house
— name
, symbol
and student_list
.
Scenario 3: What will be returned if built-in length
function is applied on objects?
Intuitively we might want number of students
in a house
as output of len(house_object)
call. To achieve that we need to implement __len__
in our house
class, as shown below:
Okay, is it working? let’s test!
>>> house_object = House('Gryffindor'
, 'Lion'
, [student_1, student_2, student_3]
)
>>> print(len(house_object))----- Output -----3
Scenario 4: How to make the object iterable/iterator ?
Now before I answer that, we have to discuss a bit about iterator
and iterable
in python.
In simple words, an object is said to be iterable
if it can be looped over. For example, lists
, tuples
, ranges
etc. Whereas Iterators
are stateful objects. The iterator objects are capable of keeping information about the current state and can produce the next element if next()
method is applied on it.
Note, To make an object iterable, the class has to implement
__iter__
method and return aniterator
from it. Because, looping constructs likefor
,while
will take theiterator
object and get the next elements one after another and assign to the loop variable.
Making objects of House class ‘Iterable’:
As you can see, __iter__
method is returning an iterator
object. The iterator
object is created by applying iter()
method on the student_list
. That is possible because built-in list
class is also an iterable
.
>>> house_object = House('Gryffindor'
, 'Lion'
, [student_1, student_2, student_3]
)
>>> for student_object in house_object:
print(student_object)----- Output -----magne
isolde
gry
Printing the ‘student_object’ inside the loop, directly prints name of that student because ‘
__str__'
is defined to return student name. (Scenario 1).
Making objects of House class `Iterator`:
Note,
__next__
method is implemented such a way, it will always return the next student on thestudent_list
and the state is maintained usingcurrent_student_id
instance variable. Moreover__iter__
now returns the object itself (after re-initiating the state variablecurrent_student_id
) — not aniterator
created of the list, because house object itself is aniterator
now.
>>> house_object = House('Gryffindor'
, 'Lion'
, [student_1, student_2, student_3]
)
>>> print(next(house_object))
>>> print(next(house_object))
>>> print(next(house_object))>>> print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")>>> for student_object in house_object:
>>> print(student_object, end=', ')----- Output -----magne
isolde
gry
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
magne, isolde, gry,
‘__next__’ method must raise ‘StopIteration’ error once the available states come to end, the loop constructs wait for that to happen to stop the looping.
Scenario 5: How to support indexing on the object?
At times, we face situations, where indexing comes quite handy. Indexing is a method by which we can extract data from a sequential data structure by specifying the index of the data.
Now, we can implement the similar behaviour on our object also. For example I want house_object[1]
to return isolde
, although we are actually doing index operation on student_list
but I want that transition to be implicit. That way user can apply much more complicated logic than just indexing some list
type instance variable.
To achieve this, we have to implement __getitem__
in our class and write the indexing logic, for our example, the logic is really simple, just return the value of same index from student_list
.
Now we can do this:
>>> house_object = House('Gryffindor'
, 'Lion'
, [student_1, student_2, student_3]
)
>>> print(house_object[2])----- Output -----gry
Note, in this approach, we can support slicing also:
>>> house_object = House('Gryffindor'
, 'Lion'
, [student_1, student_2, student_3]
)
>>> for student in house_object[1:2]:
>>> print(student)----- Output -----isolde
gry
Scenario 6: what will be the result if any arithmetic operator (+, -, * etc) is used with the objects?
That will be decided based on the implementation of magic methods like __add__
, __sub__
, __mult__
, __div__
etc. As all these methods work exactly similar manner, we will see one(__add__
) in our example, and I am sure anyone can implement the rest following same pattern.
So here is how we declare to merge the students, when two house_object
are added:
Now we see the magic:
>>> new_student_1 = Student('Maeve', 9999900000, 'X')
>>> new_student_2 = Student('Otis', 8888800000, 'X')
>>> new_student_3 = Student('Eric', 7777700000, 'X')
>>> new_house_object = House(
'Ravenclaw',
'Eagle',
[new_student_1, new_student_2, new_student_3]
)
>>> print(house_object + new_house_object)----- Output -----[magne, isolde, raxa, Maeve, Otis, Eric]
Similarly __sub__
, __mult__
etc methods can be implemented to support respective operators.
Note: To show the names of student inside a
list
while we are printingstudent_list
, implement__repr__
method inStudent
class exactly similarly to__str__
method.
I hope, this article will help reader to be more pythonic. There are lots other magic method that are exteemely useful too. As the purpose of this article was to introduce readers to python magic methods, I discussed about only a few of them. __new__ and __init__ are two most important magic methods, but I kept those out of this article solely because of the complexity. Maybe some other time, keep an eye out.
P.S.: Any suggestions will be highly appreciated. I am available at saumalya75@gmail.com
and linkedin.com/in/saumalya-sarkar-b3712817b .