In the previous part of this article I introduced the
Params2attribs decorator that copies object initialiser parameters to object attributes.
In this part I aim to introduce the Python decorator mechanisms needed to drill down into
Params2attribs in the forthcoming third part of this series.
Python decorator 101
Python decorators come in two flavours: simple and not so simple 🙂 The
Params2attribs decorator which is the subject of this article series falls into the second category. But, as they say, first things first..
The simple decorator variety is in essence a function that returns a wrapper function that is to be invoked instead of the wrapped function. Confused? You should be 🙂
Let’s have a look at an example. In the code below the wrapper function (lines 13 – 16) merely logs the invocation of the wrapped function.
Just in case you haven’t seen this before: the
**kwargs constructs (on lines 13 and 15) are the Python way to pass positional and keyword function parameters in a generic fashion.
1 #!/usr/bin/env python 2 """simple decorator example""" 3 4 # Copyright: (c) Muharem Hrnjadovic 5 # created: 17/10/2006 08:25:09 6 7 __version__ = "$Id$" 8 # $HeadURL $ 9 10 from sys import stdout 11 12 def interceptor(f): 13 def wrapper(*args, **kwargs): 14 stdout.write("Hey %s(), you have been wrapped..\\n\\t" % f.__name__) 15 f(*args, **kwargs) 16 stdout.write(".. resistance is futile\\n") 17 return wrapper 18 19 @interceptor 20 def plf(): stdout.write("Help! I am only a poor little function!\\n") 21 22 if __name__ == '__main__': 23 plf()
A few points to note here: the decorator function
- is invoked by Python whenever it loads any code decorated with it (lines 19 – 20)
- defines (lines 13 – 16) and returns (line 17) another function (
wrapper()) that is to be invoked (line 23) instead of the decorated one (
As you may have guessed already, running the code above results in the following output:
mhr@playground2:~/src/published$ python simpledeco.py Hey plf(), you have been wrapped.. Help! I am only a poor little function! .. resistance is futile
Complex decorators may be implemented as classes or using nested functions. The latter variant was suggested by a reader and is shown after the section on decorator classes.
When using decorator classes any data beyond the function to be wrapped is passed to the decorator class initialiser (see e.g. the
after parameters of initialiser
interceptor::__init__(), lines 13 – 15).
Furthermore, instances of decorator classes must be callable like functions. Our decorator class is hence implementing the special method
__call__ (lines 16 – 21).
1 #!/usr/bin/env python 2 """not so simple decorator example""" 3 4 # Copyright: (c) Muharem Hrnjadovic 5 # created: 17/10/2006 08:25:09 6 7 __version__ = "$Id$" 8 # $HeadURL $ 9 10 from sys import stdout 11 12 class interceptor(object): 13 def __init__(self, before, after): 14 self.before = before 15 self.after = after 16 def __call__(self, f): 17 def wrapper(*args, **kwargs): 18 stdout.write(self.before) 19 f(*args, **kwargs) 20 stdout.write(self.after) 21 return wrapper 22 23 @interceptor('>> ', ' <<\\n') 24 def plf(): stdout.write("Help! I am only a poor little function!") 25 26 if __name__ == '__main__': 27 plf()
The code above works approximately in the following fashion:
- Python loads the module and encounters the decorated code on lines 23 – 24
- at that point it will instantiate an
interceptorobject and pass the two string parameters to its initialiser
- the initialiser just copies the
afterparameters to object attributes for later use (lines 13 – 15)
- then the
interceptorobject (instantiated in the previous step) is called by Python as a function at which point its
__call__method is invoked
__call__defines (lines 17 – 20) and returns (line 21) the wrapper function
- the call to
plf()(line 27) invokes the
wrapper()function defined and returned in step 5
The output (did you guess it?) is as follows:
mhr@playground2:~/src/published$ python notsosimpledeco.py >> Help! I am only a poor little function! <<
As a reader pointed out after the first draft of this article was posted (see also the first two comments on this weblog entry) a similar effect can be achieved using nested functions. The nested functions variant of the decorator looks as follows:
1 #!/usr/bin/env python 2 """a variant of the not so simple decorator example""" 3 4 from sys import stdout 5 6 def interceptor(before, after): 7 def interceptor_inner(f): 8 def wrapper(*args, **kwargs): 9 stdout.write(before) 10 f(*args, **kwargs) 11 stdout.write(after) 12 return wrapper 13 return interceptor_inner 14 15 @interceptor('>> ', ' <<'\\n') 16 def plf(): stdout.write("Help! I am only a poor little function!") 17 18 if __name__ == '__main__': 19 plf()
This is the end of part two of the Python decorator mini-study. In the next part of this article (part 3) we’ll finally get to dissect the
Params2attribs decorator introduced in part one