Photo by K B on Unsplash

Make Python Objects Magical

Saumalya Sarkar
The Startup
6 min readAug 22, 2020

--

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 Basic 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:

  1. What will be shown, when I print an object?
  2. How can I compare between two objects?
  3. What will be returned if I apply built-in length function on one object?
  4. How to make the object iterable/iterator?
  5. How to support indexing on the object, i.e. obj[0]?
  6. 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:

Implemented __str__ method

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:

Implemented __eq__, __gt__, __ge__, __lt__ and__le__ methods

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.

A Basic House Class

Again quite a basic class, which takes three parameters to define a housename, 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:

Implemented __len__ method

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 an iterator from it. Because, looping constructs like for , while will take the iterator object and get the next elements one after another and assign to the loop variable.

Making objects of House class ‘Iterable’:

Implemented __iter__ method

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`:

Implemented __next__ method and updated __iter__ method

Note, __next__ method is implemented such a way, it will always return the next student on the student_list and the state is maintained using current_student_id instance variable. Moreover __iter__ now returns the object itself (after re-initiating the state variable current_student_id) — not an iterator created of the list, because house object itself is an iterator 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.

Implemented __getitem__ method

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:

Implemented __add__ method

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 printing student_list, implement __repr__ method in Student 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 .

--

--

Saumalya Sarkar
The Startup

Pythonista. Messi Maniac. Data Engineer. ML Practitioner. AI Enthusiasts.