When I define a class, I often want to set a collection of attributes for that class upon object creation. Until now, I have done so by passing the attributes as arguments to the init method. However, I have been unhappy with the repetitive nature of such code:
class Repository(OrderedDict,UserOwnedObject,Describable):
def __init__(self,user,name,gitOriginURI=None,gitCommitHash=None,temporary=False,sourceDir=None):
self.name = name
self.gitOriginURI = gitOriginURI
self.gitCommitHash = gitCommitHash
self.temporary = temporary
self.sourceDir = sourceDir
...
In this example, I have to type name
three times, gitOriginURI
three times, gitCommitHash
three times, temporary
three times, and sourceDir
three times. Just to set these attributes. This is extremely boring code to write.
I've considered changing classes like this to be along the lines of:
class Foo():
def __init__(self):
self.a = None
self.b = None
self.c = None
And initializing their objects like:
f = Foo()
f.a = whatever
f.b = something_else
f.c = cheese
But from a documentation standpoint, this seems worse, because the user of the class then needs to know which attributes need to be set, rather than simply looking at the autogenerated help()
string for the class's initializer.
Are there any better ways to do this?
One thing that I think might be an interesting solution, would be if there was a store_args_to_self()
method which would store every argument passed to init as an attribute to self. Does such a method exist?
One thing that makes me pessimistic about this quest for a better way, is that looking at the source code for the date
object in cPython's source, for example, I see this same repetitive code:
def __new__(cls, year, month=None, day=None):
...
self._year = year
self._month = month
self._day = day
https://github.com/python/cpython/blob/master/Lib/datetime.py#L705
And urwid, though slightly obfuscated by the use of setters, also has such "take an argument and set it as an attribute to self" hot-potato code:
def __init__(self, caption=u"", edit_text=u"", multiline=False,
align=LEFT, wrap=SPACE, allow_tab=False,
edit_pos=None, layout=None, mask=None):
...
self.__super.__init__("", align, wrap, layout)
self.multiline = multiline
self.allow_tab = allow_tab
self._edit_pos = 0
self.set_caption(caption)
self.set_edit_text(edit_text)
if edit_pos is None:
edit_pos = len(edit_text)
self.set_edit_pos(edit_pos)
self.set_mask(mask)
https://github.com/urwid/urwid/blob/master/urwid/widget.py#L1158
Well, you could do this:
output
But if you do, it's probably a Good Idea to check that the keys in
kwargs
are sane before dumping them into your instances__dict__
. ;)Here's a slightly fancier example that does a little bit of checking of the passed-in args.
output
I'll just leave another one recipe here.
attrs
is useful, but have cons, main of which is lack of IDE suggestions for class__init__
.Also it's fun to have initialization chains, where we use instance of parent class as first arg for
__init__
instead of providing all it's attrs one by one.So I propose the simple decorator. It analyses
__init__
signature and automatically adds class attributes, based on it (so approach is opposite to attrs's one). This gave us nice IDE suggestions for__init__
(but lack of suggestions on attributes itself).Usage:
Source:
For Python 2.7 my solution is to inherit from namedtuple and use namedtuple itself as only argument to init. To avoid overloading new every time we can use decorator. The advantage is that we have explicit init signature w/o *args, **kwargs and, so, nice IDE suggestions
You could use the
dataclasses
project to have it take care of generating the__init__
method for you; it'll also take care of a representation, hashing and equality testing (and optionally, rich comparisons and immutability):dataclasses
were defined in PEP 557 - Data Classes, which has been accepted for inclusion in Python 3.7. The library will work on Python 3.6 and up (as it relies on the new variable annotation syntax introduced in 3.6).The project was inspired by the
attrs
project, which offers some more flexibility and options still, as well as compatibility with Python 2.7 and Python 3.4 and up.