如何避免在__init__中使用"self.x = x; self.y = y; self.z = z"模式?

173

我看到类似的模式

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

经常出现这种情况,通常还带有更多的参数。有没有好的方法来避免这种繁琐的重复?应该让类继承自namedtuple吗?


31
并非所有的接收性都是不好的。请记住,Python的类模型不包括实例属性的显式定义,因此这些赋值语句是自我记录的等效语句。 - chepner
4
@chepner:好的,不一定需要显式定义。你可以使用 __slots__ 来实现这个目的;虽然相对来说有点不符合 Python 的风格(更加冗长以实现内存节省),但我还是喜欢这种方法,主要是为了避免因为打错属性名而导致自动生成一个新的属性。 - ShadowRanger
2
任何好的编辑器都会有模板。你只需输入 ini <shortcut> x, y, z): <shortcut> 就可以完成了。 - Gere
3
如果你想要一个不可变的值对象,那么命名元组是非常棒的选择。但如果你需要一个普通的可变类,则无法使用它们。 - RemcoGerlich
4
“不”是一个好的选择,任何其他选项都会破坏方法签名(因此可能破坏整个接口)。另外,如果你的类有太多需要初始化的字段,你可能需要考虑将它们拆分。 - Kroltan
显示剩余7条评论
11个回答

108

免责声明:似乎有几个人对于呈现这个解决方案感到担忧,因此我将提供非常清晰的免责声明。您不应该使用这个解决方案。我只提供它作为信息,让您知道语言是有能力实现这一点的。其余的答案只是展示了语言的能力,而不是赞成以这种方式使用它们。


明确地将参数复制到属性中并没有什么问题。如果构造函数中有太多的参数,有时会被视为代码异味,也许你应该将这些参数分组成更少的对象。其他时候,这是必要的,然后这样做就没有任何问题了。无论如何,明确地完成这个任务是正确的方法。

然而,既然您正在询问如何完成它(而不是是否应该这样做),那么一个解决方案是:

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)可能更符合Python语言的习惯用法。 - Joran Beasley
45
这种方法的问题在于没有记录A.__init__实际需要哪些参数,并且也没有检查输入参数名是否有误。 - MWB
7
盲目使用kwargs更新实例字典会让你的代码容易受到类似于SQL注入攻击的攻击。 如果你的对象有一个名为my_method的方法,并且你在构造函数中传递了一个名为my_method的参数,然后又使用update()方法更新字典,那么你刚刚覆盖了这个方法。请注意保护你的代码免受此类攻击。 - Pedro
3
正如其他人所说,这种建议是很差的编程风格。它隐藏了关键信息。你可以展示它,但应该明确地劝阻提问者使用它。 - Gere
3
gruzczy和JoranBeasley的语法有什么语义上的区别吗? - gerrit
显示剩余7条评论

88

编辑:如果您使用的是Python 3.7+,只需使用dataclasses

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

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”。 - MWB
3
“decorator.decorator” 装饰器会自动包装函数。 - Siphor
4
我对于喜欢还是讨厌这个东西感到很矛盾。我确实赞赏保留原有的特色。 - Kyle Strand
14
"显式优于隐式。简单优于复杂。" - Python之禅 - user521945
13
坦率地说,这太糟糕了。一眼看去我不知道这段代码在做什么,而且代码行数是原来的十倍。虽然聪明很酷,但这是你聪明才智的误用。 - Ian Newson
显示剩余2条评论

29

显式优于隐式... 所以,确实可以使它更加简洁:

def __init__(self,a,b,c):
    for k,v in locals().items():
        if k != "self":
             setattr(self,k,v)
更好的问题是:你应该吗? ...话虽如此,如果你想要一个命名元组,我建议使用namedtuple(记住元组有特定的条件附加在它们上面)...也许你想要一个OrderedDict甚至只是一个dict...

那么该对象将需要循环垃圾回收,因为它本身作为一个属性。 - John La Rooy
3
@Bernie(或者是Bemie?),有时候克耐(Ke R Ning)很难。 - cat
4
为了使测试稍微更有效率,可以将if k != "self":改为使用便宜的身份测试if v is not self:,而不是进行字符串比较。我想从技术上讲,在构建后可能会再次调用__init__并将self作为接下来的参数传递,但我真的不想去想那种怪物。 :-) - ShadowRanger
这可以制作成一个函数,该函数接受locals的返回值:set_fields_from_locals(locals())。然后它就不会比更神奇的基于装饰器的解决方案更长了。 - Lii

29

如其他人所提到的,重复并不是坏事,但在某些情况下,namedtuple可能非常适合这种类型的问题。这避免了使用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

我发现它的用处有限,但你可以像任何其他对象一样继承一个具名元组(示例继续):

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方法可以简单地编写为return tuple(self),如果将来将更多字段添加到命名元组定义中,则更易于维护。 - PaulMcG
1
另外,你的namedtuple声明字符串不需要在字段名之间加逗号,XYZ = namedtuple("XYZ", "x y z")同样有效。 - PaulMcG
感谢@PaulMcGuire。我一直在尝试想出一个非常简单的附加组件来显示继承关系,但有点忘了。你是100%正确的,这也是其他继承对象的很好的速记方式!我确实提到字段名称可以用逗号或空格分隔 - 我习惯使用CSV。 - A Small Shell Script
1
我经常在这种情况下使用namedtuple,特别是在数学代码中,其中一个函数可能高度参数化,并且有一堆系数只有在一起才有意义。 - detly
namedtuple 的问题在于它们是只读的。你不能执行 abc.x += 1 或者类似的操作。 - hamstergene
@hamstergene:这完全是一件好事。 ;) 唯一的缺点是你必须编写自己的“update”方法(返回一个更新了特定字段的新副本)。 - mike3996

21
为了进一步解释gruszczy的回答,我使用了这样的模式:

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)

https://docs.python.org/2/library/functions.html#locals locals() 不应被修改(这可能会影响解释器,在您的情况下,从调用函数的作用域中删除 self)。 - MWB
从您引用的文档中可以看出:*...更改可能不会影响解释器使用的局部和自由变量的值。* self__init__ 中仍然可用。 - Mike Müller
正确,读者期望它会影响局部变量,但这取决于许多情况,可能会影响也可能不会。关键是这是未定义行为。 - MWB
“这个字典的内容不应该被修改。” - MWB
@MaxB 我增加了一个版本,它不会改变 locals()。 - Mike Müller

8

Python 3.7 及以上版本

在 Python 3.7 中,你可以使用位于 dataclasses 模块中的 dataclass 装饰器(滥用)。以下是官方文档的描述:

This module provides a decorator and functions for automatically adding generated special methods such as __init__() and __repr__() to user-defined classes. It was originally described in PEP 557.

The member variables to use in these generated methods are defined using PEP 526 type annotations. For example this code:

@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

Will add, among other things, a __init__() that looks like:

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

Note that this method is automatically added to the class: it is not directly specified in the InventoryItem definition shown above.

如果你的类很大且复杂,使用 dataclass 可能不合适。 我是在发布Python 3.7.0当天写这篇文章的,因此使用模式尚未得到很好的确定。

由于3.6及以下版本已经停止维护,因此这应该是首选答案。 - Karl Knechtel

7

一个有趣的库可以处理这个问题(同时避免了许多其他样板文件)是attrs。例如,您的示例可以简化为以下内容(假设类名为MyClass):

import attr

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

现在你甚至不需要一个__init__方法了,除非它还做其他事情。这里有一个由Glyph Lefkowitz提供的很好的介绍


dataclasses模块对attr的功能有多少冗余? - gerrit
2
@gerrit 这在 attrs 包的文档 中有讨论。说实话,这些差异似乎已经不那么明显了。 - 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)

此外,迈克·米勒的答案(我认为是最好的答案)可以使用这种技术来简化:
def auto_init(ns):
    self = ns.pop('self')
    vars(self).update(ns)

"并且只需在您的 __init__ 中调用 auto_init(locals()) 即可。"

1
不应修改locals()(未定义行为)。 - MWB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接