Decorators and Functional Python
Decorators are one of Python's great features. In addition to their intrinsic usefulness in the language, they also help us to think in an interesting way — a functional way.
I intend to explain how decorators work from the ground up. We'll start by covering a few topics you'll need in order to understand decorators. After that, we'll dive in and explore a few simple decorators and how they work. Finally, we'll talk about some more advanced ways to use decorators, such as passing them optional arguments or chaining them together.
First, let's define what a Python function is in the simplest way I can think of. From that simple definition, we can then define decorators in a similarly simple way.
A function is a block of reusable code that performs a specific task.
Okay, so then what is a decorator?
A decorator is a function that modifies other functions.
Now let's start to expand on that definition of decorators, starting with a couple prerequisite explanations. Functions are first class objects
In Python, everything is an object. What this means is that functions can be referred to by name and passed around like any other object. For example:
def traveling_function(): print "Here I am!"function_dict = { "func": traveling_function}trav_func = function_dict['func']trav_func()# >> Here I am!
traveling_function was assigned as the value of the func key in the function_dict dictionary and can still be called like normal. First class functions allow for higher order functions
We can pass functions around like any other object. We can pass them as values to dictionaries, put them in lists, or assign them as object properies. So couldn't we pass them as arguments to another function? We can! A function that accepts another function as a parameter or returns another function is called a higher order function.
def self_absorbed_function(): return "I'm an amazing function!"def printer(func): print "The function passed to me says: " + func()# Call `printer` and give it `self_absorbed_function` as an argumentprinter(self_absorbed_function)# >> The function passed to me says: I'm an amazing function!
So here you can see that a function can be passed to another function as an argument, and that function can then invoke the passed in function. This allows us to create some interesting functions, like decorators! The basics of a decorator
At heart, a decorator is just a function that takes another function as an argument. In most cases they return a function that is a modified version of the function they are wrapping. Let's look at the simplest decorator we can that might help us understand how all of this works — the identity decorator.
def identity_decorator(func): def wrapper(): func() return wrapperdef a_function(): print "I'm a normal function."# `decorated_function` is the function that `identity_decorator` returns, which# is the nested function, `wrapper`decorated_function = identity_decorator(a_function)# This calls the function that `identity_decorator` returneddecorated_function()# >> I'm a normal function
Here, identity_decorator does not modify the function it wraps at all. It simply returns a function (wrapper) that when called, will invoke the original function identity_decorator received as an argument. This is a useless decorator!
What's interesting about identity_decorator is that wrapper has access to the funcvariable even though func was not passed in as one of its arguments. This is due to closures. Closures
Closure is a fancy term meaning that when a function is declared, it maintains a reference to the lexical environment in which it was declared.
When wrapper was defined in the previous example, it had access to the funcvariable in its local scope. This means that throughout the life of wrapper (which got returned and assigned to the name decorated_function), it will have access to the func variable. Once identity_decorator returns, the only way to access func is through decorated_function. func does not exist as a variable anywhere other than inside decorated_function's closure environment. A simple decorator
Now let's create a decorator that will actually be of a little use. All this decorator will do is log how many times the function it modifies gets called.
def logging_decorator(func): def wrapper(): wrapper.count += 1 print "The function I modify has been called {0} times(s).".format( wrapper.count) func() wrapper.count = 0 return wrapperdef a_function(): print "I'm a normal function."modified_function = logging_decorator(a_function)modified_function()# >> The function I modify has been called 1 time(s).# >> I'm a normal function.modified_function()# >> The function I modify has been called 2 time(s).# >> I'm a normal function.
We said a decorator modifies a function, and it can be useful to think of it like that. But as you can see in our example, what logging_decorator does is return a new function that is similar to a_function, but with the addition of a logging feature.
In this example, logging_decorator not only accepts a function as a parameter, it also returns a function, wrapper. Each time the function that logging_decorator returns gets called, it increments wrapper.count, prints it, and then calls the function that logging_decorator is wrapping.
You might be wondering why our counter is a property of wrapper instead of a regular variable. Wouldn't wrapper's closure environment give us access to any variable declared in its local scope? Yes, but there's a catch. In Python, a closure provides fullread access to any variable in the function's scope chain, but only provides write access to mutable objects (lists, dictionaries, etc.). An integer is an immutable object in Python, so we wouldn't be able to increment its value inside of wrapper. Instead, we made our counter a property of wrapper, a mutable object, and thus we can increment it all we like! Decorator syntax
In the last example, we saw that a decorator can be used by passing it a function as an argument, thus 'wrapping' that function with the decorator function. However, Python also has a syntax pattern that makes this more intuitive and easier to read once you are comfortable with decorators.
# In the previous example, we used our decorator function by passing the# function we wanted to modify to it, and assigning the result to a variabledef some_function(): print "I'm happiest when decorated."# Here we will make the assigned variable the same name as the wrapped functionsome_function = logging_decorator(some_function)
# We can achieve the exact same thing with this syntax:@logging_decoratordef some_function(): print "I'm happiest when decorated."
Using the decorator syntax, this is the bird's eye view of what happens:
The interpreter reaches the decorated function, compiles some_function, and gives it the name 'some_function'.
That function is then passed to the decorator function that is named in the decoration line (logging_decorator).
The return value of the decorator function (usually another function that wraps the original) is substituted for the original function (some_function). It is now bound to the name 'some_function'.
With these steps in mind, let's annotate the identity_decorator a little for clarification.
def identity_decorator(func): # Everything here happen