我如何避免“ self.x = x; self.y = y; __init__中的self.z = z”模式?


170

我看到像

def __init__(self, x, y, z):
    ...
    self.x = x
    self.y = y
    self.z = z
    ...

非常频繁,通常带有更多参数。是否有避免这种乏味重复的好方法?该类应该继承namedtuple吗?


31
并非所有的接受能力都是不好的。请记住,Python的类模型不包括实例属性的显式定义,因此这些分配是自记录的等效项。
chepner '16

4
@chepner:好吧,不需要显式定义。您可以__slots__为此目的使用; 这有点不讲究Python(为了节省内存而更加冗长),但我在很大程度上喜欢它,以避免输入名称时自动保留整个新属性的风险。
ShadowRanger

2
任何好的编辑器都会有模板。您输入ini <shortcut> x, y, z): <shortcut>并完成。
Gerenuk '16

3
如果您想要一个不可变的值对象,命名元组很棒。如果要使用常规的可变类,则不能使用它们。
RemcoGerlich

4
“不要”是一个很好的选择,任何可用的选择都会杀死方法签名(从而可能杀死整个接口)。此外,如果您的类中有太多无法初始化的字段,则可能需要考虑拆分它们。
Kroltan '02

Answers:


87

编辑:如果您有python 3.7+,只需使用数据类

保留签名的装饰器解决方案:

import decorator
import inspect
import sys


@decorator.decorator
def simple_init(func, self, *args, **kws):
    """
    @simple_init
    def __init__(self,a,b,...,z)
        dosomething()

    behaves like

    def __init__(self,a,b,...,z)
        self.a = a
        self.b = b
        ...
        self.z = z
        dosomething()
    """

    #init_argumentnames_without_self = ['a','b',...,'z']
    if sys.version_info.major == 2:
        init_argumentnames_without_self = inspect.getargspec(func).args[1:]
    else:
        init_argumentnames_without_self = tuple(inspect.signature(func).parameters.keys())[1:]

    positional_values = args
    keyword_values_in_correct_order = tuple(kws[key] for key in init_argumentnames_without_self if key in kws)
    attribute_values = positional_values + keyword_values_in_correct_order

    for attribute_name,attribute_value in zip(init_argumentnames_without_self,attribute_values):
        setattr(self,attribute_name,attribute_value)

    # call the original __init__
    func(self, *args, **kws)


class Test():
    @simple_init
    def __init__(self,a,b,c,d=4):
        print(self.a,self.b,self.c,self.d)

#prints 1 3 2 4
t = Test(1,c=2,b=3)
#keeps signature
#prints ['self', 'a', 'b', 'c', 'd']
if sys.version_info.major == 2:
    print(inspect.getargspec(Test.__init__).args)
else:
    print(inspect.signature(Test.__init__))

2
不错的答案,但不适用于python2.7:否signature
MaxB

3
@alexis“ decorator.decorator”装饰器会自动包装该函数
Siphor

4
我是喜欢还是讨厌这个话题,我感到非常沮丧。我非常感谢保留签名。
凯尔·史兰德

14
“ ...显式胜于隐式。简单胜于复杂。...”
Jack Stout,

9
-1坦白地说,这太可怕了。我不知道这些代码的功能一目了然,它实际上是代码量的十倍。聪明感觉很酷,但这是对您明显的聪明的滥用。
伊恩·纽森

108

免责声明:似乎有些人担心提出此解决方案,因此我将提供一个非常明确的免责声明。您不应该使用此解决方案。我仅将其作为信息提供,因此您知道该语言可以做到这一点。剩下的答案只是显示语言功能,而不是认可以这种方式使用它们。


明确地将参数复制到属性中并没有什么错。如果ctor中的参数太多,有时会被认为是代码异味,也许您应该将这些参数分组到更少的对象中。在其他时候,这是必要的,没有错。无论如何,明确地做到这一点是必须的。

但是,由于您要问如何完成(而不是是否应该这样做),因此一种解决方案是:

class A:
    def __init__(self, **kwargs):
        for key in kwargs:
          setattr(self, key, kwargs[key])

a = A(l=1, d=2)
a.l # will return 1
a.d # will return 2

16
不错的答案+1 ...尽管self.__dict__.update(kwargs)可能稍微有点蟒蛇味
Joran Beasley

44
这种方法的问题在于,没有关于参数A.__init__实际期望值的记录,也没有错误键入参数名称的错误检查。
MaxB

7
@JoranBeasley盲目地更新实例字典,kwargs让您处于开放状态,相当于SQL注入攻击。如果您的对象有一个名为的方法,my_method并且将一个名为的参数传递my_method给构造函数,则update()该字典将覆盖该方法。
佩德罗

3
就像其他人说的那样,建议实际上是糟糕的编程风格。它隐藏了关键信息。您可以显示它,但是您应该明确阻止OP使用它。
Gerenuk '16

3
@Pedro gruzczy和JoranBeasley的语法之间在语义上有区别吗?
gerrit

29

正如其他人所提到的,重复并不坏,但在某些情况下,命名元组可能非常适合此类问题。这样可以避免使用locals()或kwargs,这通常不是一个好主意。

from collections import namedtuple
# declare a new object type with three properties; x y z
# the first arg of namedtuple is a typename
# the second arg is comma-separated or space-separated property names
XYZ = namedtuple("XYZ", "x, y, z")

# create an object of type XYZ. properties are in order
abc = XYZ("one", "two", 3)
print abc.x
print abc.y
print abc.z

我发现它的用途有限,但是您可以像其他任何对象一样继承一个namedtuple(示例继续):

class MySuperXYZ(XYZ):
    """ I add a helper function which returns the original properties """
    def properties(self):
        return self.x, self.y, self.z

abc2 = MySuperXYZ(4, "five", "six")
print abc2.x
print abc2.y
print abc2.z
print abc2.properties()

5
这些元组,因此您的properties方法可以编写为just return tuple(self),如果将来将更多字段添加到namedtuple定义中,则该方法将更易于维护。
PaulMcG '16

1
同样,您的namedtuple声明字符串不需要在字段名之间使用逗号,XYZ = namedtuple("XYZ", "x y z")效果也一样。
PaulMcG '16

谢谢@PaulMcGuire。我试图考虑一个非常简单的附加组件,以显示其继承性和种类。您是100%正确的,这也是与其他继承对象的绝妙速记!我提的字段名称可以是逗号或空格分隔-我喜欢CSV从习惯
一个小的shell脚本

1
我经常将namedtuples用于此精确目的,尤其是在数学代码中,其中一个函数可能是高度参数化的,并且具有一堆只有在一起才有意义的系数。
熟练地

问题namedtuple在于它们是只读的。您不能做abc.x += 1或类似的事情。
hamstergene '16

29

显式比隐式更好...因此,请确保您可以使其更简洁:

def __init__(self,a,b,c):
    for k,v in locals().items():
        if k != "self":
             setattr(self,k,v)

更好的问题是您?

...这就是说,如果您想要一个命名元组,我建议您使用namedtuple(记住元组具有某些附加条件)...也许您想要一个有序的字典甚至是一个字典...


然后,由于该对象本身具有属性,因此需要循环垃圾回收
John La Rooy

3
@bernie(或者是bemie?),有时柯[R宁是很难

4
对于稍微更有效的测试,if k != "self":可以将其更改为if v is not self:便宜的身份测试,而不是字符串比较。从技术上讲,我想__init__在构造之后可以第二次调用它,并self作为随后的论点传递,但是我真的不想考虑哪种怪物会这样做。:-)
ShadowRanger

那可以做成一个函数的返回值localsset_fields_from_locals(locals())。然后,它不再是基于魔术师的解决方案。
Lii 2016年

20

为了扩展gruszczys的答案,我使用了类似的模式:

class X:
    x = None
    y = None
    z = None
    def __init__(self, **kwargs):
        for (k, v) in kwargs.items():
            if hasattr(self, k):
                setattr(self, k, v)
            else:
                raise TypeError('Unknown keyword argument: {:s}'.format(k))

我喜欢这种方法,因为它:

  • 避免重复
  • 构造对象时可以抵抗拼写错误
  • 可以很好地与子类化(只需super().__init(...)
  • 允许在类级别(它们所属的地方)而不是在 X.__init__

在Python 3.6之前,这无法控制属性的设置顺序,如果某些属性是带有访问其他属性的设置器的属性,则可能会出现问题。

可能会有所改善,但是我是我自己的代码的唯一用户,因此我不担心任何形式的输入卫生。也许AttributeError更合适。


10

您也可以这样做:

locs = locals()
for arg in inspect.getargspec(self.__init__)[0][1:]:
    setattr(self, arg, locs[arg])

当然,您将必须导入inspect模块。


8

这是一个无需任何其他导入的解决方案。

辅助功能

一个小的辅助函数使它更加方便和可重复使用:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    self = local_name_space.pop('self')
    for name, value in local_name_space.items():
        setattr(self, name, value)

应用

您需要使用以下命令调用它locals()

class A:
    def __init__(self, x, y, z):
        auto_init(locals())

测试

a = A(1, 2, 3)
print(a.__dict__)

输出:

{'y': 2, 'z': 3, 'x': 1}

不变 locals()

如果您不想更改,请locals()使用以下版本:

def auto_init(local_name_space):
    """Set instance attributes from arguments.
    """
    for name, value in local_name_space.items():
        if name != 'self': 
            setattr(local_name_space['self'], name, value)

docs.python.org/2/library/functions.html#locals locals()不应修改(在您的情况下,它可能会影响解释器,self从调用函数的作用域中删除)
MaxB

@MaxB您从文档中引用:...更改可能不会影响解释器使用的局部变量和自由变量的值。 self仍可在中使用__init__
MikeMüller2016年

是的,读者希望它会影响局部变量,但它可能会或不会(取决于许多情况)。关键是它是UB。
MaxB

Quote:“此字典的内容不应该被修改”
MaxB

@MaxB我添加了一个不会更改locals()的版本。
迈克·米勒

7

一个有趣的库可以处理这个问题(并避免很多其他样板文件)是attrs。例如,您的示例可以简化为以下示例(假设该类称为MyClass):

import attr

@attr.s
class MyClass:
    x = attr.ib()
    y = attr.ib()
    z = attr.ib()

您甚至不需要任何__init__方法,除非它也执行其他操作。这是Glyph Lefkowitz的精彩介绍


attr冗余在什么程度上起作用dataclasses
Gerrit

1
@gerrit这在attrs软件包文档中进行了讨论。Tbh,差异似乎不再那么大。
Ivo Merchiers,

5

我的0.02 $。它与Joran Beasley的答案非常接近,但更为优雅:

def __init__(self, a, b, c, d, e, f):
    vars(self).update((k, v) for k, v in locals().items() if v is not self)

此外,可以使用以下技术来减少MikeMüller的答案(最适合我的口味):

def auto_init(ns):
    self = ns.pop('self')
    vars(self).update(ns)

auto_init(locals())您的来话__init__


1
docs.python.org/2/library/functions.html#locals locals()不应修改(未定义的行为)
MaxB

4

这是用Python做事的自然方法。不要尝试发明更聪明的东西,它会导致代码太聪明,而团队中没人会理解。如果您想成为团队合作者,然后继续以这种方式编写。


4

Python 3.7以上

在Python 3.7中,您可以(ab)使用模块dataclass提供的装饰器dataclasses。从文档中:

该模块提供了一个装饰器和一些函数,用于自动将生成的特殊方法(例如__init__()和)添加__repr__()到用户定义的类中。它最初在PEP 557中进行了描述。

这些生成的方法中使用的成员变量是使用PEP 526类型注释定义的。例如此代码:

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

除其他外,将添加__init__()如下所示的:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
      self.name = name
      self.unit_price = unit_price
      self.quantity_on_hand = quantity_on_hand

请注意,此方法会自动添加到类中:上面显示的InventoryItem定义中未直接指定此方法。

如果您的课程又大又复杂,那么使用可能是不合适的dataclass。我在Python 3.7.0发行之日就在写这篇文章,因此用法模式尚未很好地建立。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.