What is the best way to set the attributes of a cl

2019-06-01 07:06发布

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

4条回答
Rolldiameter
2楼-- · 2019-06-01 07:19

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楼-- · 2019-06-01 07:26

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
查看更多
放荡不羁爱自由
4楼-- · 2019-06-01 07:31

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
查看更多
Juvenile、少年°
5楼-- · 2019-06-01 07:33

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.

查看更多
登录 后发表回答