Understanding Python Decorators and Closure Functions


Strictly speaking, decorators are just syntactic sugar to passing in one function as a parameter to another function.

In [1]: def foo(function):
    ...:     print 'foo running'

In [2]: @foo
    ...: def fooTwo():
    ...:     print 'fooTwo running'
foo running

In [3]: foo(fooTwo)
foo running

As we just saw, you can always simply call a decorator like any regular callable, passing another function. Sometimes that is actually convenient, especially when doing metaprogramming - changing program behavior at runtime.

As you can see above, a decorator is defined like any other function. Calling it is simple, it's just like any other regular callable. But the parameters being passed here is another function. It may not seem convenient at first sight. However, when you delve into tedious coding tasks, such as metaprogramming, decorators become extremely convenient.

To summarize, a decorator is a callable that takes a function as an argument, the decorated function. The decorator may perform some processing with the decorated function, and returns it or replaces it with another function or callable object.

When are decorators executed? 

To ensure that you construct the exact program flow you intend, you must be aware of how and when decorators are executed. They're not like any other function, awaiting to be invoked. If you notice in the example demonstrated above, 'foo running' was printed immediately after the function fooTwo was defined. fooTwo was defined with the decorator foo. Which means decorators run right after the decorated function is defined. That is usually at import time. If you don't understand from the example above, that's no problem. The following example will make much more sense. 


carsParked = []

def carPark(car):
    print('running carPark {}'.format(car))
    carsParked.append(car)
    return car

@carPark
def CarOne():
    print('Car One Parked')

@carPark
def CarTwo():
    print('Car Two Parked')

def CarThree():
    print('Car Three Parked')

def main():
    print('Main Function Running')
    print carsParked
    CarOne()
    CarTwo()
    CarThree()

if __name__ == "__main__":
    main()

If you run the above code from the command line, you'll get...

running carPark <function CarOne at 0x1004d7398>
running carPark <function CarTwo at 0x1004d7410>
Main Function Running
[<function CarOne at 0x1004d7398>, <function CarTwo at 0x1004d7410>]
Car One Parked
Car Two Parked
Car Three Parked

To conclude, function decorators run immediately after the module is imported. However, decorated functions are only executed when they're explicitly invoked, as in the main function above. This demonstrates what runtime and import time mean.

Through my experience, I've mostly seen (and written) decorators being used to change/replace the decorated function. This is usually done by defining a nested function within the decorator. Before we get any deeper, if we're gonna talk about nested functions, we must cover closure first. 

Closure 

Strictly speaking, a closure is a function with access to a wider scope than its own (local). Take a look below.


In [1]: def parkingLot():
   ...:     lot = []
   ...:     def carParker(newCar):
   ...:         lot.append(newCar)
   ...:         totalCars = len(lot)
   ...:         return totalCars
   ...:     return carParker
   ...:

In [2]: FirstStreetParkingLot = parkingLot()

In [3]: FirstStreetParkingLot('CarOne')
Out[3]: 1

In [4]: FirstStreetParkingLot('CarTwo')
Out[4]: 2

The closure for carParker extends its own local scope to include the variable 'lot'. In this situation, 'lot' is a free variable. By definition, a free variable is one that is not bound to the local scope. Take a look below.

In [5]: FirstStreetParkingLot.__code__.co_freevars
Out[5]: ('lot',)

In [6]: FirstStreetParkingLot.__code__.co_varnames
Out[6]: ('newCar', 'totalCars')


A point I must emphasize on pertains to the definition of closure above. Notice that I said with access when I was describing the closure's scope of capability. The point I'm trying to make here is that the free variable cannot be assigned to, in the body of the closure function.



In [1]: def parkingLot():
   ...:     totalCars = 0
   ...:     def carParker(newCar):
   ...:         totalCars += 1
   ...:         return totalCars
   ...:     return carParker

In [2]: newLot = parkingLot()

In [3]: newLot('newCar')

Traceback (most recent call last):
...
UnboundLocalError: local variable 'totalCars' referenced before assignment

The error above occurs from the attempt to assign a value to the free variable, when totalCars is a number or any immutable type. What we're trying to accomplish makes totalCars a local variable, which is not possible. This problem didn't occur in the example above because 'lot' was never assigned to. We leveraged the fact that it's of mutable type list and appended to it. 

To reiterate, with immutable types, you cannot assign or update, doing so implies you're attempting to create a local variable from a free variable. With immutable types you can only read.

The workaround this is simple using the nonlocal declaration.

In [1]: def parkingLot():
   ...:     totalCars = 0
   ...:     def CarParker(newCar):
   ...:         nonlocal totalCars
   ...:         totalCars += 1
   ...:         print(totalCars)
   ...:         return totalCars
   ...:     return CarParker

In [2]: newLot = parkingLot()

In [3]: newLot('Ford')

1

nonlocal allows you to declare a variable as a free variable even when it's assigned a new value within the closure function. However, keep in mind that nonlocal is only available in Python 3.


Now that we understand the basic functionality of a decorator and the scope of a closure, lets take a look at some decorators that are in the Python standard library, and how we can utilize them in this next post!