Python's Decorators

Decorators of decorators of decorators... Wait! Where was I already?

Functions Are First Class Objects

Functions are like any objects in Python. You can:

  • Reassign them
def hello_world():
    print("Hello, World!")

hello = hello_world
hello() # Hello, World!
  • Use them as arguments
def hello_world():
    print("Hello, World")

def greetings(func):
    func()

greetings(hello_world) # Hello, World!
  • Define them inside other functions
def hello_world():
    def hello():
        return 'Hello,'
    def world():
        return 'World!'
    print(hello(), world())

hello_world() # Hello, World!
  • Return them
def hello_world():
    def hello():
        return 'Hello, World!'
    return hello

print(hello_world()) # Hello, World!

Decorating Functions

You decorate a function when you want to improve it without changing its core behaviour. See it like a cake you’re baking. First you make the biscuit and then you put things on top of it like frosting. The savor of the biscuit doesn’t change but you have enhanced it with your toping. It’s exactly the same.

def biscuit(savor):
    print("The biscuit is made of {}.".format(savor))

def frosting(biscuit):
    def wrapper(savor):
        print('Some frosting on top of the biscuit.')
        biscuit(savor)
    return wrapper

frosted_biscuit = frosting(biscuit)
frosted_biscuit('chocolate')
# Some frosting on top of the biscuit
# The biscuit is made of chocolate.

We define a function wrapper inside frosting who decorates our biscuit. Then we return it to get the decorated version and name it frosted_biscuit.

You can do the same for some content you want to put inside a html tag:

def fetch_quote()
    return 'Alea jacta est', 'Caesar'

def quote_to_html(quote):
    def wrapper():
        html = '<blockquote>{}<footer>— {}</footer></blockquote>'
        return html.format(*fetch_quote())
    return wrapper

formatted_quote = quote_to_html(fetch_quote)
formatted_quote()
# <blockquote>Alea jacta est<footer>— Caesar</footer></blockquote>

The current fetch_quote function is not interesting. Yet if instead of returning a simple tuple you make an actual call to an API, it become way more useful than it is now.

Syntactic Sugar

Python provide something a bit nicer for the eye. Let’s take our biscuit example from before:

def biscuit(savor):
    print("The biscuit is made of {}.".format(savor))

def frosting(biscuit):
    def wrapper(savor):
        print('Some frosting on top of the biscuit.')
        biscuit(savor)
    return wrapper

frosted_biscuit = frosting(biscuit)
frosted_biscuit('chocolate')
# Some frosting on top of the biscuit
# The biscuit is made of chocolate.

Instead of frosted_biscuit = frosting(biscuit) we can use the @ symbol.

def frosting(biscuit):
    def wrapper(savor):
        print('Some frosting on top of the biscuit.')
        biscuit(savor)
    return wrapper

@frosting
def biscuit(savor):
    print("The biscuit is made of {}.".format(savor))

biscuit('chocolate')
# Some frosting on top of the biscuit
# The biscuit is made of chocolate.

It does exactly the same yet it makes the code more clearer than before. Each time we call biscuit it will be directly the decorated version.

Decorators on Top of Decorators

We are not limited to a single decorator per function.

def coco_layer(func):
    def wrapper(savor):
        print('The layer is made of coco.')
        func(savor)
    return wrapper

def vanilla_layer(func):
    def wrapper(savor):
        print('The layer is made of vanilla.')
        func(savor)
    return wrapper

def banana_layer(func):
    def wrapper(savor):
        print('The layer is made of banana.')
        func(savor)
    return wrapper

@coco_layer
@vanilla_layer
@banana_layer
def biscuit(savor):
    print("The biscuit is made of {}.".format(savor))

biscuit('chocolate')
# The layer is made of coco.
# The layer is made of vanilla.
# The layer is made of banana.
# The biscuit is made of chocolate.

However the order of your decorators is very important. First the banana layer will decorate our biscuit, then vanilla and finally coco.

Decorator with Arguments

At the moment all of our decorators doesn’t have arguments. To add them we need another layer on our decorating function.

def frosting(savor):
    def frosting(func):
        def wrapper():
            print("Frosting with {}".format(savor))
            func()
        return wrapper
    return frosting

@frosting('chocolate')
def biscuit():
    print('Biscuit')

biscuit()
# Frosting with chocolate
# Biscuit

So we have a function who returns a function who returns a function. Are you still following?

Think of it as a factory. We give it some arguments and it builds for us our decorator.

One Decorator to Rule them All

Here’s a generic version for all your decorators.

def decorator_factory(*args, **kwargs):
    def decorator(func):
        def wrapper(*f_args, **f_kwargs):
            func(*f_args, **f_kwargs)
        return wrapper
    return decorator


@decorator_factory(*args, **kwargs)
def decorated(*f_args, **f_kwargs):
    pass

Caveats & Solutions

When a function is decorated, its signature is replaced.

def decorator(func):
    """This is the decorator function."""
    def wrapper():
        """This is the wrapper function."""
        func()
    return wrapper

def decorated():
    """This is the decorated function."""
    print('Hello')

print(decorated.__name__) # wrapper
print(decorated.__doc__) # This is the wrapper function.

Fortunately there is a handy library at the rescue: functools. It provides a decorator wraps who keeps the right signature.

from functools import wraps

def decorator(func):
    """This is the decorator function."""
    @wraps(func)
    def wrapper():
        """This is the wrapper function."""
        func()
    return wrapper

def decorated():
    """This is the decorated function."""
    print('Hello')

print(decorated.__name__) # decorated
print(decorated.__doc__) # This is the decorated function.

Conclusion

Decorators are very useful things as they enhance a function without changing its code. There are a lot of examples out there. We already saw wraps from functools, but here is flask’s @route. It maps the decorated function responsible of a view to an url.