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()
1 Answer 1
The parameters to
autofillcould have better names:argsgives the argument names to the constructor, andkwargsgives the keyword names and their default values, so perhapsargnamesanddefaultswould be better. This will also help to distinguish them from the parameters to thefillerfunction.The function
filleris used to implement the__init__method, so its first argument should be namedself, notcls.The original
__init__method is recorded in thesuper_initproperty of thefillerfunction. 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 clsThe 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 toinspect.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_switcherThere's no test case that checks that the original
__init__method is called.
-
\$\begingroup\$ Thank you for your thorough analysis. I completely agree with almost all the points and have to say I never heard of the
inspectmodule 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\$magu_– magu_2016年09月26日 16:25:37 +00:00Commented Sep 26, 2016 at 16:25
You must log in to answer this question.
Explore related questions
See similar questions with these tags.