Decorators 101 - A gentle introduction
21 Jul 2016 #pythonDecorators you say
If you are familiar with python
, chances are that you might have already seen the decorator syntax. It comes off as a simle
concept when being used, but when you try to get your head around the underlying details, you find yourself in a hot fix. And are probably
asking yourself
How the heck does it work?
Python does a very good job in abstracting about the intricacies, so much so that we take it almost for granted. Remember the
routes in flask
?
Adding a route is as simple as doing a
@app.route('/index/')
def hello():
return "hello there"
Where the @
symbol denotes the decorator syntax.
But before diving into decorators, discussing about functions would seem appropriate
>>> def foo(name):
... return "hello {}".format(name)
...
>>> foo
<function foo at 0x7f59dc601aa0>
>>> foo("tasdik")
'hello tasdik'
>>> bar = foo
>>> bar
<function foo at 0x7f59dc601aa0>
>>> bar("body double")
'hello body double'
>>>
If you have been dabbling away in python, this might not seem very unfamiliar to you I assume.
As you can see, functions can be assigned to each other
Everything in python is considered as a first class object, which include functions
, classes
and everything else which you thought
could not be an object. Jokes apart, this paradigm is really different from the other programming languages but it has it’s own
advantages to it.
Moving forward this analogy of treating everything in python as first class objects. Functions can also be passed on other functions! Not sure about that? Here you go
>>>
>>> def sum(a, b):
... return a+b
...
>>> def diff(a, b):
... return a-b
...
>>> def operation(func, a, b):
... return func(a, b)
...
>>> operation(sum, 10, 20)
30
>>> operation(diff, 10, 20)
-10
>>>
We have passed around the function name to the function operation()
as we pass around normal values.
Now what if I told you functions can be returned as return values for other functions! Let’s see how we do that
>>> def foo(value):
... i = 2
... def bar():
... return value
... return bar
...
>>> bar
<function foo.<locals>.bar at 0x7fe2c7229b70>
>>>
>>> bar = foo(10)
>>> bar.__closure__[0].cell_contents
10
>>> bar()
10
>>>
Not that’s surprising then I suppose. The only gotcha here would be that the inner enclosing functions have the access to the
enclosing function variables. That is the reason, the variable value
is still accessible to the function foo()
The foo
function displays the closure property beautifully here as it stores the value that was passed on to it.
Talking about closures, this can be used very cleverly in some cases
>>> val1 = foo(10)
>>> val1
<function bar at 0x7f59dc601de8>
>>> val1()
10
>>> val2 = foo(20)
>>> val2()
20
>>>
You can see a special behaviour here demonstrated by the function foo()
. It remembers the value passed to it between function
calls. A property which can then be utilized for implementing other features. It’s somewhat similar to the demonstration of public and
private interface. Where the function foo()
would be acting as the public function and inner()
being the private one.
So how do I write one
Simply put, decorators are nothing but funcions which take on another functions and modify it’s behaviour without changing the original code
Confused? Let’s write one
This is more useful in the context when we have a function and we want to modify the output of that function without playing around with the original source code. Reasons may be that we are not allowed to do or because it’s simply not possible, whatever the reason may be. Decorators are here to the rescue.
>>> def greet(name):
... return "hello there {}!".format(name)
...
>>> def tagify(func):
... def wrap(name):
... return "<p>{}</p>".format(name)
... return wrap
...
>>>
>>> tagify_tasdik = tagify(greet)
>>> tagify_tasdik("tasdik")
'<p>tasdik</p>'
So we just decorated the return value of a function!
But where is that @
syntax you were talking all along?
Don’t worry, here is an example for you. Keeping in mind what we have discussed so far. Keeping the above example in mind,
We don’t always have to do tagify_tasdik = tagify(greet)
for decorating our function. Python provides some syntactic sugar
for doing the same.
>>>
>>> def tagify(func):
... def wrap(name):
... return "<p>{}</p>".format(func(name))
... return wrap
...
>>>
>>> @tagify
... def greet(name):
... return "hello there {}".format(name)
...
>>>
>>> greet("foo")
'<p>hello there foo</p>'
>>>
Chaining one or more decorators
As the title suggests, let’s say we want to decorate our function further, we can chain the decorators to get the desired output.
>>> def p_tagify(func):
... def wrap(content):
... return "<p>{}</p>".format(func(content))
... return wrap
...
>>> def h1_tagify(func):
... def wrap(content):
... return "<h1>{}</h1".format(func(content))
... return wrap
...
>>> def div_tagify(func):
... def wrap(content):
... return "<div>{}</div>".format(func(content))
... return wrap
...
>>> @div_tagify
... @h1_tagify
... @p_tagify
... def greet(name):
... return "hello there {}".format(name)
...
>>> greet("tasdik")
'<div><h1><p>hello there tasdik</p></h1</div>'
>>>
But wait a second! What do we have here
>>> greet.__name__
'wrap'
>>>
As you can see, the functions name got changed to the method which was decorating it and this can cause a huge pain when you are debugging your programs.
But as usual, we have functools
to the rescue
>>>
>>> from functools import wraps
>>>
>>> def p_tagify(func):
... @wraps(func)
... def decorate(content):
... return "<p>{}</p>".format(func(content))
... return decorate
...
>>> @p_tagify
... def greet(name):
... return "hello there {}".format(name)
...
>>> greet("tasdik")
'<p>hello there tasdik</p>'
>>> greet.__name__
'greet'
>>>
Passing Arguments to decorators
Now wouldn’t it have been real nice if you could pass on arguments to decorators to tagify the content as you wished. This would reduce 3 functions into 1. (Remember the decorator chaining example?)
>>> from functools import wraps
>>>
>>> def tag(tag_name):
... def tag_decorator(func):
... @wraps(func)
... def func_wrapper(content):
... return "<{0}>{1}</{0}>".format(tag_name, func(content))
... return func_wrapper
... return tag_decorator
...
>>> @tag("p")
... def greet(name):
... return "hello there {}".format(name)
...
>>> greet("tasdik")
'<p>hello there tasdik</p>'
>>> greet.__name__
'greet'
>>>
So I hope you now have a good idea about how decorators work in python.