【问题标题】:What is the best way to set the attributes of a class at object creation time in Python?在 Python 中创建对象时设置类属性的最佳方法是什么?
【发布时间】:2017-12-16 16:28:22
【问题描述】:

当我定义一个类时,我经常想在创建对象时为该类设置一组属性。到目前为止,我通过将属性作为参数传递给 init 方法来做到这一点。但是,我对此类代码的重复性感到不满:

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
    ...

在这个例子中,我必须输入三遍name,三遍gitOriginURI,三遍gitCommitHash,三遍temporary,三遍sourceDir。只需设置这些属性。这是非常无聊的代码。

我考虑过像这样更改类以遵循以下原则:

class Foo():
  def __init__(self):
    self.a = None
    self.b = None
    self.c = None

并像这样初始化它们的对象:

f = Foo()
f.a = whatever
f.b = something_else
f.c = cheese

但从文档的角度来看,这似乎更糟,因为类的用户需要知道需要设置哪些属性,而不是简单地查看类初始化器的自动生成的 help() 字符串。

有没有更好的方法来做到这一点?

我认为可能是一个有趣的解决方案,如果有一个store_args_to_self() 方法可以将传递给init 的每个参数存储为self 的属性。有这样的方法吗?

让我对这种寻求更好方法感到悲观的一件事是,例如,查看 cPython 源代码中 date 对象的源代码时,我看到了相同的重复代码:

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

而urwid,虽然被setter的使用稍微混淆了,但也有这样的“接受一个参数并将其设置为self的属性”的热土豆代码:

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

【问题讨论】:

  • 我不明白将变量名写三遍有什么问题。主要优点是它在__init__ 的文档中。因此,如果您使用 IDE,它会在稍后提示名称。现在有些工作,以后会受益匪浅。此外,您不会立即分配给属性:您可能首先要验证值,然后进行一些处理,然后将属性设置为该处理的结果。

标签: python class attributes


【解决方案1】:

您可以使用dataclasses project 让它负责为您生成__init__ 方法;它还将处理表示、散列和相等性测试(以及可选的丰富比较和不变性):

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

dataclassesPEP 557 - Data Classes 中定义,已被接受包含在 Python 3.7 中。该库将在 Python 3.6 及更高版本上运行(因为它依赖于 3.6 中引入的新变量注释语法)。

该项目的灵感来自 attrs project,它提供了更多的灵活性和选项,以及与 Python 2.7 和 Python 3.4 及更高版本的兼容性。

【讨论】:

  • 这看起来正是我想要的。它是一个外部库太糟糕了,但对于我当前的项目,我可以接受。你知道是否可以在不指定属性类型的情况下制作数据类?
  • @timthelion: 不,dataclasses 专门寻找用variable annotations 定义的类变量。如果这不适合您的项目,请使用 attrs。不过,该库将成为 Python 3.7 及更高版本的一部分。
【解决方案2】:

我将在这里留下另一个食谱。 attrs 很有用,但也有缺点,主要是缺少对 __init__ 类的 IDE 建议。

拥有初始化链也很有趣,我们使用父类的实例作为__init__ 的第一个参数,而不是一个一个地提供它的所有属性。

所以我建议使用简单的装饰器。它分析__init__ 签名并基于它自动添加类属性(因此方法与attrs 的方法相反)。这给了我们很好的__init__ IDE 建议(但缺乏关于属性本身的建议)。

用法:

@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__

来源:

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

【讨论】:

    【解决方案3】:

    好吧,你可以这样做:

    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)
    

    输出

    1 two iii
    

    但是,如果您这样做了,最好在将 kwargs 中的密钥转储到您的实例 __dict__ 之前检查它们是否正常。 ;)

    这是一个稍微花哨的示例,它对传入的参数进行了一些检查。

    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)
    

    输出

    Foo(a=1, b=None, c=iii)
    

    【讨论】:

    • 感谢您的回答。虽然我可以做到这一点,但它与我给出的第二种方法会有同样的问题。类的 help() 字符串中没有相应属性的文档。
    • @timthelion 明白了。您可以通过提供允许的属性列表来改进此方法,如我的第二个示例所示。您可以通过添加一些类型检查使其更加健壮。但是您仍然需要编写适当的文档字符串。然而,Martijn 的建议 更胜一筹,正如他所提到的,dataclasses 将成为 Python 3.7 中标准库的一部分。
    【解决方案4】:

    对于 Python 2.7,我的解决方案是从 namedtuple 继承并将 namedtuple 本身用作 init 的唯一参数。为了避免每次都重载 new ,我们可以使用装饰器。优点是我们有明确的 init 签名 w/o *args, **kwargs 以及很好的 IDE 建议

    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
    

    【讨论】:

      猜你喜欢
      • 2020-11-27
      • 2013-11-12
      • 2021-02-13
      • 2023-03-13
      • 1970-01-01
      • 2011-09-18
      • 1970-01-01
      • 1970-01-01
      • 2020-05-28
      相关资源
      最近更新 更多