如何避免“self.x = x;self.y = y;self.z = z“ 模式在__init__?

How do I avoid the "self.x = x; self.y = y; self.z = z" pattern in __init__?

提问人:MWB 提问时间:2/4/2016 最后编辑:Karl KnechtelMWB 更新时间:9/23/2022 访问量:15937

问:

我看到这样的模式

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

相当频繁,通常具有更多参数。有没有好方法可以避免这种乏味的重复?该类是否应该继承自?namedtuple

python-dataclasses named元组 python-attrs

评论

31赞 chepner 2/4/2016
并非所有的接受能力都是坏的。请记住,Python 的类模型不包括实例属性的显式定义,因此这些赋值是自记录的等效项。
4赞 ShadowRanger 2/4/2016
@chepner:嗯,不需要明确的定义。不过,您可以将__slots__用于此目的;它有点不蟒蛇(为了节省内存而更冗长),但我喜欢它很大程度上是为了避免如果我打错名称就会自动激活一个全新的属性的风险。
2赞 Gere 2/4/2016
任何好的编辑器都会有模板。你打字就完成了。ini <shortcut> x, y, z): <shortcut>
3赞 RemcoGerlich 2/4/2016
如果你想要一个不可变的值对象,Namedtuples 很棒。如果你想要一个常规的、可变的类,你就不能使用它们。
4赞 Kroltan 2/5/2016
“Don't”是一个不错的选择,任何可用的选项都会杀死方法签名(因此可能杀死整个接口)。此外,如果您的类有大量字段需要初始化,您可能需要考虑拆分它们。

答:

29赞 Joran Beasley 2/4/2016 #1

显性总比隐性好...... 所以可以肯定的是,你可以让它更简洁:

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

更好的问题是:你应该吗?

...也就是说,如果你想要一个命名的元组,我建议使用一个命名的元组(记住,元组附加了某些条件)......也许你想要一个 OrderedDict,甚至只是一个 dict ......

评论

0赞 John La Rooy 2/4/2016
然后,该对象将需要循环垃圾回收,因为它本身是一个属性
3赞 cat 2/4/2016
@bernie(或者是贝米?),有时 ke r ning 很难
4赞 ShadowRanger 2/4/2016
对于稍微更有效的测试,可以改为廉价的同一性测试,而不是字符串比较。我想从技术上讲,可以在构造后第二次调用并作为后续参数通过,但我真的不想去想什么样的怪物会这样做。:-)if k != "self":if v is not self:__init__self
0赞 Lii 2/17/2016
这可以做成一个函数,该函数的返回值为: 。那么它就不再比基于更神奇的装饰器的解决方案更长了。localsset_fields_from_locals(locals())
108赞 gruszczy 2/4/2016 #2

免責聲明:似乎有几个人担心提出这个解决方案,所以我将提供一个非常明确的免责声明。不应使用此解决方案。我只提供它作为信息,所以你知道这门语言能够做到这一点。答案的其余部分只是展示语言能力,而不是认可以这种方式使用它们。


将参数显式复制到属性中实际上没有任何问题。如果 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赞 Joran Beasley 2/4/2016
好答案 +1 ...虽然可能稍微多一些 pythonicself.__dict__.update(kwargs)
45赞 MWB 2/4/2016
这种方法的问题在于,没有参数实际预期的记录,也没有对输入错误的参数名称进行错误检查。A.__init__
7赞 Pedro 2/4/2016
@JoranBeasley 盲目地更新实例字典会使您面临相当于 SQL 注入攻击的风险。如果你的对象有一个名为的方法,并且你将一个命名的参数传递给构造函数,那么字典,你只是覆盖了这个方法。kwargsmy_methodmy_methodupdate()
3赞 Gere 2/4/2016
正如其他人所说,这个建议确实是糟糕的编程风格。它隐藏了关键信息。您可以展示它,但您应该明确阻止 OP 使用它。
3赞 gerrit 2/4/2016
@Pedro gruzczy 和 JoranBeasley 的语法之间有什么语义差异吗?
10赞 zondo 2/4/2016 #3

您还可以执行以下操作:

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

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

29赞 A Small Shell Script 2/4/2016 #4

正如其他人所提到的,重复还不错,但在某些情况下,命名元组可能非常适合此类问题。这避免了使用 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赞 PaulMcG 2/4/2016
这些是元组,因此您的方法可以编写为 just ,如果将来将更多字段添加到 namedtuple 定义中,这将更易于维护。propertiesreturn tuple(self)
1赞 PaulMcG 2/4/2016
此外,namedtuple 声明字符串不需要在字段名称之间使用逗号,同样有效。XYZ = namedtuple("XYZ", "x y z")
0赞 A Small Shell Script 2/4/2016
谢谢@PaulMcGuire。我试图想出一个非常简单的附加组件来显示继承和间隔。你是 100% 正确的,它也是其他继承对象的一个很好的速记!我确实提到字段名称可以用逗号或空格分隔 - 我更喜欢习惯中的CSV
1赞 detly 2/4/2016
我经常将 s 用于此目的,尤其是在数学代码中,其中函数可能是高度参数化的,并且具有一堆只能在一起才有意义的系数。namedtuple
0赞 hamstergene 2/4/2016
问题是它们是只读的。你不能做或类似的事情。namedtupleabc.x += 1
88赞 Siphor 2/4/2016 #5

编辑: 如果你有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赞 MWB 2/4/2016
不错的答案,但不适用于 python2.7:否signature
3赞 Siphor 2/5/2016
@alexis “decorator.decorator” 装饰器会自动包装函数
4赞 Kyle Strand 2/5/2016
我很纠结是爱这个还是恨它。我很感激保留签名。
14赞 2/10/2016
"...显式比隐式好。简单总比复杂好。...“ -Python 的禅宗
13赞 Ian Newson 2/11/2016
-1 坦率地说,这太可怕了。我一眼就不知道这段代码在做什么,它实际上是代码量的十倍。聪明感觉很酷,但这是对你明显聪明的滥用。
21赞 gerrit 2/4/2016 #6

为了扩展答案,我使用了如下模式: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 之前,这无法控制属性的设置顺序,如果某些属性是具有访问其他属性的 setter 的属性,则这可能是一个问题。

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

8赞 Mike Müller 2/6/2016 #7

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

Helper 函数

一个小小的助手功能,让它更加方便和可重用:

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)

评论

0赞 MWB 12/11/2016
不应修改 docs.python.org/2/library/functions.html#locals(它可能会影响解释器,在您的情况下,从调用函数的作用域中删除)locals()self
0赞 Mike Müller 12/11/2016
@MaxB 从您引用的文档中:......更改可能不会影响解释器使用的局部变量和自由变量的值。 在 中仍然可用。self__init__
0赞 MWB 12/11/2016
是的,读者希望它会影响局部变量,但它可能会也可能不会,这取决于许多情况。关键是它是UB。
0赞 MWB 12/11/2016
引用:“本词典的内容不应修改”
0赞 Mike Müller 12/11/2016
@MaxB我添加了一个不更改 locals() 的版本。
5赞 bgusach 2/10/2016 #8

我的 0.02 美元。它与乔兰·比斯利的回答非常接近,但更优雅:

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)

此外,Mike Müller 的答案(最适合我的口味)可以用这种技术来减少:

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

而你的电话auto_init(locals())__init__

评论

1赞 MWB 12/11/2016
不应修改 docs.python.org/2/library/functions.html#locals(未定义的行为)locals()
7赞 RafG 1/6/2017 #9

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

import attr

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

你甚至不再需要一种方法,除非它也能做其他事情。这是 Glyph Lefkowitz 的一个很好的介绍__init__

评论

0赞 gerrit 6/27/2018
数据类在多大程度上使功能变得冗余?attr
2赞 Ivo Merchiers 3/28/2019
@gerrit attrs 软件包的文档中对此进行了讨论。呸呸呸,差异似乎不再那么大了。
8赞 gerrit 6/27/2018 #10

Python 3.7 及更高版本

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

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

在这些生成的方法中使用的成员变量是使用 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 定义中指定。

如果您的类很大且很复杂,则使用 .我是在 Python 3.7.0 发布当天写这篇文章的,所以使用模式还没有很好地建立起来。dataclass

评论

0赞 Karl Knechtel 9/23/2022
Since 3.6 and below are now end-of-life, this should be the preferred answer.