In Python, the “at symbol” (@) is used in two different contexts:
- 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
- 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))
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:
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)
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)
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())
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
mass = Mass(100) print(mass.pounds) mass.kilos = 500 print(mass.pounds)
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)
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:
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:
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.
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)) ] 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)
[[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.
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.