決定されました 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
デコレータは、後でフィールドオブジェクト、メソッドなどを適切に作成することもできます。
この問題に対して私が見つけた最も複雑でない解決策は、__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)
Rick Teachey の suggestion に続いて、_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
_ディクショナリを使用してコードで実行されます。