Getting Started with Decorators in Python

To get started with Decorators , First we have to understand few things :-

  1. everything we see in python is an object.
  2. function in python is also an object.
  3. we can declare function within a function.
  4. these inner function have access to local variables in th e enclosing function.
  5. a function can return any value, including a function

to understand the above point let us take an example

Example :-

>>> def hello(name):
...     return f"Hello {name}"
...
>>> a = hello
>>> # as we can see variable a referencing to a function object
>>> a("Ram")
'Hello Ram'
>>>

with the above example we can,

  • functions are objects
  • they can be assigned/reference by other name ,such as variable a

let's take another example to clear other remaining
points

>>> def foo(name):
...     def bar():
...             print(f"Hey I'm the inner function bar enclosed inside foo function , {name}")
...     return bar
...
>>> f = foo("Ram")
>>> f.__name__
'bar'
>>> type(f)
<class 'function'>
>>> f()
Hey I'm the inner function bar enclosed inside foo function , Ram
>>>

In the above example we have declared a function foo which contains another function bar, and when we called the outer function foo we get back reference to bar function object,
we also verified the same by,

  • checking f.name attribute which return name of the function
  • checked type(f) to see that it is of type function

  • Inner function bar has access to all varibles of enclosing function like variable name

The concepts that we have covered in the above example
will help in the understanding of Decorators

Now we see some examples of special type of decorator

Function Wrapper

Wrappers around the functions are also knows as decorators which are a very powerful and useful tool in Python since it allows programmers to modify the behavior of function or class.

Let's understand with an example

Example :-

lines = '-' * 60 + '\n'

def with_lines(func):
    def wrapper():
        return f'{lines}{func()}{lines}'
    return wrapper

def a():
    return f'I am in a!\n'
new_a = with_lines(a)


def b():
    return f'I am in b!\n'
new_b = with_lines(b)

print(a())
print(b())
print(new_a())
print(new_b())

Output :-
I am in a!

I am in b!

------------------------------------------------------------
I am in a!
------------------------------------------------------------

-----------------------------------------------------------------
I am in b!
-----------------------------------------------------------------

In the above example we created a with_lines function which takes func (callable object ) as and argument and with_lines function has an inner function wrapper
which has access to the outer function variable,
So we call the function passed by outer function inside the wrapper function by adding global varible lines at the start and end and return wrapper function object.

With this now we have a with_lines function wrapper which can we used with any function to change behiviour of the function,

As we used it with new_a and new_b functions that have some formating added to them using with_lines wrapper function.

Wrapper are one of the most powerful tools of python which can we used to change or added some extra behiviour to the existing function.

Now we have gathered enough information to understand Decorators.

What is a Decorator?

In simple terms :-

  • A function
  • that takes function as an argument
  • and return a function as its output
  • Because of the @ syntax in Python, decorating a function replaces that function with the result of calling the decorator on the function.

What can I use a Decorator For?

  • Anything that is repeated across multiple functions
  • Grab / modify / filter input arguments
  • Grab / modify / filter outputs
  • Change the state
  • Stop a function from being called under certain conditions
  • Choose a different function to be called instead

Now we will understand decorators with couple of examples

Example :-

lines = '-' * 60 + '\n'

def with_lines(func):
    def wrapper(*args, **kwargs):
        return f'{lines}{func(*args, **kwargs)}{lines}'
    return wrapper

@with_lines
def a():
    return f'I am in a!\n'
# a = with_lines(a)   # this line (after) is the same as @with_lines (before)


@with_lines
def b():
    return f'I am in b!\n'
# b = with_lines(b)  # this line (after) is the same as line 14

@with_lines
def add(x, y):
    return f'{x} + {y} = {x+y}\n'

print(a())
print(b())
print(add(3,5))  # add is actually wrapper! so when I call add(3,5), it's saying wrapper(3,5)

OUTPUT :- 

-----------------------------------------------------------------
I am in a!
-----------------------------------------------------------------

-----------------------------------------------------------------
I am in b!
-----------------------------------------------------------------

-----------------------------------------------------------------
3 + 5 = 8
-----------------------------------------------------------------

Using the same with_lines wrapper function used in the above example we added this decorator with the existing method a() ,b() and add() method using using @ Symbol which replaces that function with the result of calling the decorator on the function.

Let's take another example to see how we can filter the input using decorator :-
We have given a list of int from which we have to all numbers which are only even, let's see how we can do it using decorator

Example :-

# Wrapper that filter even from list of ints
def only_evens(func):
    def wrapper(*args):
        even_numbers = [num for num in args if num % 2 == 0]
        print("\n\nWrapper added")
        print(f"even numbers {even_numbers}")
        result = f"sum of even numbers :- {func(*even_numbers)}"
        return result

    return wrapper



def add_nums(*numbers):
    sum = 0
    for num in numbers:
        sum += num
    return sum


print("Original add_nums function output :-",add_nums(1,2,3,4,5,6,7,8,9,10))

@only_evens
def add_nums(*numbers):
    sum = 0
    for num in numbers:
        sum += num
    return sum


print("Original add_nums function output :-",add_nums(1,2,3,4,5,6,7,8,9,10))

Output :-
Original add_nums function output :- 55


Wrapper added
even numbers [2, 4, 6, 8, 10]
Original add_nums function output :- sum of even numbers :- 30

In the above example
first we called the orignal method add_nums() which add all the numbers.
then we declared the add_nums() function again but this time
added @only_evens wrapper to it , the wrapper only_evens filter the list of values first then the filters values passed to the original method add_nums() which return the sum of only even values.

with this example we have come to an end,
hope you all have learn some basis of decorators
this topic , at first is not easy to grasp,
but as you explore more about decorators
you will know how useful it is.
See you next time :-)

19