In [38]:
#https://stackoverflow.com/questions/739654/how-to-make-a-chain-of-function-decorators?rq=1
#https://dbader.org/blog/python-decorators

To understand decorators, you must first understand that:

  1. functions are objects in Python
  2. functions can be defined inside another function

Functions are objects


In [1]:
def shout(word="yes"):
    return word.capitalize()+"!"

In [2]:
shout()


Out[2]:
'Yes!'

In [3]:
# As an object, you can assign the function to a variable like any other object 
scream = shout

In [5]:
# Notice we don't use parentheses: we are not calling the function,
# we are putting the function "shout" into the variable "scream".
# It means you can then call "shout" from "scream"
scream


Out[5]:
<function __main__.shout>

In [6]:
scream()


Out[6]:
'Yes!'

In [9]:
#You can also remove the old name 'shout', and the function
# will still be accessible from 'scream'
del shout
print(shout())


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-9-182ac82bd74c> in <module>()
      1 #You can also remove the old name 'shout', and the function
      2 # will still be accessible from 'scream'
----> 3 del shout
      4 print(shout())

NameError: name 'shout' is not defined

In [8]:
scream()


Out[8]:
'Yes!'

Functions inside another functions


In [10]:
def talk():
    def whisper(word="yes"):
        return word.lower() + '...'
    
    print(whisper())

In [11]:
talk()


yes...

In [12]:
# But "whisper" DOES NOT EXIST outside "talk":
whisper()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-12-45edfa9d85a4> in <module>()
      1 # But "whisper" DOES NOT EXIST outside "talk":
----> 2 whisper()

NameError: name 'whisper' is not defined

Function references

So a function can be:

  1. assigned to a variable
  2. defined in another function

That means that a function can return another function


In [13]:
def getTalk(kind="shout"):

    # We define functions on the fly
    def shout(word="yes"):
        return word.capitalize()+"!"

    def whisper(word="yes") :
        return word.lower()+"...";

    # Then we return one of them
    if kind == "shout":
        # We don't use "()", we are not calling the function,
        # we are returning the function object
        return shout  
    else:
        return whisper

In [14]:
getTalk()


Out[14]:
<function __main__.getTalk.<locals>.shout>

In [15]:
talk = getTalk()

In [16]:
talk


Out[16]:
<function __main__.getTalk.<locals>.shout>

In [17]:
talk()


Out[17]:
'Yes!'

In [20]:
getTalk()("HellO")


Out[20]:
'Hello!'

With getTalk() we return the shout function and then with ("HellO"), we pass the argument at shout function

If you can return a function, you can pass one as a parameter


In [21]:
def doSomethingBefore(func): 
    print("I do something before then I call the function you gave me")
    print(func())

doSomethingBefore(scream)


I do something before then I call the function you gave me
Yes!

Decorator

Decorators are “wrappers”, which means that they let you execute code before and after the function they decorate without modifying the function itself.

Decorators allow you to define reusable building blocks that can change or extend the behavior of other functions. And they let you do that without permanently modifying the wrapped function itself. The function’s behavior changes only when it’s decorated.

In basic terms, a decorator is a callable that takes a callable as input and returns another callable.

The following function has that property and could be considered the simplest decorator one could possibly write:


In [49]:
def null_decorator(func):
    print("I'm decorating")
    return func

def greet():
    return 'Hello!'

In [50]:
greeting= null_decorator(greet)
greeting()


I'm decorating
Out[50]:
'Hello!'

null_decorator is a callable (it’s a function), it takes another callable as its input, and it returns the same input callable without modifying it.


In [51]:
@null_decorator
def greet():
    return 'Hello!'

greet()


I'm decorating
Out[51]:
'Hello!'

More complex decorator

The decorator converts the result of the decoreted functin to uppercase letters


In [53]:
def uppercase(func):
    def wrapper():
        original_result = func()
        modified_result = original_result.upper()
        return modified_result
    return wrapper

In [54]:
@uppercase
def greet():
    return 'Hello!'

In [55]:
greet()


Out[55]:
'HELLO!'

Decorating function that accept Arguments


In [56]:
# Template
def proxy(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

In [57]:
def trace(func):
    def wrapper(*args, **kwargs):
        print(f'TRACE: calling {func.__name__}() '
              f'with {args}, {kwargs}')

        original_result = func(*args, **kwargs)

        print(f'TRACE: {func.__name__}() '
              f'returned {original_result!r}')

        return original_result
    return wrapper

In [58]:
@trace
def say(name, line):
    return f'{name}: {line}'

In [59]:
say('Jane', 'Hello, World')


TRACE: calling say() with ('Jane', 'Hello, World'), {}
TRACE: say() returned 'Jane: Hello, World'
Out[59]:
'Jane: Hello, World'

Example Make a sandwich


In [33]:
def bread(func):
    def wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    return wrapper

def ingredients(func):
    def wrapper():
        print("#tomatoes#")
        func()
        print("~salad~")
    return wrapper

@bread
@ingredients
def sandwich(food="--ham--"):
    print(food)

In [34]:
sandwich()


</''''''\>
#tomatoes#
--ham--
~salad~
<\______/>

Example HTML


In [36]:
# The decorator to make it bold
def makebold(fn):
    # The new function the decorator returns
    def wrapper():
        # Insertion of some code before and after
        return "<b>" + fn() + "</b>"
    return wrapper

# The decorator to make it italic
def makeitalic(fn):
    # The new function the decorator returns
    def wrapper():
        # Insertion of some code before and after
        return "<i>" + fn() + "</i>"
    return wrapper

@makebold
@makeitalic
def say():
    return "hello"

In [37]:
say()


Out[37]:
'<b><i>hello</i></b>'

Other Example


In [39]:
### Hard coding
# A decorator is a function that expects ANOTHER function as parameter
def my_shiny_new_decorator(a_function_to_decorate):

    # Inside, the decorator defines a function on the fly: the wrapper.
    # This function is going to be wrapped around the original function
    # so it can execute code before and after it.
    def the_wrapper_around_the_original_function():

        # Put here the code you want to be executed BEFORE the original function is called
        print("Before the function runs")

        # Call the function here (using parentheses)
        a_function_to_decorate()

        # Put here the code you want to be executed AFTER the original function is called
        print("After the function runs")

    # At this point, "a_function_to_decorate" HAS NEVER BEEN EXECUTED.
    # We return the wrapper function we have just created.
    # The wrapper contains the function and the code to execute before and after. It’s ready to use!
    return the_wrapper_around_the_original_function

# Now imagine you create a function you don't want to ever touch again.
def a_stand_alone_function():
    print("I am a stand alone function, don't you dare modify me")

a_stand_alone_function()
#outputs: I am a stand alone function, don't you dare modify me


I am a stand alone function, don't you dare modify me

In [40]:
# Well, you can decorate it to extend its behavior.
# Just pass it to the decorator, it will wrap it dynamically in 
# any code you want and return you a new function ready to be used:

a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
#outputs:
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs


Before the function runs
I am a stand alone function, don't you dare modify me
After the function runs

In [41]:
@my_shiny_new_decorator
def another_stand_alone_function():
    print("Leave me alone")

In [42]:
another_stand_alone_function()


Before the function runs
Leave me alone
After the function runs

In [ ]: