Python Decorators

Monday, 24 September, 2018

There are four steps to understanding Python decorators.

Python Functions are First Class Objects

An object in Python can be assigned a reference. It can be passed as an argument. It can be returned from a function. It can become a part of a list, tuple, dictionary or wherever objects can be placed.

The major difference between most conventional objects and Python function objects is - latter are callable.

def f():
    return 5

print(f)
print(f())
print(type(f))

<function f at 0x7f667039d048>
5
<class 'function'>

Key thing to note in the above statement is that 'f' is a reference to a function object. It is the syntax f() which calls it. And then notice that type(f) gives <class 'function'>. This means there is a class called function which describes how function objects behave.

This is not true with functions in many other languages. Functions are something special and cannot be treated like other objects. That is why we say Python functions are first class objects.

Let us see more examples of the first class nature of objects.

g = f
g()

5

In the above example, f evaluates to a function object and the assignment operator results in g now pointing to the same object. So, whether I say, f() or g(), I get the same result.

def f(a):
    return a**2

def caller(f, a):
    return f(a)

caller(f, 3)

9

In the above example, I am passing an object f to the caller() function which in turn is able to call f using the usual f() syntax. The in-built Python function map() follows this exact approach. You pass a function and a list, map then applies the function on every member of the list.

def function_maker(num):
    def func(x):
        return x**num
    return func

cuber = function_maker(3)
cuber(3)

27

The above example is different from the previous two examples in that we are able to define a function inside a function. Why not? It is just like any other object and I should be able to declare them anywhere where other objects can be declared. The function function_maker is also referred to as a function factory by some peope. Here it is important to remember that func is inside the scope of function_maker and therefore is not callable from outside. Try it!

func(3)

---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

<ipython-input-6-b523faf31d8e> in <module>()
----> 1 func(3)


NameError: name 'func' is not defined

In the next example, we illustrate how functions, being first class objects, can be placed inside a list.

# A wicked example.
def f1(x):
    return x*2

def f2(x):
    return x*3

def f3(x):
    return x*4

funcs = [f1, f2, f3]
for f in funcs:
    print(f(10))

20
30
40

Modifying or Extending Functions Using Higher Order Functions

A small terminology absorption that we need to do - a function that works on other functions is called a higher order function.

Okay, now imagine we want a function to be modified so that it is able to tell us how much time it took for executing the function. Let's see how we do this.

import time

def add_self_timing(f):
    def modified_function():
        t1 = time.time()
        f()
        t2 = time.time()
        print("Execution Time = ", (t2 - t1))
    return modified_function

def myFunction():
    for i in range(100000):
        i**22

# Now, we modify myFunction to declare its own timing.
myFunction = add_self_timing(myFunction)
myFunction()

Execution Time =  0.05740070343017578

Decorator Syntax

myFunction = add_self_timing(myFunction)

This is how we modified our function in accordance with the definition of add_self_timing. Below is a nice syntax that allows us to be lazy (and adds beautiful expression capability to the Python language).

@add_self_timing
def myFunction():
for i in range(100000):
    i**22

It can be applied on any function.

@add_self_timing
def myFunction2():
    for i, j in zip(range(1000), range(1000, 2000)):
        i ** j

myFunction2()

Execution Time =  0.07813024520874023

Some Real Examples of Decorators

A very frequently found decorator is called wraps. Lot of people find it confusing. Let's understand this guy! Let's create a function and apply dir() to it, which will give me a list of all the properties or attributes of the function object.

def superBoy():
    print("Hello")

print(dir(superBoy))

['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

Specifically, let us print out the value of __name__ attribute.

print(superBoy.__name__)

superBoy

Let us consider making some decorator for this function.

def some_decorator(f):
    def new_guy():
        print("Executing")
        f()
    return new_guy

@some_decorator
def myFunction4():
    return 33**12

myFunction4.__name__

'new_guy'

Did you notice what happened? The decorator replaces the function myFunction4 with new_guy. So, when you decided to ask its name by printing out its __name__ attribute, you are printing __name__ of the new_guy function defined by the decorator. So, the original properties of the function being modified are replaced by the properties of the new function that the decorator creates! That is bad! The functools library in Python provides another decorator which can be used on top of the modified function. This is called wraps and what it does is to modify all properties of the new function to be the same as the function being modified. Let's see an example.

from functools import wraps

def some_decorator2(f):
    @wraps(f)
    def new_guy():
        print("Executing")
        f()
    return new_guy

@some_decorator2
def myFunction4():
    return 33**12

Let's print myFunction4.__name__.

print(myFunction4.__name__)

'myFunction4'

We have only looked at __name__ variable in this case. But the same argument would matter to the all important __doc__ attribute as well. Without wraps you will lose the documentation of the function being modified. So @wraps(f) is actually very important to master.

Another example of decorators at work is in Flask. Flask is a web framework which means one of the main things you use it for is designing functions that can be assigned to handle various URLs. These are called view functions. What Django (another web framework) does is maintain a list of URL - function mappings and when you add a new function, you have to go and make a new entry in that file. Whereas in Flask, you simply say something like:

@app.route('/')
def index():
    ...

The route() decorator will then go and update this URL - view function mapping. Further, it will extend the function to actually return an HTTP response while you as a programmer can simply return a string!

Yet another example from the world of Flask is that you want a view function to be executed only if the user is authenticated. With the Flask-Login extension for Flask, you can achieve this as simply as:

@app.route('/wall/<id>')
@login_required
def wall(id):
    ...
    ...

Yet another place where decorators are intensively used is when you are designing classes and indulging in OOP design. Let's see an example.

class A:
    def someMethod(*args):
        print("Inside method")
        print(args)

a = A()
print(a.someMethod())
print(a.someMethod(2,3,4))

Inside method
(<__main__.A object at 0x7f66702c47b8>,)
None
Inside method
(<__main__.A object at 0x7f66702c47b8>, 2, 3, 4)
None

By default, all methods inside a class are bound methods. This means that these methods always expect one argument which is self, the object of the class in the context of which the method was called.

Sometimes, we want some methods to be part of a class but not require an object context for being called. In other words, we want our method to be static.

class A:
    @staticmethod
    def someMethod(*args):
        print("Inside method")
        print(args)

a = A()
print(a.someMethod())
print(a.someMethod(2,3,4))

Inside method
()
None
Inside method
(2, 3, 4)
None

Notice the difference in the output. When a method is not static, the first argument being passed to the method is always the object from which the method is called. But when the method is static, as in the second example, no object reference is passed to the method.

That's it, folks! If you have made it this far, kudos to you! Hope you have more clarity than before about the concept of decorators in Python.




Up