Symbol @ in Python

@symbol in Python

In Python, the “at symbol” (@) is used in two different contexts:

  • Decorators.
  • Matrix multiplication.

Let’s take a more in-depth look at both of these.

Decorators in Python

The main use case of the symbol @ in Python is decorators.

In Python, a decorator is a function that extends the functionality of an existing function or class.

Here is a demonstration of how a decorator works in Python:

def extend_behavior(func):
   # Create an updated version of 'func'
   return func

@extend_behavior
def some_func():
    pass

Here the extend_behavior decorator function extends the behavior of some_func. (However, in this simplified example, the extend_behavior only returns the original version of some_func.)

Notice how syntactically it looks beautiful and the intention of extending a function is clear.

However, it is important to notice how the above code works this way under the hood:

def extend_behavior(func):
    return func

def some_func():
    pass

some_func = extend_behavior(some_func)

So whenever you use a decorator, you actually pass a function into another function that returns a new version of it. Then you assign the new function to the original one.

A Practical Example of Decorators in Python

Let’s say you have a function that divides two numbers x and y:

def divide(x, y):
    return x / y

The problem with this function is nothing prevents y from being 0.

You could obviously solve this problem with an if check. But for the sake of demonstration, let’s fix the issue using a decorator.

Let’s create a decorator called guard_zero():

def guard_zero(operate):
    def inner(x, y):
        if y == 0:
            print("Cannot divide by 0.")
            return
        return operate(x, y)
    return inner

This decorator:

  • Takes a function operate() as an argument. This is the function that we are extending.
  • Extends the function operate() by creating an inner function with the extended behavior.
  • It returns the inner() function—a new version of operate() function.

Now you can use the decorator guard_zero() to extend the behavior of divide() to ensure no divisions with 0 are made:

@guard_zero
def divide(x, y):
    return x / y

Let’s now test our original divide function:

print(divide(5, 0))
print(divide(5, 2))

Output:

Cannot divide by 0.
None
2.5

As you can see, the updated version of the divide function now makes sure no divisions are made by 0.

(In case you are wondering, the None output is caused by the guard_zero decorator returning None when y is 0.)

Cool! Now you know what is a decorator function in Python and how you can use one. Next, let’s take a look at some common built-in decorators you are going to see.

Useful Decorators in Python

There’s a lot of decorators in Python you are going to see, but the most common ones are:

  • @property
  • @classmethod
  • @staticmethod

Let’s go through each with an example.

@property Decorator in Python

When you mark a method with @property in Python, it is possible to call that method as if it was a property of that class. In other words, you don’t need to use parenthesis to call the method:

weight.pounds() ---> weight.pounds

A method marked as @property is also known as a getter method.

Let’s see an example.

Say you have a class called Mass that stores weight both in kilos and pounds:

class Mass:
    def __init__(self, kilos):
        self.kilos = kilos
        self.pounds = kilos * 2.205

You can use this class by:

mass = Mass(100)

print(mass.kilos)
print(mass.pounds)

Output:

100
220.5

But now, let’s modify the number of kilos of the mass object. Pay attention to what happens to the pounds property.

mass.kilos = 10000000
print(mass.pounds)

Output:

220.5

Yup, it remains the same that it started. But this is not a surprise as we did not modify the pounds. We only touched kilos.

One way to fix the problem is to not store pounds as a separate variable in the object. Instead, you can write a method that converts the current number of kilos to pounds:

class Mass:
    def __init__(self, kilos):
        self.kilos = kilos
            
    def pounds(self):
        return self.kilos * 2.205

Now let’s see when we create a Mass object and update its weight in kilograms:

mass = Mass(100)
print(mass.pounds())

mass.kilos = 500
print(mass.pounds())

Output:

220.5
1102.5

Now the weight is always correct in both pounds and kilos.

But as a consequence, you can no longer call mass.pounds without parenthesis, as pounds() is now a method. This change breaks the code if you still call mass.pounds somewhere.

This is where the @property decorator helps you.

If you mark the pounds() method with @property decorator, you can call mass.pounds without parenthesis again:

class Mass:
    def __init__(self, kilos):
        self.kilos = kilos
        
    @property
    def pounds(self):
        return self.kilos * 2.205

Example call:

mass = Mass(100)
print(mass.pounds)

mass.kilos = 500
print(mass.pounds)

Output:

220.5
1102.5

The decorated pounds() method is now called a getter method. It does not store the number of pounds in the object while syntactically it looks as if it did.

@classmethod Decorator in Python

A class method is useful when you need a method that involves the class and isn’t instance-specific.

For example, you can create an alternative initializer method for a class by using a class method.

To create a class method in Python, decorate it with @classmethod.

Use Class Method as an Alternative Constructor

Let’s say you have a Weight class that instantiates weight objects with kilos:

class Weight:
    def __init__(self, kilos):
        self.kilos = kilos

You can create Weight objects like this:

w1 = Weight(100)

But if you want to create a Weight object from pounds, you have to convert pounds to kilos in advance:

pounds = 220.5
kilos = pounds / 2.205

w2 = Weight(kilos)
print(w2.kilos)

Output:

100

So you always need to remember to convert pounds to kilos before creating a Weight. This is bad practice.

What if you could create a Weight object from pounds directly like this?

w2 = Weight.from_pounds(500)

To do this, let’s create a class method from_pounds() that acts as an alternative constructor or a “second initializer”:

class Weight:
    def __init__(self, kilos):
        self.kilos = kilos
    
    @classmethod
    def from_pounds(cls, pounds):
        # convert pounds to kilos
        kilos = pounds / 2.205
        # cls is the same as Weight. calling cls(kilos) is the same as Weight(kilos)
        return cls(kilos)

Now it is indeed possible to create a Weight object directly from pounds using the second initializer:

w2 = Weight.from_pounds(220.5)
print(w2.kilos)

And the number of kilos is also automatically correct:

100

Let’s take a look at how the from_pounds class method works more in-depth:

  • The @classmethod marks the from_pounds() as a classmethod.
  • The first argument cls is a mandatory argument. It’s similar to self in a regular method. The difference is that cls represents the whole Weight class, not an instance of it.
  • Inside the actual from_pounds() method, the pounds are converted to kilos and a new Weight object is returned.
  • (Here the return cls(kilos) is the same as return Weight(kilos).)

So the class method takes a number of pounds as an argument, converts it to kilos, and returns a new Weight object.

@staticmethod Decorator in Python

A static method is tied to the class, not to the instance of it.

A static method reminds a class method. But the key difference is that a static method doesn’t modify the class at all. This means a static method does not accept self nor cls as arguments either.

A static method is commonly used as a utility related to the class.

For instance, let’s continue with the Weight class. Let’s add a static method conversion_info() to tell how kilos are converted to pounds:

class Weight:
    def __init__(self, kilos):
        self.kilos = kilos
    
    @classmethod
    def from_pounds(cls, pounds):
        # convert pounds to kilos
        kilos = pounds / 2.205
        # cls is the same as Weight. calling cls(kilos) is the same as Weight(kilos)
        return cls(kilos)
    
    @staticmethod
    def conversion_info():
        print("Kilos are converted to pounds by multiplying by 2.205.")

Now you can call this utility method:

Weight.conversion_info()

Output:

Kilos are converted to pounds by multiplying by 2.205.

The point of a static method is that no matter what values the object has, it always produces the same result. For example, the conversion factor 2.205 is the same regardless of the number of kilos.

Remember you can also accept arguments in static methods if needed.

Matrix Multiplication

Since Python 3.5, it’s been possible to use @ symbol to multiply matrixes.

This of course means you need to have a Matrix class that implements the matrix multiplication algorithm in the __matmul__ method.

For example, let’s create a Matrix class, and implement the __matmul__ for matrix multiplication:

class Matrix(list):
    def __matmul__(self, B):
        A = self
        return Matrix([[sum(A[i][k] * B[k][j] for k in range(len(B)))
                    for j in range(len(B[0])) ] for i in range(len(A))])

# Example matrices
A = Matrix([[1, 2],[3, 4]])
B = Matrix([[5, 6],[7, 8]])


# Example of multiplying matrices
print(A @ B)

Output:

[[19, 22], [43, 50]]

As the point of this guide is to show how to use @ in Python, I’m not going into further details about matrix multiplication. In case you are interested to learn more about matrix multiplication, check this article that explains the algorithm.

Conclusion

Today you learned how the at symbol @ is used in Python.

To recap, the main use for the @ symbol is decorators. A decorator is used to extend the functionality of a function, method, or class from outside.

Common decorators you are going to see are:

  • @property. Turns a method in a class into a getter method that can be called without parenthesis.
  • @classmethod. Turns a method in a class to a classmethod that is not tied to an object, but to the class instead. This is commonly used to create a second initializer to a class.
  • @staticmethod. A class-independent method. Does not modify the class objects but is related to the class in a way that makes sense to add it ther.

Another use case for @ symbol is matrix multiplication. To support this, you need to implement a Matrix class with the __matmul__ method.

Thanks for reading.

Happy coding!

Further Reading

50 Python Interview Questions

Share on facebook
Share on twitter
Share on linkedin

Leave a Comment

Your email address will not be published.