Python decorator mini-study (part 2 of 3)

Introduction

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..

Simple decorators

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 *args and **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 interceptor()

  • 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 (plf())

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

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.

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 before and 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:

  1. Python loads the module and encounters the decorated code on lines 23 – 24
  2. at that point it will instantiate an interceptor object and pass the two string parameters to its initialiser
  3. the initialiser just copies the before and after parameters to object attributes for later use (lines 13 – 15)
  4. then the interceptor object (instantiated in the previous step) is called by Python as a function at which point its __call__ method is invoked
  5. __call__ defines (lines 17 – 20) and returns (line 21) the wrapper function
  6. 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! <<

Nested functions

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()

Conclusion

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

About these ads

8 thoughts on “Python decorator mini-study (part 2 of 3)

  1. >Complex decorators have to be implemented as classes.

    This is not true – you can implement your interceptor decorator using just functions as below:

    def interceptor(before, after):
    def decorator(f):
    def wrapper(*args, **kwargs):
    stdout.write(before)
    f(*args, **kwargs)
    stdout.write(after)
    return wrapper
    return decorator

    What is actually happening is that _calling_ the interceptor function is returning the decorator to use, which is then called as per the normal decorator application. The same thing as is happening with the class version, where calling creates and returns an instance, which is then used as the decorator, and called.

  2. My indentation got mangled there. The corresponding decorator is:

    print “””
    def interceptor(before, after):
    | def decorator(f):
    | | def wrapper(*args, **kwargs):
    | | | stdout.write(before)
    | | | f(*args, **kwargs)
    | | | stdout.write(after)
    | | return wrapper
    | return decorator
    “””.replace(“| “,” “)

  3. Hello Brian,

    thank you for pointing this out! I’ll amend part2 to include your correction.

    #!/usr/bin/env python
    """variant simple decorator example"""

    from sys import stdout

    def interceptor(before, after):
    def interceptor_inner(f):
    def wrapper(*args, **kwargs):
    stdout.write(before)
    f(*args, **kwargs)
    stdout.write(after)
    return wrapper
    return interceptor_inner

    @interceptor(‘>> ‘, ‘ <<\n’)
    def plf(): stdout.write("Help! I am only a poor little function!\n")

    if __name__ == ‘__main__’:
    plf()

    e>

  4. Pingback: see whatever… » Deprecation decorator

  5. Pingback: Miscellanea and a Pycon closeout — jessenoller.com

  6. Thanks dude, great article on python decorators. There seems to be no mention of python decorators in Dive Into Python book, nor the python tutorial on python.org.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s