Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Video: @decorators in Python Decorators in Python

Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 19

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 .

You might also like