For quite some time I was unable to wrap (pun intended) my head around python decorators. Rendered as "syntactic sugar" I was missing the real usecases and the overall idea behind implementation. Reading Jeff Knupp's blog, especially this post has helped me a lot.

Unfortunately even though I had foundations on how to use decorators, I couldn't find any good ways to use them in my code or at least a valid usecase for them. In part 2 of this post we'll inspect some much smarter implementations using Flask as an example.

Decorators - core concepts

The syntax of the decorator is fairly easy. Consider this trivial example:

def myfunc(name):  
    return name
print(myfunc('Chris'))  
>>> Chris

Nothing worth noting here. We have a function which takes a name as argument and returns that name.

Let's add another function which will serve as our decorator:

def my_decorator(func):  
    def function_wrapper(name):
        return 'That guy, {0} is really the MVP!'.format(func(name))
    return function_wrapper

So what happens here? We created a new function called my_decorator which takes an argument func - i.e. some function. my_decorator will generate a new function - function_wrapper and get all the details that it needs from func and return a string. The top function will actually return the wrapper. Confusing?

Let's inspect the results of calling these functions:

myfunc = my_decorator(myfunc)  
print(myfunc('Chris'))  
>>> That guy, Chris is really the MVP!

Our myfunc is now a function within a function. We pass Chris as an argument (just as we did before) but our output is now a function augmented with the functionality of the our decorator function.

Decorators per se are syntactic sugar and as such, can represent this concept in an easier way. Our final code would be like this:

def my_decorator(func):  
    def function_wrapper(name):
        return 'That guy, {0} is really the MVP!'.format(func(name))
    return function_wrapper

# we use @-notation to decorate a function 
@my_decorator
def myfunc(name):  
    return name

# no need for magic here - we call myfunc as usual
print(myfunc('Chris'))  

Thanks to the @-notation, we can call our function just as in our first example. Python already knows that if the function is preceded by @my_decorator, it means it needs to be decorated

Decorators with arguments

Decorators accept arguments too! Let's see how we can make use of that functionality - bear in mind though that this is not the most useful example that the mankind has seen, rather a demonstration of the concept.

Let's say we have a directory listing like this:

$ pwd && tree
/Users/defaultokun/Desktop/decoractors
.
├── basedir_1
│   └── temp
│       ├── temp_file1
│       ├── temp_file2
│       └── temp_file3
└── basedir_2
    └── temp
        ├── temp2_file1
        ├── temp2_file2
        └── temp2_file3

We have a function that scans a selected directory using os.listdir. Unfortunately it takes only folder name as an argument. Similarly, we have a function which creates a directory and takes a new folder name as an argument:

def dir_printer(reldir):  
    print(os.listdir(reldir))

def make_dir(reldir):  
    os.mkdir(reldir)

We also have the code for our decorator:

def base_directory(basedir):  
    def decorated(func):
        def wrapper(reldir):
            abs_path = os.path.join(basedir, reldir)
            func(abs_path)
        return wrapper
    return decorated

Hey! what's going on here? We ended up with function in a function... in a function. The base_directory will be our @base_directory decorator - but this time it takes an argument. The decorated function will digest the function which we are trying to decorate, i.e. make_dir and dir_printer and the wrapper will handle the execution of the wrapped function - in our case it will join the reldir from the function we are trying to decorate with the base from the decorator. Return all the things and we're done!

Finally let's add our sugar:

@base_directory('/Users/defaultokun/Desktop/decorators')
def dir_printer(reldir):  
    print(os.listdir(reldir))

@base_directory('/Users/defaultokun/Desktop/decorators')
def make_dir(reldir):  
    os.mkdir(reldir)

We are providing a base directory on top of our functions so when we execute both of them we are acting on the same path:

dir_printer('basedir_1/temp')  
>>> ['temp_file1', 'temp_file2', 'temp_file3']

and after doing:

make_dir('new_basedir')  
>>>

we get an updated tree

/Users/defaultokun/Desktop/decorators
.
├── basedir_1
│   └── temp
│       ├── temp_file1
│       ├── temp_file2
│       └── temp_file3
├── basedir_2
│   └── temp
│       ├── temp2_file1
│       ├── temp2_file2
│       └── temp2_file3
└── new_basedir

Hopefully this short piece sheds some light on the topic. Part 2 will look more closely on the implementation in Flask framework. I'll also share some good resources on the topic in case you still can't (bad pun incoming) wrap your head around it.

Tags: