Video: @decorators in Python Decorators in Python
Video: @decorators in Python Decorators in Python
Video: @decorators in Python Decorators in Python
A decorator takes in a function, adds some functionality and returns it. In this
tutorial, you will learn how you can create a decorator and why you should use
it.
We must be comfortable with the fact that everything in Python (Yes! Even
classes), are objects. Names that we define are simply identifiers bound to
these objects. Functions are no exceptions, they are objects too (with
attributes). Various different names can be bound to the same function object.
Here is an example.
def first(msg):
print(msg)
first("Hello")
second = first
second("Hello")
Run Code
Output
Hello
Hello
When you run the code, both functions first and second give the same output.
Here, the names first and second refer to the same function object.
Now things start getting weirder.
If you have used functions like map , filter and reduce in Python, then you
already know about this.
Such functions that take other functions as arguments are also called higher
order functions. Here is an example of such a function.
def inc(x):
return x + 1
def dec(x):
return x - 1
def is_called():
def is_returned():
print("Hello")
return is_returned
new = is_called()
# Outputs "Hello"
new()
Run Code
Output
Hello
Here, is_returned() is a nested function which is defined and returned each
time we call is_called() .
Finally, we must know about Closures in Python.
def make_pretty(func):
def inner():
print("I got decorated")
func()
return inner
def ordinary():
print("I am ordinary")
Run Code
>>> ordinary()
I am ordinary
pretty = make_pretty(ordinary)
The function ordinary() got decorated and the returned function was given the
name pretty .
We can see that the decorator function added some new functionality to the
original function. This is similar to packing a gift. The decorator acts as a
wrapper. The nature of the object that got decorated (actual gift inside) does
not alter. But now, it looks pretty (since it got decorated).
This is a common construct and for this reason, Python has a syntax to
simplify this.
We can use the @ symbol along with the name of the decorator function and
place it above the definition of the function to be decorated. For example,
@make_pretty
def ordinary():
print("I am ordinary")
is equivalent to
def ordinary():
print("I am ordinary")
ordinary = make_pretty(ordinary)
This function has two parameters, a and b . We know it will give an error if we
pass in b as 0.
>>> divide(2,5)
0.4
>>> divide(2,0)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Now let's make a decorator to check for this case that will cause the error.
def smart_divide(func):
def inner(a, b):
print("I am going to divide", a, "and", b)
if b == 0:
print("Whoops! cannot divide")
return
return func(a, b)
return inner
@smart_divide
def divide(a, b):
print(a/b)
Run Code
This new implementation will return None if the error condition arises.
>>> divide(2,5)
I am going to divide 2 and 5
0.4
>>> divide(2,0)
I am going to divide 2 and 0
Whoops! cannot divide
A keen observer will notice that parameters of the nested inner() function
inside the decorator is the same as the parameters of functions it decorates.
Taking this into account, now we can make general decorators that work with
any number of parameters.
In Python, this magic is done as function(*args, **kwargs) . In this way, args will
be the tuple of positional arguments and kwargs will be the dictionary of
keyword arguments. An example of such a decorator will be:
def works_for_all(func):
def inner(*args, **kwargs):
print("I can decorate any function")
return func(*args, **kwargs)
return inner
This is to say, a function can be decorated multiple times with different (or
same) decorators. We simply place the decorators above the desired function.
def star(func):
def inner(*args, **kwargs):
print("*" * 30)
func(*args, **kwargs)
print("*" * 30)
return inner
def percent(func):
def inner(*args, **kwargs):
print("%" * 30)
func(*args, **kwargs)
print("%" * 30)
return inner
@star
@percent
def printer(msg):
print(msg)
printer("Hello")
Run Code
Output
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
@star
@percent
def printer(msg):
print(msg)
is equivalent to
def printer(msg):
print(msg)
printer = star(percent(printer))
The order in which we chain decorators matter. If we had reversed the order
as,
@percent
@star
def printer(msg):
print(msg)
The output would be:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
Hello
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
class Celsius:
def __init__(self, temperature = 0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
We can make objects out of this class and manipulate the temperature attribute
as we wish:
# Basic method of setting and getting attributes in Python
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
Output
37
98.60000000000001
The extra decimal places when converting into Fahrenheit is due to the
floating point arithmetic error. To learn more, visit Python Floating Point
Arithmetic Error.
Whenever we assign or retrieve any object attribute like temperature as shown
above, Python searches it in the object's built-in __dict__ dictionary attribute.
>>> human.__dict__
{'temperature': 37}
def to_fahrenheit(self):
return (self.get_temperature() * 1.8) + 32
# getter method
def get_temperature(self):
return self._temperature
# setter method
def set_temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible.")
self._temperature = value
def to_fahrenheit(self):
return (self.get_temperature() * 1.8) + 32
# getter method
def get_temperature(self):
return self._temperature
# setter method
def set_temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible.")
self._temperature = value
Output
37
98.60000000000001
Traceback (most recent call last):
File "<string>", line 30, in <module>
File "<string>", line 16, in set_temperature
ValueError: Temperature below -273.15 is not possible.
Note: The private variables don't actually exist in Python. There are simply
norms to be followed. The language itself doesn't apply any restrictions.
However, the bigger problem with the above update is that all the programs
that implemented our previous class have to modify their code
from obj.temperature to obj.get_temperature() and all expressions
like obj.temperature = val to obj.set_temperature(val) .
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
# getter
def get_temperature(self):
print("Getting value...")
return self._temperature
# setter
def set_temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible")
self._temperature = value
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
# getter
def get_temperature(self):
print("Getting value...")
return self._temperature
# setter
def set_temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible")
self._temperature = value
human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())
human.temperature = -300
Run Code
Output
Setting value...
Getting value...
37
Getting value...
98.60000000000001
Setting value...
Traceback (most recent call last):
File "<string>", line 31, in <module>
File "<string>", line 18, in set_temperature
ValueError: Temperature below -273 is not possible
As we can see, any code that retrieves the value of temperature will
automatically call get_temperature() instead of a dictionary (__dict__) look-up.
Similarly, any code that assigns a value to temperature will automatically
call set_temperature() .
We can even see above that set_temperature() was called even when we
created an object.
>>> human.temperature
Getting value
37
>>> human.temperature = 37
Setting value
>>> c.to_fahrenheit()
Getting value
98.60000000000001
where,
>>> property()
<property object at 0x0000000003239B38>
A property object has three methods, getter() , setter() , and deleter() to
specify fget , fset and fdel at a later point. This means, the line:
temperature = property(get_temperature,set_temperature)
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
@property
def temperature(self):
print("Getting value...")
return self._temperature
@temperature.setter
def temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273 is not possible")
self._temperature = value
# create an object
human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())
coldest_thing = Celsius(-300)
Run Code
Output
Setting value...
Getting value...
37
Getting value...
98.60000000000001
Setting value...
Traceback (most recent call last):
File "", line 29, in
File "", line 4, in __init__
File "", line 18, in temperature
ValueError: Temperature below -273 is not possible