web-dev-qa-db-ja.com

__slots__でデータクラスをより適切に機能させるにはどうすればよいですか?

決定されました Python 3.7のデータクラスから__slots__の直接サポートを削除します。

それにもかかわらず、__slots__はデータクラスで引き続き使用できます。

from dataclasses import dataclass

@dataclass
class C():
    __slots__ = "x"
    x: int

ただし、__slots__の動作方法により、データクラスフィールドにデフォルト値を割り当てることはできません。

from dataclasses import dataclass

@dataclass
class C():
    __slots__ = "x"
    x: int = 1

これにより、エラーが発生します。

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: 'x' in __slots__ conflicts with class variable

__slots__フィールドとデフォルトのdataclassフィールドを連携させるにはどうすればよいですか?

この問題はデータクラスに固有のものではありません。競合するクラス属性は、スロット全体を踏みにじります。

class Failure:
    __slots__ = Tuple("xyz")
    x=1
# ERROR

これは単にスロットが機能する方法です。これを防ぐには、クラスの名前空間を変更する必要がありますbeforeクラスオブジェクトは、クラスオブジェクトメンバーの同じスロットで競合する2つの競合オブジェクトがないようにインスタンス化されます。

  • 指定された(デフォルト)値(またはフィールドオブジェクト)
  • スロットマシナリーによって作成されたメンバー記述子

このため、親クラスの__init_subclass__メソッドでは不十分であり、クラスデコレータでも不十分です。どちらの場合も、クラスオブジェクトはすでに作成されているためです。

スロットマシンがより柔軟になるように変更されるまで、私たちの唯一の選択肢はメタクラスを使用することです。

この問題を解決するために作成されたメタクラスは、少なくとも次の条件を満たしている必要があります。

  • 名前空間から競合するクラス属性/メンバーを削除します
  • クラスオブジェクトをインスタンス化して、スロット記述子を作成します
  • スロット記述子への参照を保存します
  • 以前に削除されたメンバーとその値をクラス__dict__に戻します(dataclass機械がそれらを見つけることができるように)
  • クラスオブジェクトをdataclassデコレータに渡します
  • スロット記述子をそれぞれの場所に復元します
  • また、多くのコーナーケース(__dict__スロットがある場合の対処方法など)も考慮に入れてください。

控えめに言っても、これは非常に複雑な取り組みです。次のようにクラスを定義して(競合がまったく発生しないように)、後でデータクラスフィールドが目的のデフォルト値になるように変更する方が簡単です。

@dataclass
class C:
    __slots__ = "x"
    x: int # field(default = 1)

変更は簡単です。 __init__署名を変更して目的のデフォルト値を反映させてから、__dataclass_fields__を変更してデフォルト値の存在を反映させます。

from functools import wraps

def change_init_signature(init):
    @wraps(init)
    def __init__(self, x=1):
        init(self,x)
    return __init__

C.__init__ = change_init_signature(C.__init__)

C.__dataclass_fields__["x"].default = 1

テスト:

>>> C()
C(x=1)
>>> C(2)
C(x=2)
>>> C.x
<member 'x' of 'C' objects>
>>> vars(C())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: vars() argument must have __dict__ attribute

できます!

少し努力すれば、いわゆるslotted_dataclassデコレータを使用して、上記の方法でクラスを自動的に変更できます。これには、データクラスAPIから逸脱する必要があります-おそらく次のようなものです。

@slotted_dataclass(x:int=field(default=1))
class C:
    __slots__="x"

同じことは、親クラスの__init_subclass__メソッドを介して実行することもできます。

class SlottedDataclass:
    def __init_subclass__(cls, **kwargs):
        cls.__init_subclass__()
        # make the class changes here

class C(SlottedDataclass, x=1):
    __slots__ = "x"
    x: int

この問題に対処する別の潜在的な方法は、dataclass_slotsユーティリティ関数をデータクラスAPI(または独自のデコレータを持つカスタムの個別のAPI)に追加することです。

次のようなものが機能する可能性があります。

@slotted_dataclass
class C:
    __slots__ = dataclass_slots(x=field(default=1))
    x: int

dataclass_slots関数によって返されるオブジェクトは反復可能であり、既存のスロットマシンが機能できるようにします。ただし、slotted_dataclassデコレータは、後でフィールドオブジェクト、メソッドなどを適切に作成することもできます。

4
Rick Teachey

この問題に対して私が見つけた最も複雑でない解決策は、__init__を使用してカスタムobject.__setattr__を指定して値を割り当てることです。

@dataclass(init=False, frozen=True)
class MyDataClass(object):
    __slots__ = (
        "required",
        "defaulted",
    )
    required: object
    defaulted: Optional[object]

    def __init__(
        self,
        required: object,
        defaulted: Optional[object] = None,
    ) -> None:
        super().__init__()
        object.__setattr__(self, "required", required)
        object.__setattr__(self, "defaulted", defaulted)

1
mcguip

Rick Teacheysuggestion に続いて、_slotted_dataclass_デコレータを作成しました。キーワード引数では、_[field]: [type] =_なしのデータクラスで___slots___の後に指定するものをすべて取ることができます—フィールドとfield(...)の両方のデフォルト値。古い_@dataclass_コンストラクターに移動する引数を指定することもできますが、辞書オブジェクトでは最初の位置引数として指定します。したがって、この:

_@dataclass(frozen=True)
class Test:
    a: dict = field(repr=False)
    b: int = 42
    c: list = field(default_factory=list)
_

になります:

_@slotted_dataclass({'frozen': True}, a=field(repr=False), b=42, c=field(default_factory=list))
class Test:
    __slots__ = ('a', 'b', 'c')
    a: dict
    b: int
    c: list
_

そして、これがこの新しいデコレータのソースコードです:

_def slotted_dataclass(dataclass_arguments=None, **kwargs):
    if dataclass_arguments is None:
        dataclass_arguments = {}

    def decorator(cls):
        old_attrs = {}

        for key, value in kwargs.items():
            old_attrs[key] = getattr(cls, key)
            setattr(cls, key, value)

        cls = dataclass(cls, **dataclass_arguments)
        for key, value in old_attrs.items():
            setattr(cls, key, value)
        return cls

    return decorator
_

コードの説明

上記のコードは、dataclassesモジュールがクラスでgetattrを呼び出すことにより、デフォルトのフィールド値を取得するという事実を利用しています。これにより、クラスの___dict___の適切なフィールドを置き換えることでデフォルト値を提供できます(これは、コードでsetattr関数を使用して行われます)。 _@dataclass_デコレータによって生成されたクラスは、クラスに_=_が含まれていない場合と同様に、___slots___の後に指定して生成されたクラスと完全に同一になります。

ただし、___dict___を持つクラスの___slots___には_member_descriptor_オブジェクトが含まれているため:

_>>> class C:
...     __slots__ = ('a', 'b', 'c')
...
>>> C.__dict__['a']
<member 'a' of 'C' objects>
>>> type(C.__dict__['a'])
<class 'member_descriptor'>
_

良いことは、それらのオブジェクトをバックアップし、_@dataclass_デコレータがその仕事をした後にそれらを復元することです。これは_old_attrs_ディクショナリを使用してコードで実行されます。

1
Anonymouse