Skip to content Skip to footer

11 Python Magic Methods Every Programmer Should Know


11 Python Magic Methods Every Programmer Should Know
Image by Author

 

In Python, magic methods help you emulate the behavior of built-in functions in your Python classes. These methods have leading and trailing double underscores (__), and hence are also called dunder methods.

These magic methods also help you implement operator overloading in Python. You’ve probably seen examples of this. Like using the multiplication operator * with two integers gives the product. While using it with a string and an integer k gives the string repeated k times:

 >>> 3 * 4
12
>>> 'code' * 3
'codecodecode'

 

In this article, we’ll explore magic methods in Python by creating a simple two-dimensional vector Vector2D class.

We’ll start with methods you’re likely familiar with and gradually build up to more helpful magic methods.

Let’s start writing some magic methods! 

 

 

Consider the following Vector2D class:

 

Once you create a class and instantiate an object, you can add attributes like so: obj_name.attribute_name = value.

However, instead of manually adding attributes to every instance that you create (not interesting at all, of course!), you need a way to initialize these attributes when you instantiate an object.

To do so you can define the __init__ method. Let’s define the define the __init__ method for our Vector2D class:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

v = Vector2D(3, 5)

 

 

When you try to inspect or print out the object you instantiated, you’ll see that you don’t get any helpful information. 

v = Vector2D(3, 5)
print(v)

 

Output >>> <__main__.Vector2D object at 0x7d2fcfaf0ac0>

 

This is why you should add a representation string, a string representation of the object. To do so, add a __repr__ method like so:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

v = Vector2D(3, 5)
print(v)

 

Output >>> Vector2D(x=3, y=5)

 

The __repr__ should include all the attributes and information needed to create an instance of the class. The __repr__ method is typically used for the purpose of debugging.

 

 

The __str__ is also used to add a string representation of the object. In general, the __str__ method is used to provide info to the end users of the class.

Let’s add a __str__ method to our class:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

v = Vector2D(3, 5)
print(v)

 

Output >>> Vector2D(x=3, y=5)

 

If there is no implementation of __str__, it falls back to __repr__. So for every class that you create, you should—at the minimum—add a __repr__ method. 

 

 

Next, let’s add a method to check for equality of any two objects of the Vector2D class. Two vector objects are equal if they have identical x and y coordinates.

Now create two Vector2D objects with equal values for both x and y and compare them for equality:

v1 = Vector2D(3, 5)
v2 = Vector2D(3, 5)
print(v1 == v2)

 

The result is False. Because by default the comparison checks for equality of the object IDs in memory.

 

Let’s add the __eq__ method to check for equality:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

 

The equality checks should now work as expected:

v1 = Vector2D(3, 5)
v2 = Vector2D(3, 5)
print(v1 == v2)

 

 

 

Python’s built-in len() function helps you compute the length of built-in iterables. Let’s say, for a vector, length should return the number of elements that the vector contains. 

So let’s add a __len__ method for the Vector2D class:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

    def __len__(self):
        return 2

v = Vector2D(3, 5)
print(len(v))

 

All objects of the Vector2D class are of length 2:

 

 

Now let’s think of common operations we’d perform on vectors. Let’s add magic methods to add and subtract any two vectors.

If you directly try to add two vector objects, you’ll run into errors. So you should add an __add__ method:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

    def __add__(self, other):
        return Vector2D(self.x + other.x, self.y + other.y)

 

You can now add any two vectors like so:

v1 = Vector2D(3, 5)
v2 = Vector2D(1, 2)
result = v1 + v2
print(result)

 

Output >>> Vector2D(x=4, y=7)

 

 

Next, let’s add a __sub__ method to calculate the difference between any two objects of the Vector2D class:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

    def __sub__(self, other):
        return Vector2D(self.x - other.x, self.y - other.y)

 

v1 = Vector2D(3, 5)
v2 = Vector2D(1, 2)
result = v1 - v2
print(result)

 

Output >>> Vector2D(x=2, y=3)

 

 

We can also define a __mul__ method to define multiplication between objects.

Let’s implement let’s handle 

  • Scalar multiplication: the multiplication of a vector by scalar and 
  • Inner product: the dot product of two vectors 
class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

    def __mul__(self, other):
        # Scalar multiplication
        if isinstance(other, (int, float)):
            return Vector2D(self.x * other, self.y * other)
        # Dot product
        elif isinstance(other, Vector2D):
            return self.x * other.x + self.y * other.y
        else:
            raise TypeError("Unsupported operand type for *")

 

Now we’ll take a couple of examples to see the __mul__ method in action.

v1 = Vector2D(3, 5)
v2 = Vector2D(1, 2)

# Scalar multiplication
result1 = v1 * 2
print(result1)  
# Dot product
result2 = v1 * v2
print(result2)

 

Output >>>

Vector2D(x=6, y=10)
13

 

 

The __getitem__ magic method allows you to index into the objects and access attributes or slice of attributes using the familiar square-bracket [ ] syntax.

For an object v of the Vector2D class:

  • v[0]: x coordinate
  • v[1]: y coordinate

If you try accessing by index, you’ll run into errors:

v = Vector2D(3, 5)
print(v[0],v[1])

 

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

TypeError                             	Traceback (most recent call last)

 in ()
----> 1 print(v[0],v[1])

TypeError: 'Vector2D' object is not subscriptable

 

Let’s implement the __getitem__ method: 

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

    def __getitem__(self, key):
        if key == 0:
            return self.x
        elif key == 1:
            return self.y
        else:
            raise IndexError("Index out of range")

 

You can now access the elements using their indexes as shown:

v = Vector2D(3, 5)
print(v[0])  
print(v[1])

 

 

 

With an implementation of the __call__ method, you can call objects as if they were functions. 

In the Vector2D class, we can implement a __call__ to scale a vector by a given factor:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y
 	 
    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

    def __call__(self, scalar):
        return Vector2D(self.x * scalar, self.y * scalar)

 

So if you now call 3, you’ll get the vector scaled by factor of 3:

v = Vector2D(3, 5)
result = v(3)
print(result)

 

Output >>> Vector2D(x=9, y=15)

 

 

The __getattr__ method is used to get the values of specific attributes of the objects.

For this example, we can add a __getattr__ dunder method that gets called to compute the magnitude (L2-norm) of the vector:

class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector2D(x={self.x}, y={self.y})"

    def __getattr__(self, name):
        if name == "magnitude":
            return (self.x ** 2 + self.y ** 2) ** 0.5
        else:
            raise AttributeError(f"'Vector2D' object has no attribute '{name}'")

 

Let’s verify if this works as expected:

v = Vector2D(3, 4)
print(v.magnitude)

 

 

 

That’s all for this tutorial! I hope you learned how to add magic methods to your class to emulate the behavior of built-in functions.

We’ve covered some of the most useful magic methods. But this is not this is not an exhaustive list. To further your understanding, create a Python class of your choice and add magic methods depending on the functionality required. Keep coding!
 
 

Bala Priya C is a developer and technical writer from India. She likes working at the intersection of math, programming, data science, and content creation. Her areas of interest and expertise include DevOps, data science, and natural language processing. She enjoys reading, writing, coding, and coffee! Currently, she’s working on learning and sharing her knowledge with the developer community by authoring tutorials, how-to guides, opinion pieces, and more.





Source link