Have you ever wondered what @property means? Or what A @ B does? I’ll show you how the symbol @ is used in Python.
There are two use cases: decorators and matrix multiplication.
When to Use the @ Symbol in Python
Decorators
The main use case of the symbol @ in Python are decorators. In Python, a decorator extends the functionality of an existing function or class.
For example, this piece of code . . .
def extend_behavior(func):}
return func
@extend_behavior
def some_func():
pass
. . . does the exact same as this piece of code:
def extend_behavior(func):
return func
def some_func():
Pass
some_func = extend_behavior(some_func)
Decorators: A Practical Example
Let’s say you have a function that divides two numbers:
def divide(x, y):
return x / y
The problem with this function is that nothing prevents y
from being 0
.
You could solve this with an if
check. But for the sake of demonstration, let’s patch the issue using a decorator.
Let’s start by creating a decorator function 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 . . .
- accepts
operate()
function as an argument. - extends the function
operate()
by creating an inner function with the extended behavior. - 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:
divide = guard_zero(divide)
This works, but there’s a more conventional approach to applying a decorator:
@guard_zero
def divide(x, y):
return x / y
This makes it more readable and the programmer’s intent becomes clear.
Now you can test the divide()
function with different inputs to see it do what it is supposed to:
print(divide(5, 0))
print(divide(5, 2))
Output:
Cannot divide by 0.
None
2.5
(The None
output originates from the fact that guard_zero()
returns None
when the value y
is 0
.)
Common Decorators
There are many decorators you’ll see, but here are the most common ones.
Common Decorators in Python
- @property
- @classmethod
- @staticmethod
Let’s go through each with an example.
@Property
Tagging a method with @property
makes it possible to access an object’s method like a regular attribute:
weight.pounds() ---> weight.pounds
A property decorator makes the decorated method a getter method.
Example
Say you have a Mass
class that stores the mass in kilos and pounds.
class Mass:
def __init__(self, kilos):
self.kilos = kilos
self.pounds = kilos * 2.205
You can use it by:
mass = Mass(100)
print(mass.kilos)
print(mass.pounds)
Result:
100
220.5
Now, let’s modify the number of kilos
and see what happens to pounds
:
mass.kilos = 1500
print(mass.pounds)
Output:
220.5
The pounds
didn’t change. This happened because we never updated pounds
.
You can fix the issue by replacing the pounds
attribute with a pounds()
method:
class Mass:
def __init__(self, kilos):
self.kilos = kilos
def pounds(self):
return self.kilos * 2.205
Now you may modify the number of kilos
and the pounds
will be correct:
mass = Mass(100)
print(mass.pounds())
mass.kilos = 500
print(mass.pounds())
Output:
220.5
1102.5
Now you can’t call mass.pounds
anymore because pounds()
is a method, not an attribute. If the Mass
class is used somewhere else, the above change breaks the code.
This is where a @property
decorator helps you.
If you mark the pounds()
method with @property
it allows you to call mass.pounds
without parentheses again.
class Mass:
def __init__(self, kilos):
self.kilos = kilos
@property
def pounds(self):
return self.kilos * 2.205
The pounds()
method is now called a getter method. It doesn’t store the number of pounds in the object but computes it by converting kilos to pounds.
@Classmethod
A class method is useful when you need a method that involves the class but 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 take care of converting 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.
Wouldn’t something like this be more efficient?
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)
Let’s see how this class method works:
- The
@classmethod
marks thefrom_pounds()
as a class method. - The first argument
cls
is a mandatory argument on a class method. It’s similar to self in a method. The difference is thatcls
represents the wholeWeight
class, not just an instance of it. - In the method, the
pounds
are converted tokilos
. - The return
cls(kilos)
is the same asreturn Weight(kilos)
.
Simply put, this class method takes a number of pounds
as an argument, converts it to kilos
and returns a new Weight
object.
Now it’s possible to do:
w2 = Weight.from_pounds(220.5)
print(w2.kilos)
Output:
100
@Staticmethod
A static method is tied to the class, not to its instance. This may remind you of a class method but the key difference is that a static method doesn’t modify the class at all. In other words, a static method doesn’t take self
or cls
as its arguments.
We most often use a static method as a utility related to the class.
Let’s continue with the Weight
class. Start by adding 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 use this utility to print how the conversion happens:
Weight.conversion_info()
Result:
Kilos are converted to pounds by multiplying by 2.205.
Matrix Multiplication
Since Python 3.5, it’s been possible to use @
to multiply matrices.
For example, let’s create a matrix class, and implement the __matmul__()
method 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))])
A = Matrix([[1, 2],[3, 4]])
B = Matrix([[5, 6],[7, 8]])
print(A @ B)
Output:
[[19, 22], [43, 50]]
There you have it: the @
symbol in Python and how you can use it to clean up your code. Happy coding!