4
\$\begingroup\$

I personally don't like the boilerplate of most __init__ methods:

self.a = a
self.b = b
...

So I thought this would be a nice opportunity to learn a bit more about decorators. As this is my first attempt on a class decorator I'm sure there is a lot to improve so fire away:

Implementation

from collections import namedtuple
def autofill(*args, **kwargs):
 """ This class decorator declares all attributes given into the constructor
 followed by a call of __init__ without arguments (beside reference to self).
 Order is the same than namedtuple with the possibility of default elements.
 Note that in this decorator alters the existing class instance instead of
 returning a wrapper object. """
 def filler(cls, *args, **kwargs):
 """ This is our custom initialization method. Input sanitation and
 ordering is outsourced to namedtuple. """
 for key, val in InputSanitizer(*args, **kwargs)._asdict().items():
 setattr(cls, key, val)
 filler.super_init(cls)
 def init_switcher(cls):
 filler.super_init = cls.__init__
 cls.__init__ = filler
 return cls
 # Taken from http://stackoverflow.com/questions/11351032/named-tuple-and-
 # optional-keyword-arguments
 InputSanitizer = namedtuple('InputSanitizer', args + tuple(kwargs.keys()))
 InputSanitizer.__new__.__defaults__ = tuple(kwargs.values())
 return init_switcher

Some test cases

import unittest
class TestAutoFill(unittest.TestCase):
 @autofill('a', b=12)
 class Foo(dict):
 pass
 def test_zero_input(self):
 with self.assertRaises(TypeError):
 self.Foo()
 def test_one_input(self):
 bar = self.Foo(1)
 self.assertEqual(bar.a, 1)
 self.assertEqual(bar.b, 12)
 bar = self.Foo(a=1)
 self.assertEqual(bar.a, 1)
 self.assertEqual(bar.b, 12)
 with self.assertRaises(TypeError):
 self.Foo(b=1)
 with self.assertRaises(TypeError):
 self.Foo(c=12)
 def test_two_input(self):
 bar = self.Foo(1, 2)
 self.assertEqual(bar.a, 1)
 self.assertEqual(bar.b, 2)
 bar = self.Foo(b=2, a=1)
 self.assertEqual(bar.b, 2)
 self.assertEqual(bar.a, 1)
 def test_other_object_functions(self):
 bar = self.Foo(1)
 bar.c = 3
 bar['key'] = 4
 self.assertEqual(bar.a, 1)
 self.assertEqual(bar.b, 12)
 self.assertEqual(bar.c, 3)
 self.assertEqual(bar['key'], 4)
if __name__ == '__main__':
 unittest.main()
200_success
146k22 gold badges190 silver badges478 bronze badges
asked Sep 22, 2016 at 0:18
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$
  1. The parameters to autofill could have better names: args gives the argument names to the constructor, and kwargs gives the keyword names and their default values, so perhaps argnames and defaults would be better. This will also help to distinguish them from the parameters to the filler function.

  2. The function filler is used to implement the __init__ method, so its first argument should be named self, not cls.

  3. The original __init__ method is recorded in the super_init property of the filler function. I think that's needlessly tricky. It would be simpler to record it in a local variable:

    def init_switcher(cls):
     original_init = cls.__init__
     def init(self, *args, **kwargs):
     for k, v in InputSanitizer(*args, **kwargs)._asdict().items():
     setattr(self, k, v)
     original_init(self)
     cls.__init__ = init
     return cls
    
  4. The implementation mechanism is quite ingenious! It would not have occurred to me to delegate the argument processing to collections.namedtuple. However, I think that it is clearer to delegate it to inspect.Signature:

    from inspect import Parameter, Signature
    def autofill(*argnames, **defaults):
     """Class decorator that replaces the __init__ function with one that
     sets instance attributes with the specified argument names and
     default values. The original __init__ is called with no arguments
     after the instance attributes have been assigned. For example:
     >>> @autofill('a', 'b', c=3)
     ... class Foo: pass
     >>> sorted(Foo(1, 2).__dict__.items())
     [('a', 1), ('b', 2), ('c', 3)]
     """
     def init_switcher(cls):
     kind = Parameter.POSITIONAL_OR_KEYWORD
     signature = Signature(
     [Parameter(a, kind) for a in argnames]
     + [Parameter(k, kind, default=v) for k, v in defaults.items()])
     original_init = cls.__init__
     def init(self, *args, **kwargs):
     bound = signature.bind(*args, **kwargs)
     bound.apply_defaults()
     for k, v in bound.arguments.items():
     setattr(self, k, v)
     original_init(self)
     cls.__init__ = init
     return cls
     return init_switcher
    
  5. There's no test case that checks that the original __init__ method is called.

answered Sep 26, 2016 at 10:40
\$\endgroup\$
1
  • \$\begingroup\$ Thank you for your thorough analysis. I completely agree with almost all the points and have to say I never heard of the inspect module before. I guess I'm in for a read... On point 3. I prefer the flatter structure (not 3 level of nested) but otherwise completely agree. \$\endgroup\$ Commented Sep 26, 2016 at 16:25

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.