可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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
回答1:
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):
from dataclasses import dataclass
from typing import Optional
@dataclass
class Repository(OrderedDict, UserOwnedObject, Describable):
name: str
gitOriginURI: Optional[str] = None
gitCommitHash: Optional[str] = None
temporary: bool = False
sourceDir: Optional[str] = None
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.
回答2:
Well, you could do this:
class Foo:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
foo = Foo(a=1, b='two', c='iii')
print(foo.a, foo.b, foo.c)
output
1 two iii
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.
class Foo:
attrs = ['a', 'b', 'c']
''' Some stuff about a, b, & c '''
def __init__(self, **kwargs):
valid = {key: kwargs.get(key) for key in self.attrs}
self.__dict__.update(valid)
def __repr__(self):
args = ', '.join(['{}={}'.format(key, getattr(self, key)) for key in self.attrs])
return 'Foo({})'.format(args)
foo = Foo(a=1, c='iii', d='four')
print(foo)
output
Foo(a=1, b=None, c=iii)
回答3:
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
def nt_child(c):
def __new__(cls, p): return super(c, cls).__new__(cls, *p)
c.__new__ = staticmethod(__new__)
return c
ClassA_P = namedtuple('ClassA_P', 'a, b, foo, bar')
@nt_child
class ClassA(ClassA_P):
def __init__(self, p):
super(ClassA, self).__init__(*p)
self.something_more = sum(p)
a = ClassA(ClassA_P(1,2,3,4)) # a = ClassA(ClassA_P( <== suggestion a, b, foo, bar
print a.something_more # print a. <== suggesion a, b, foo, bar, something_more
回答4:
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:
@data_class
class A:
def __init__(self, foo, bar): pass
@data_class
class B(A):
# noinspection PyMissingConstructor
def __init__(self, a, red, fox):
self.red_plus_fox = red + fox
# do not call parent constructor, decorator will do it for you
a = A(1, 2)
print a.__attrs__ # {'foo': 1, 'bar': 2}
b = B(a, 3, 4) # {'fox': 4, 'foo': 1, 'bar': 2, 'red': 3, 'red_plus_fox': 7}
print b.__attrs__
Source:
from collections import OrderedDict
def make_call_dict(f, is_class_method, *args, **kwargs):
vnames = f.__code__.co_varnames[int(is_class_method):f.__code__.co_argcount]
defs = f.__defaults__ or []
d = OrderedDict(zip(vnames, [None] * len(vnames)))
d.update({vn: d for vn, d in zip(vnames[-len(defs):], defs)})
d.update(kwargs)
d.update({vn: v for vn, v in zip(vnames, args)})
return d
def data_class(cls):
inherited = hasattr(cls, '_fields')
if not inherited: setattr(cls, '_fields', None)
__init__old__ = cls.__init__
def __init__(self, *args, **kwargs):
d = make_call_dict(__init__old__, True, *args, **kwargs)
if inherited:
# tricky call of parent __init__
O = cls.__bases__[0] # put parent dataclass first in inheritance list
o = d.values()[0] # first arg in my __init__ is parent class object
d = OrderedDict(d.items()[1:])
isg = o._fields[O] # parent __init__ signature, [0] shows is he expect data object as first arg
O.__init__(self, *(([o] if isg[0] else []) + [getattr(o, f) for f in isg[1:]]))
else:
self._fields = {}
self.__dict__.update(d)
self._fields.update({cls: [inherited] + d.keys()})
__init__old__(self, *args, **kwargs)
cls.__attrs__ = property(lambda self: {k: v for k, v in self.__dict__.items()
if not k.startswith('_')})
cls.__init__ = __init__
return cls