Understanding Python Dunder (Magic) Methods

Python Dunder or magic methods are special methods that are built-in to Python that can be used to enhance your class functionality. Here the name Dunder is used to mean double underscore, as the methods start and end with a double underscore.

Arguably the most prominent Dunder method is the __init__ method. This is the method which is responsible for object construction and is always called on object instantiation, allowing you to control how an object is instantiated.

Python offers support to overload all of its operators. This allows support for any kind of functionality you may wish to extend your classes with. This can be as simple as what is returned when you attempt to print an object, or as involved as altering how your class works with complex numbers. Below is a non-exhaustive list of Python operators with their Dunder method counterparts.

float() __float__
abs() __abs__
+ __add__
__sub__
*= __imul__
//= __ifloordiv__
< __lt__
>= __ge__

There is even support for which side of the operation the object is used with. For example, if we wish to multiply an object by something else (object * x) we use the __mul__ method. However, if we also need to control how the object behaves if we reverse the order of the multiplication (x * object) we would use the __rmul__ method.

Dunders in Action

To explain this concept, let’s consider a class for counting values added to it. This class should allow us to do a few things. First, it should keep track of values added to it in a list. Secondly, the length operator should be overloaded to return the number of items added to the list. Finally, the string method should be overloaded so that printing an instace of the class returns information on what values were added to it.

To start with lets initialize the class using the __init__ method. Here we will create an empty list to keep track of the values added to the object.

class Counter:
    def __init__(self):
        self.values = []

Now lets overload the length method, so instead of throwing an error the object will return the length of the list containing the values.

def __len__(self):
    return len(self.values)

Next lets overload the string method, so we can return information from the object. Here we’ll return the list of values, the number of values in the list, and the sum of the values. Note that when getting the number of elements in the list we use the previously overloaded method len().

def __str__(self):
    counter_sum = sum(self.values)
    return f"Values: {self.values}\nNum Values: {len(self)}\nTotal: {counter_sum}"

If we create an instance of this and print it we get the following:

counter = Counter()
print(counter)

>>> Values: []
>>> Num Values: 0
>>> Total: 0

Now that we have printed output in place, let’s add methods to allow values to be added and subtracted to the total. Here we will use the __iadd__ and __isub__ methods. The i prefix stands for in-place, meaning that it overloads the += and -= operators. If you wish to overload the standard add and subtract, __add__ and __sub__ should be used.

Putting It All Together

We will use these operators to add positive and negative values to the values list, within the __isub__ method. We will multiply the value with -1 to store it as negative within the list. The following is the completed class:

class Counter:
    def __init__(self):
        self.values = []

    def __len__(self):
        return len(self.values)

    def __iadd__(self, other):
        self.values.append(other)
        return self

    def __isub__(self, other):
        self.values.append(-1*other)
        return self

    def __str__(self):
        counter_sum = sum(self.values)
        return f"Values: {self.values}\nNum Values: {len(self)}\nTotal: {counter_sum}"

Now, lets see the code in action. First, we will create a counter object and add positive and negative values to it.

counter = Counter()

counter += 2
counter += 3
counter += 5
counter -= 4

print(counter)

>>> Values: [2, 3, 5, -4]
>>> Num Values: 4
>>> Total: 6

A Real Use Case

A great example of a use case is the mathematical concepts of vectors and matrices. Standard mathematical operations (such as addition and subtraction) work differently on these concepts. Classes created for these operations can use Dunder methods to overload mathematical operations to handle them correctly. Further to this, matrices handle certain operations (such as multiplication) differently depending on if they are multiplied by a matrix, vector or scalar. In this case, the multiplication operator can be overloaded and can correctly handle the multiplication based on what the matrix is multiplied by.

In fact, this is exactly what I did when I was first teaching myself Dunder methods. I wrote a library that can handle vectors, matrices and quaternary and their many interactions. If you’re mathematically minded and trying to learn how Dunder methods work I highly recommend this path, the classes can start off extremely simple and can be extended as you go to include increased functionality. My library named Pyclid can be found here.

Conclusion

Dunders are a great way to enhance the functionality of your classes and decrease the complexity of their use. However, overuse and unnecessary use will cause your classes to become over-complex and difficult to implement and debug.

1
0

Related Posts