Bits of Py.

If the implementation is hard to explain, it's a bad idea

Sun 12 March 2017

Decorators in Python

Posted by marodrig in blog   

Decorators in Python.

Have you seen that weird @ on top of your brand new function that you created? What is it? What does it mean exactly? and how does it work... If you have done web development using Django, or Flask, you have come across decorators. If you have done PyQt or PySide you have seen them for those Signals and Slots you love so much. I am going to explain what decorators are in Python, and what are some good examples of using them. Let's get started on decorators.

What are Decorators?

Before we go and discuss decorators we need to review certain concepts about functions in Python. The first one is that in Python-verse functions are first citizens. This means that we can pass functions as parameter to other functions, like this:

Functions as parameters.

>>> def say_hello_with_name(fnc, name):
...     return fnc(name)
...
>>> def hello_and_arg(arg):
...     return " Hello " + arg
...

If we call say_hello_with_name using hello_and_arg as a parameter, and a name:

>>> print(say_hello_with_name(hello_and_arg, "John"))

We get the following output:

  Hello John

Functions can be Nested in other functions.

In addition to being able to pass functions to other functions as parameter, we can also define functions within other function:

>>> def main_parent_function():
...     print("This is the main parent function.")
...     def first_nested_fnc():
...             return "I am the first nested function."
...     def second_nested_fnc():
...             return "I am the second nested function."
...     print(first_nested_fnc())
...     print(second_nested_fnc())

Let's see what is the output when we call main_parent_function:

>>> main_parent_function()
This is the main parent function.
I am the first nested function.
I am the second nested function.

We can see that the output is what we expected. The parent function is executed first, and the nested functions are executed in the order they are called within the parent function.

Let's try to call one of the nested functions, first_nested_fnc from outside the parent function and see what happens:

>>> first_nested_fnc()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'first_nested_fnc' is not defined

We can see that the scope of the nested functions is limited to our main_parent_function. Remember: functions defined within a parent function are limited in scope to that parent function.

We can return functions inside functions.

Another Extra cool trick Python allow us to do is to return functions from inside functions:

    def parent_fnc(number):

        def first_nested_fnc():
            return "First nested function."

        def second_nested_fnc():
            return "Second nested function."


    if number == 10:
        return first_nested_fnc
    else:
        return second_nested_fnc

And the result of calling the parent function with different parameter values:

>>> is_ten = parent_fnc(10)
>>> not_ten = parent_fnc(11)
>>> print(is_ten)
<function first_nested_fnc at 0x1071016e0>
>>> print(not_ten)
<function second_nested_fnc at 0x107107c08>
>>> print(is_ten())
First wrapper.
>>> print(not_ten())
Second nested function.

And just like that we return different functions depending on the input to our parent function!

Decorators

Decorators are callable objects that modify the behavior of other functions. Python's syntax for decorators is @. They are stackable, meaning you can use more than one, and can have arguments.

First we define our decorator function and our nested function:

>>> def all_upper(fnc):
...     def wrapper():
...             return fnc().upper()
...     return wrapper
...

We also need our normal, to be modified by our decorator function:

>>> def hello_world():
... return "Hello World!"
...

Hold on, we need to tell it that we want it to be wrapped around our decorator, we use @ and the name of our decorator function:

>>> @all_upper
... def hello_world():
... return "Hello World!"
...

There, we are now ready to call our decorated function and see what happens:

>>> print(hello_world())
HELLO WORLD!
>>>

Our hello_world function is modified by all_upper and instead of Hello World!, we get the output in all upper case.

Summary

Decorators are a syntax that allows us to call functions that modify the behavior of other functions in a very Pythonic way (Clear, concise, and simple way).

Some of the more common uses for decorators include:

  • Logging.
  • Debugging.
  • Asserting types.
  • Check returned types.
  • Authentication.
  • Authorization.

And benefits of using decorators:

  • Robust design by separating concerns.
  • Easy to turn behavior on or off as needed.
  • Improve readability by:
    • Less lines of boiler plate.
    • Less code duplication.
    • Simple maintenance of our code base.

And remember these best practice to keep in mind when using decorators:

  • If your use more than one at the same time (stacking) note where they can be a problem.
  • Use the functools.wraps decorator for internal functions. This preserves the metadata from our wrapped function.
  • DOCUMENT THEM WELL!
  • Did I mention to document them?