Basic Dunder Methods In Python

Also called magic methods, dunder methods are necessary to understand Python. Here’s a guide to getting started with them.

In a piece I wrote on Object-Oriented Programming (OOP), I specifically addressed a single magic method, __init__, which is also called as a constructor method in OOP terminology. The magic part of __init__ is that it automatically gets called whenever an object is created. But the good news is that it’s not the only method that does so. Python provides users with many other magic methods that you have probably used without even knowing about them. Ever used len(), print() or the [] operator on a list? If so, you have been using dunder methods.

5 DUNDER METHODS TO KNOW

  1. Operator Dunder Methods
  2. __str__
  3. __len__
  4. Assignment Dunder Methods
  5. __getitem__

1. OPERATOR MAGIC METHODS

Everything in Python is an object, ranging from the data types like int, str and float to the models we use in data science. We can call methods on an object, like this str object:

Fname = "Rahul"

Now, we can use various methods defined in the string class using the below syntax.

Fname.lower()

But, as we know, we can also use the + operator to concatenate multiple strings.

Lname = "Agarwal"

print(Fname + Lname)

------------------------------------------------------------------

RahulAgarwal

So, why does the addition operator work? How does the string object know what to do when it encounters the plus sign? How do you write it in the str class? And the same + operation happens differently in the case of integer objects. Thus, the operator + behaves differently in the case of string and integer. Fancy people call this  process  operator overloading.

So, can we add any two objects? Let’s try to add two objects from our elementary account class.

Doing so fails, as expected, since the operand + is not supported for an object of type account. But we can add the support of + to our account class using our magic method __add__.

class Account:

   def __init__(self, account_name, balance=0):

       self.account_name = account_name

       self.balance = balance

   def __add__(self,acc):

       if isinstance(acc,Account):

           return self.balance  + acc.balance

       raise Exception(f"{acc} is not of class Account")

Here, we added a magic method __add__ to our class, which takes two arguments  —  self and acc. We first need to check if acc is of class account. If it is, we return the sum of balances when we add these accounts. If we add anything else to an account other than an object from the account class, we would get a descriptive error. Let’s try it:

So, we can add any two objects. In fact, we also have different magic methods for a variety of other operators.

  • __sub__ for subtraction(-)
  • __mul__ for multiplication(*)
  • __truediv__ for division(/)
  • __eq__ for equality (==)
  • __lt__ for less than(<)
  • __gt__ for greater than(>)
  • __le__ for less than or equal to (≤)
  • __ge__ for greater than or equal to (≥)

As a running example, I will try to explain all these concepts by creating a class called complex to handle complex numbers. Don’t worry, though: Complex is just the class’ name, and I will keep the example as simple as possible.

Below, I have created a simple method called __add__ that adds two complex numbers or a complex number and an int/float. It first checks if the number being added is of type int or float or complex. Based on the type of number, we then do the required addition. We also use the isinstance function to check the type of the other object. Do read the hashtagged comments in the code box below.

import math

class Complex:

    def __init__(self, re=0, im=0):

        self.re = re

        self.im = im

    def __add__(self, other):

        # If Int or Float Added, return a Complex number where float/int is added to the real part

        if isinstance(other, int) or isinstance(other, float):

            return Complex(self.re + other,self.im)

        # If Complex Number added return a new complex number having a real and complex part

        elif  isinstance(other, Complex):

            return Complex(self.re + other.re , self.im + other.im)

        else:

            raise TypeError

It can be used as:

a = Complex(3,4)

b = Complex(4,5)

print(a+b)

You would now be able to understand the following code, which allows us to add, subtract, multiply and divide complex numbers with themselves as well as scalars like float, int and so on. You can see how these methods then return a complex number. This code also provides the functionality to compare two complex numbers using __eq__,__lt__,__gt__.

You don’t necessarily need to understand all of the complex number math, but I have tried to use most of these magic methods in this particular class.

import math

class Complex:

    def __init__(self, re=0, im=0):

        self.re = re

        self.im = im

    def __add__(self, other):

        if isinstance(other, int) or isinstance(other, float):

            return Complex(self.re + other,self.im)

        elif  isinstance(other, Complex):

            return Complex(self.re + other.re , self.im + other.im)

        else:

            raise TypeError

        

    def __sub__(self, other):

        if isinstance(other, int) or isinstance(other, float):

            return Complex(self.re - other,self.im)

        elif  isinstance(other, Complex):

            return Complex(self.re - other.re, self.im - other.im)

        else:

            raise TypeError

    def __mul__(self, other):

        if isinstance(other, int) or isinstance(other, float):

            return Complex(self.re * other, self.im * other)

        elif isinstance(other, Complex):

        #   (a+bi)*(c+di) = ac + adi +bic -bd

            return Complex(self.re * other.re - self.im * other.im, 

                           self.re * other.im + self.im * other.re)

        else:

            raise TypeError

            

    def __truediv__(self, other):

        if isinstance(other, int) or isinstance(other, float):

            return Complex(self.re / other, self.im / other)

        elif isinstance(other, Complex):

            x = other.re

            y = other.im

            u = self.re

            v = self.im

            repart = 1/(x**2+y**2)*(u*x + v*y)

            impart = 1/(x**2+y**2)*(v*x - u*y)

            return Complex(repart,impart)

        else:

            raise TypeError

    

    def value(self):

        return math.sqrt(self.re**2 + self.im**2)

    

    def __eq__(self, other):

        if isinstance(other, int) or isinstance(other, float):

            return  self.value() == other

        elif  isinstance(other, Complex):

            return  self.value() == other.value()

        else:

            raise TypeError

    

    def __lt__(self, other):

        if isinstance(other, int) or isinstance(other, float):

            return  self.value() < other

        elif  isinstance(other, Complex):

            return  self.value() < other.value()

        else:

            raise TypeError

            

    def __gt__(self, other):

        if isinstance(other, int) or isinstance(other, float):

            return  self.value() > other

        elif  isinstance(other, Complex):

            return  self.value() > other.value()

        else:

            raise TypeError

Now we can use our complex class as:

2. WHY DOES THE COMPLEX NUMBER PRINT AS A RANDOM STRING?

Ahh! You got me. This brings us to another dunder method called __str__ that lets us use the print method on our object. Here, the main idea is again that when we call print(object), it calls the __str__ method in the object. Here is how we can use that method with our complex class.

class Complex:

   def __init__(self, re=0, im=0):

       self.re = re

       self.im = im

   .....

   .....

   def __str__(self):

       if self.im>=0:

           return f"{self.re}+{self.im}i"

       else:

           return f"{self.re}{self.im}i"

We can now recheck the output:

Now our object gets printed in a better way. But still, if we try to do the process in our notebook, the __str__ method is not called:

This happens because we aren’t printing in the above code, and thus the __str__ method doesn’t get called. In this case, another magic method called __repr__ gets called instead. As a result, we can just add this in our class to get the same result as a print. It’s a dunder method inside a dunder method. Pretty nice!

def __repr__(self):

   return self.__str__()

3. IM CATCHING ON. SO, IS THAT HOW THE LEN METHOD WORKS TOO?

len() is another function that works with strings, lists and matrices, among others. To use this function with our complex numbers class, we can use the __len__ magic method, even though this is really not a valid use case for complex numbers as the return type of __len__ needs to be an int, as per the documentation.

class Complex:

   def __init__(self, re=0, im=0):

       self.re = re

       self.im = im

   ......

   ......

   def __len__(self):

       # This function return type needs to be an int

       return int(math.sqrt(self.re**2 + self.im**2))

Here is its usage:

4. BUT WHAT ABOUT THE ASSIGNMENT OPERATIONS?

We know how the + operator works with an object. But have you wondered why the += operator works? For example:

myStr = "This blog"

otherStr = " is awesome"

myStr+=otherStr

print(myStr)

This brings us to another set of dunder methods called assignment methods that include __iadd__, __isub__, __imul__, __itruediv__, and many others.

So, if we just add the method __iadd__ to our class, we would be able to make assignment-based additions too.

class Complex:

   .....

   def __iadd__(self, other):

       if isinstance(other, int) or isinstance(other, float):

           return Complex(self.re + other,self.im)

       elif  isinstance(other, Complex):

           return Complex(self.re + other.re , self.im + other.im)

       else:

           raise TypeError

And use it as:

5. CAN YOUR CLASS OBJECT SUPPORT INDEXING?

Sometimes, objects might contain lists, and we might need to index the object to get the value from the list. To understand this, let’s take a different example. Imagine you work for a company that helps users trade stock. Each user will have a daily transaction book that will contain information about the user’s trades/transactions over the course of the day. We can implement such a class by:

class TrasnsactionBook:

   def __init__(self, user_id, shares=[]):

       self.user_id = user_id

       self.shares = shares

   def add_trade(self, name , quantity, buySell):

       self.shares.append([name,quantity,buySell])

   def __getitem__(self, i):

       return self.shares[i]

Do you notice the __getitem__ here? This actually allows us to use indexing on objects of this particular class using this:

We can get the first trade done by the user or the second one based on the index we use. This is just a simple example, but you can set your object up to get a lot more information when you use indexing.

DON’T FORGET DUNDER METHODS

Python is a magical language, and there are many constructs in Python that even advanced users may not know about. Dunder methods might be very well one of them. I hope with this post, you get a good glimpse of various dunder methods that Python offers and also understand how to implement them yourself.

This article has been published from the source link without modifications to the text. Only the headline has been changed.

Source link

 

Most Popular