patternpythonMinor
Class decorator in Python to set variables for the constructor
Viewed 0 times
theconstructorpythonvariablesfordecoratorclassset
Problem
I personally don't like the boilerplate of most
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
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.assertR
__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_switcherSome 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.assertR
Solution
-
The parameters to
-
The function
-
The original
-
The implementation mechanism is quite ingenious! It would not have occurred to me to delegate the argument processing to
-
There's no test case that checks that the original
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.-
The function
filler is used to implement the __init__ method, so its first argument should be named self, not cls.-
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-
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-
There's no test case that checks that the original
__init__ method is called.Code Snippets
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 clsfrom 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_switcherContext
StackExchange Code Review Q#142073, answer score: 4
Revisions (0)
No revisions yet.