web-dev-qa-db-ja.com

__init__の「self.x = x; self.y = y; self.z = z」パターンを回避するにはどうすればよいですか?

次のようなパターンが表示されます

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

非常に頻繁に、多くの場合より多くのパラメータを使用します。この種の退屈な繰り返しを避ける良い方法はありますか?クラスは代わりにnamedtupleを継承する必要がありますか?

169
MaxB

編集: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__))
89
Siphor

免責事項:このソリューションの提示に懸念を持っている人がいるようです。そのため、非常に明確な免責事項を提供します。このソリューションを使用しないでください。私はそれを情報として提供するだけなので、あなたはその言語がこれに対応していることを知っています。残りの答えは、このように使用することを推奨するのではなく、単に言語の機能を示すことです。


パラメーターを属性に明示的にコピーすることには、実際のところ何も問題はありません。 ctorのパラメーターが多すぎる場合、コードのにおいと見なされる場合があり、これらのパラメーターをより少ないオブジェクトにグループ化する必要があります。それ以外の場合、それは必要であり、それは何も悪いことではありません。 とにかく、明示的にそれを行うことは行く方法です。

ただし、どのように行うことができるかを尋ねているので(行うべきかどうかではありません)、1つの解決策は次のとおりです。

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
108
gruszczy

他の人が述べたように、繰り返しは悪くありませんが、名前付きタプルがこの種の問題にぴったりである場合があります。これにより、通常は悪い考えである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()
29

明示的は暗黙的よりも優れているため、より簡潔にすることができます。

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

より良い質問はあなたがすべきですか?

...名前付きタプルが必要な場合は、namedtupleを使用することをお勧めします(タプルには特定の条件が関連付けられていることを覚えておいてください)...おそらくorderdictまたはさらにdictだけが必要です...

29
Joran Beasley

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がより適切でしょう。

20
gerrit

次のこともできます。

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

もちろん、inspectモジュールをインポートする必要があります。

10
zondo

これは、追加のインポートなしのソリューションです。

ヘルパー機能

小さなヘルパー関数により、より便利で再利用可能になります。

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)
8
Mike Müller

これを処理する(そして他の多くの定型文を回避する)興味深いライブラリは attrs です。たとえば、あなたの例はこれに減らすことができます(クラスがMyClassと呼ばれると仮定します):

import attr

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

__init__メソッドは、他のこともしない限り、もう必要ありません。 Glyph Lefkowitzによる素敵な紹介 です。

7
RafG

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

そして、__init__からauto_init(locals())を呼び出すだけです

5
bgusach

Pythonで物事を行うのは自然な方法です。もっと賢いものを発明しようとしないでください。あなたのチームの誰もが理解できないような過度に賢いコードにつながります。チームプレーヤーになりたい場合は、この方法で書き続けてください。

3
Peter Krumins

Python 3.7以降

Python 3.7では、dataclassモジュールから利用可能な dataclasses デコレーターを(ab)使用できます。ドキュメントから:

このモジュールは、__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のリリース日に書いているので、使用パターンはまだ十分に確立されていません。

3
gerrit