Python Decorators
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.
Video: @Decorators in Python
Decorators in Python
Python has an interesting feature called decorators to add functionality to an
existing code.
This is also called metaprogramming because a part of the program tries to
modify another part of the program at compile time.
Prerequisites for learning decorators
In order to understand about decorators, we must first know a few basic things
in Python.
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.
Functions can be passed as arguments to another function.
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 operate(func, x):
result = func(x)
return result
Run Code
We invoke the function as follows.
>>> operate(inc,3)
4
>>> operate(dec,3)
2
Furthermore, a function can return another function.
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.
Getting back to Decorators
Functions and methods are called callable as they can be called.
In fact, any object which implements the special __call__() method is termed
callable. So, in the most basic sense, a decorator is a callable that returns a
callable.
Basically, a decorator takes in a function, adds some functionality and returns
it.
def make_pretty(func):
def inner():
print("I got decorated")
func()
return inner
def ordinary():
print("I am ordinary")
Run Code
When you run the following codes in shell,
>>> ordinary()
I am ordinary
>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary
In the example shown above, make_pretty() is a decorator. In the assignment
step:
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).
Generally, we decorate a function and reassign it as,
ordinary = make_pretty(ordinary).
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 is just a syntactic sugar to implement decorators.
Decorating Functions with Parameters
The above decorator was simple and it only worked with functions that did not
have any parameters. What if we had functions that took in parameters like:
def divide(a, b):
return a/b
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
In this manner, we can decorate functions that take parameters.
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
Chaining Decorators in Python
Multiple decorators can be chained in Python.
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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
The above syntax of,
@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
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Python @property decorator
In this tutorial, you will learn about Python @property decorator; a pythonic
way to use getters and setters in object-oriented programming.
Python programming provides us with a built-in @property decorator which
makes usage of getter and setters much easier in Object-Oriented
Programming.
Before going into details on what @property decorator is, let us first build an
intuition on why it would be needed in the first place.
Class Without Getters and Setters
Let us assume that we decide to make a class that stores the temperature in
degrees Celsius. It would also implement a method to convert the temperature
into degrees Fahrenheit. One way of doing this is as follows:
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
# Create a new object
human = Celsius()
# Set the temperature
human.temperature = 37
# Get the temperature attribute
print(human.temperature)
# Get the to_fahrenheit method
print(human.to_fahrenheit())
Run Code
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}
Therefore, man.temperature internally becomes man.__dict__['temperature'] .
Using Getters and Setters
Suppose we want to extend the usability of the Celsius class defined above.
We know that the temperature of any object cannot reach below -273.15
degrees Celsius (Absolute Zero in Thermodynamics)
Let's update our code to implement this value constraint.
An obvious solution to the above restriction will be to hide the
attribute temperature (make it private) and define new getter and setter
methods to manipulate it. This can be done as follows:
# Making Getters and Setter methods
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)
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
As we can see, the above method introduces two
new get_temperature() and set_temperature() methods.
Furthermore, temperature was replaced with _temperature . An underscore _ at
the beginning is used to denote private variables in Python.
Now, let's use this implementation:
# Making Getters and Setter methods
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)
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
# Create a new object, set_temperature() internally called by __init__
human = Celsius(37)
# Get the temperature attribute via a getter
print(human.get_temperature())
# Get the to_fahrenheit method, get_temperature() called by the method itself
print(human.to_fahrenheit())
# new constraint implementation
human.set_temperature(-300)
# Get the to_fahreheit method
print(human.to_fahrenheit())
Run Code
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.
This update successfully implemented the new restriction. We are no longer
allowed to set the temperature below -273.15 degrees Celsius.
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.
>>> human._temperature = -300
>>> human.get_temperature()
-300
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) .
This refactoring can cause problems while dealing with hundreds of
thousands of lines of codes.
All in all, our new update was not backwards compatible. This is
where @property comes to rescue.
The property Class
A pythonic way to deal with the above problem is to use the property class.
Here is how we can update our code:
# using property class
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
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
# creating a property object
temperature = property(get_temperature, set_temperature)
We added a print() function inside get_temperature() and set_temperature() to
clearly observe that they are being executed.
The last line of the code makes a property object temperature . Simply put,
property attaches some code ( get_temperature and set_temperature ) to the
member attribute accesses ( temperature ).
Let's use this update code:
# using property class
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
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
# creating a property object
temperature = property(get_temperature, set_temperature)
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 = Celsius(37)
Setting value...
Can you guess why?
The reason is that when an object is created, the __init__() method gets
called. This method has the line self.temperature = temperature . This
expression automatically calls set_temperature() .
Similarly, any access like c.temperature automatically calls get_temperature() .
This is what property does. Here are a few more examples.
>>> human.temperature
Getting value
37
>>> human.temperature = 37
Setting value
>>> c.to_fahrenheit()
Getting value
98.60000000000001
By using property , we can see that no modification is required in the
implementation of the value constraint. Thus, our implementation is backward
compatible.
Note: The actual temperature value is stored in the
private _temperature variable. The temperature attribute is a property object
which provides an interface to this private variable.
The @property Decorator
In Python, property() is a built-in function that creates and returns
a property object. The syntax of this function is:
property(fget=None, fset=None, fdel=None, doc=None)
where,
fget is function to get value of the attribute
fset is function to set value of the attribute
fdel is function to delete the attribute
doc is a string (like a comment)
As seen from the implementation, these function arguments are optional. So,
a property object can simply be created as follows.
>>> 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)
can be broken down as:
# make empty property
temperature = property()
# assign fget
temperature = temperature.getter(get_temperature)
# assign fset
temperature = temperature.setter(set_temperature)
These two pieces of codes are equivalent.
Programmers familiar with Python Decorators can recognize that the above
construct can be implemented as decorators.
We can even not define the names get_temperature and set_temperature as they
are unnecessary and pollute the class namespace.
For this, we reuse the temperature name while defining our getter and setter
functions. Let's look at how to implement this as a decorator:
# Using @property decorator
class Celsius:
def __init__(self, temperature=0):
self.temperature = 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
The above implementation is simple and efficient. It is the recommended way
to use property .