Python decorator mini-study (part 1 of 3)

Introduction

On quite a few occasions I would find myself coding Python object initialiser methods where I would manually copy the method parameters to object attributes in order to make use of them later (see e.g. TheHardWay::__init__(), lines 13 – 17 below).

This is an unnecessary laborious, error-prone and unpythonic activity :-) In this article I hence present an alternative and more pythonic solution: a function decorator that copies initialiser method parameters to object attributes (see lines 19 – 24 below for an example showing how that decorator is used).

Furthermore, this article shows how to devise Python function decorators that require additional data (beyond the function that is to be decorated) in order to be useful.

Last but not least, the decorator to be shown in (part 3 of) this article is most useful in conjunction with initialiser methods but there is nothing to prevent you from using it with any object method.

Warm-up exercise

Again, the code section below (lines 13 – 17) shows how to copy the parameters to object attributes in a manual fashion.

 1  #!/usr/bin/env python
 2  """Demo code for initialiser method decorator"""
 3
 4  # Copyright: (c) 2006 Muharem Hrnjadovic
 5  # created: 15/10/2006 11:21:01
 6  __version__ = "$Id$"
 7  # $HeadURL $
 8
 9  import pprint as PP, sys
10  from p2adeco import Params2attribs
11
12  class TheHardWay(object):
13      def __init__(self, arg_a, arg_b, arg_c):
14          self.arg_a = arg_a
15          self.arg_b = arg_b
16          self.arg_c = arg_c
17          # .. now do whatever initialisation is required ..

The alternative solution below uses a python function decorator (on line 19) which is given a (possibly empty) tuple of parameter names for which no copying should occur.
In this particular example we don’t want arg_3 to be copied and hence specify it in the tuple passed to the decorator’s object initialiser.

Please note also that the decorator is capable of handling embedded parameter lists (like e.g. parameters arg_2 and arg_3 which are passed to the decorated initialiser via such a list (on line 20)).

18  class CoolApproach(object):
19      @Params2attribs(('arg_3',))
20      def __init__(self, arg_1, (arg_2, arg_3), arg_4, arg_5):
21          # .. at this point all parameters except for 'arg_3' have been
22          # copied to object attributes
23          # .. now do whatever initialisation is required ..
24          print ">> In initialiser, self.arg_1 = '%s'" % self.arg_1

Finally, the block below merely instantiates a CoolApproach object (line 27) in order to demonstrate the described decorator behaviour. Line 28 sorts the attributes of the newly instantiated object for clarity.

25  if __name__ == '__main__':
26      embedded_params = ('E1', 'E2')
27      obj = CoolApproach('aa', embedded_params, 22, 3.33)
28      sorted_attributes = sorted(obj.__dict__.iteritems())
29      sys.stdout.write("\\nObject attributes:\\n%s\\n" %
30                       PP.pformat(sorted_attributes, indent=4, width=60))

When running the code above the following output results:

>> In initialiser, self.arg_1 = 'aa'

Object attributes:
[   ('arg_1', 'aa'),
    ('arg_2', 'E1'),
    ('arg_4', 22),
    ('arg_5', 3.3300000000000001)]

From the output we can see that:

  1. the CoolApproach initialiser method was entered (print statement on line 24)
  2. all the desired initialiser parameters were copied to attributes of the respective object (print statement on lines 29 – 30)

Please note also that arg_3 is not listed as an attribute in the output above indicating that the decorator is paying attention to the parameter exclusion list passed to it.

Conclusion

This is the end of part one, the next part of this article introduces the Python decorator mechanics needed for the final part (dissecting the Params2attribs decorator)