There are four steps to understanding Python decorators.
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
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
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
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.