标签归档:python-attrs

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

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

我看到像

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

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

I see patterns like

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

quite frequently, often with a lot more parameters. Is there a good way to avoid this type of tedious repetitiveness? Should the class inherit from namedtuple instead?


回答 0

编辑:如果您有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__))

Edit: If you have python 3.7+ just use dataclasses

A decorator solution that keeps the signature:

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

回答 1

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


明确地将参数复制到属性中并没有什么错。如果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

Disclaimer: It seems that several people are concerned about presenting this solution, so I will provide a very clear disclaimer. You should not use this solution. I only provide it as information, so you know that the language is capable of this. The rest of the answer is just showing language capabilities, not endorsing using them in this way.


There isn’t really anything wrong with explicitly copying parameters into attributes. If you have too many parameters in the ctor, it is sometimes considered a code smell and maybe you should group these params into a fewer objects. Other times, it is necessary and there is nothing wrong with it. Anyway, doing it explicitly is the way to go.

However, since you are asking HOW it can be done (and not whether it should be done), then one solution is this:

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

回答 2

正如其他人所提到的,重复并不坏,但在某些情况下,命名元组可能非常适合此类问题。这样可以避免使用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()

As others have mentioned, the repetition isn’t bad, but in some cases a namedtuple can be a great fit for this type of issue. This avoids using locals() or kwargs, which are usually a bad idea.

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

I’ve found limited use for it, but you can inherit a namedtuple as with any other object (example continued):

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()

回答 3

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

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

更好的问题是您?

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

explicit is better than implicit … so sure you could make it more concise:

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

The better question is should you?

… that said if you want a named tuple I would recommend using a namedtuple (remember tuples have certain conditions attached to them) … perhaps you want an ordereddict or even just a dict …


回答 4

为了扩展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更合适。

To expand on gruszczys answer, I have used a pattern like:

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

I like this method because it:

  • avoids repetition
  • is resistant against typos when constructing an object
  • works well with subclassing (can just super().__init(...))
  • allows for documentation of the attributes on a class-level (where they belong) rather than in X.__init__

Prior to Python 3.6, this gives no control over the order in which the attributes are set, which could be a problem if some attributes are properties with setters that access other attributes.

It could probably be improved upon a bit, but I’m the only user of my own code so I am not worried about any form of input sanitation. Perhaps an AttributeError would be more appropriate.


回答 5

您也可以这样做:

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

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

You could also do:

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

Of course, you would have to import the inspect module.


回答 6

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

辅助功能

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

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)

This is a solution without any additional imports.

Helper function

A small helper function makes it more convenient and re-usable:

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)

Application

You need to call it with locals():

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

Test

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

Output:

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

Without changing locals()

If you don’t like to change locals() use this version:

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)

回答 7

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

import attr

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

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

An interesting library that handles this (and avoids a lot of other boilerplate) is attrs. Your example, for instance, could be reduced to this (assume the class is called MyClass):

import attr

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

You don’t even need an __init__ method anymore, unless it does other stuff as well. Here’s a nice introduction by Glyph Lefkowitz.


回答 8

我的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__

My 0.02$. It is very close to Joran Beasley answer, but more elegant:

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)

Additionally, Mike Müller’s answer (the best one to my taste) can be reduced with this technique:

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

And the just call auto_init(locals()) from your __init__


回答 9

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

It’s a natural way to do things in Python. Don’t try to invent something more clever, it will lead to overly clever code that no one on your team will understand. If you want to be a team player and then keep writing it this way.


回答 10

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发行之日就在写这篇文章,因此用法模式尚未很好地建立。

Python 3.7 onwards

In Python 3.7, you may (ab)use the dataclass decorator, available from the dataclasses module. From the documentation:

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.

If your class is large and complex, it may be inappropriate to use a dataclass. I’m writing this on the day of release of Python 3.7.0, so usage patterns are not yet well established.