web-dev-qa-db-ja.com

Python 3.7データクラスのクラス継承

私は現在、Python 3.7。で導入された新しいデータクラス構造に手を試しています。現在、親クラスの継承を試みようとしています。引数の順序は子クラスのboolパラメーターが他のパラメーターの前に渡されるように、現在のアプローチに失敗しているため、型エラーが発生しています。

from dataclasses import dataclass

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = False

    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f'The Name is {self.name} and {self.name} is {self.age} year old')

@dataclass
class Child(Parent):
    school: str
    ugly: bool = True


jack = Parent('jack snr', 32, ugly=True)
jack_son = Child('jack jnr', 12, school = 'havard', ugly=True)

jack.print_id()
jack_son.print_id()

このコードを実行すると、次のTypeErrorが取得されます。

TypeError: non-default argument 'school' follows default argument

どうすれば修正できますか?

30
Mysterio

データクラスが属性を結合する方法により、基本クラスでデフォルトの属性を使用し、サブクラスでデフォルトのない属性(位置属性)を使用することができなくなります。

これは、MROの一番下から開始し、最初に表示された順序で属性の順序付きリストを作成することにより、属性が結合されるためです。オーバーライドは元の場所に保持されます。 Parent['name', 'age', 'ugly']で始まり、uglyにはデフォルトがあり、Childはそのリストの最後に['school']を追加します(uglyは既にリストにあります)。これは、最終的に['name', 'age', 'ugly', 'school']になり、schoolにはデフォルトがないため、__init__の引数リストが無効になることを意味します。

これは PEP-557Dataclassesinheritance

データクラスが@dataclassデコレータによって作成されるとき、逆MRO(つまり、objectで始まる)でクラスのすべての基本クラスを調べ、検出した各データクラスについて、その基本クラスのフィールドをフィールドの順序付けられたマッピングに追加します。すべての基本クラスフィールドが追加された後、独自のフィールドを順序付けられたマッピングに追加します。生成されたすべてのメソッドは、この結合され計算されたフィールドの順序マッピングを使用します。フィールドは挿入順であるため、派生クラスは基本クラスをオーバーライドします。

および Specification の下:

デフォルト値のないフィールドがデフォルト値のあるフィールドの後に続く場合、TypeErrorが発生します。これは、これが単一のクラスで発生する場合、またはクラス継承の結果として当てはまります。

この問題を回避するには、いくつかのオプションがあります。

最初のオプションは、個別の基本クラスを使用して、デフォルトのフィールドをMRO順序の後の位置に強制することです。 Parentなど、基本クラスとして使用されるクラスにフィールドを直接設定することは避けてください。

次のクラス階層が機能します。

# base classes with fields; fields without defaults separate from fields with.
@dataclass
class _ParentBase:
    name: str
    age: int

@dataclass
class _ParentDefaultsBase:
    ugly: bool = False

@dataclass
class _ChildBase(_ParentBase):
    school: str

@dataclass
class _ChildDefaultsBase(_ParentDefaultsBase):
    ugly: bool = True

# public classes, deriving from base-with, base-without field classes
# subclasses of public classes should put the public base class up front.

@dataclass
class Parent(_ParentDefaultsBase, _ParentBase):
    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f"The Name is {self.name} and {self.name} is {self.age} year old")

@dataclass
class Child(Parent, _ChildDefaultsBase, _ChildBase):
    pass

フィールドをseparateに引き抜くことにより、デフォルトのないフィールドとデフォルトのあるフィールド、および慎重に選択された継承順序を持つ基本クラス、すべてを置くMROを生成できますデフォルトのあるフィールドより前のデフォルトのないフィールド。 objectの逆MRO(Childを無視)は次のとおりです。

_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent

Parentは新しいフィールドを設定しないことに注意してください。したがって、フィールドリストの順序の最後にいることは重要ではありません。デフォルトのないフィールド(_ParentBaseおよび_ChildBase)のあるクラスは、デフォルトのあるフィールド(_ParentDefaultsBaseおよび_ChildDefaultsBase)のあるクラスの前にあります。

結果は、ParentChildクラスが古いフィールドであり、Childは依然としてParentのサブクラスです。

>>> from inspect import signature
>>> signature(Parent)
<Signature (name: str, age: int, ugly: bool = False) -> None>
>>> signature(Child)
<Signature (name: str, age: int, school: str, ugly: bool = True) -> None>
>>> issubclass(Child, Parent)
True

したがって、両方のクラスのインスタンスを作成できます。

>>> jack = Parent('jack snr', 32, ugly=True)
>>> jack_son = Child('jack jnr', 12, school='havard', ugly=True)
>>> jack
Parent(name='jack snr', age=32, ugly=True)
>>> jack_son
Child(name='jack jnr', age=12, school='havard', ugly=True)

別のオプションは、デフォルトのフィールドのみを使用することです。 __post_init__で値を上げることにより、school値を提供しないようにエラーを発生させることができます。

_no_default = object()

@dataclass
class Child(Parent):
    school: str = _no_default
    ugly: bool = True

    def __post_init__(self):
        if self.school is _no_default:
            raise TypeError("__init__ missing 1 required argument: 'school'")

ただし、このdoesはフィールドの順序を変更します。 schooluglyの後になります:

<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>

そして、タイプヒントチェッカーwillは、_no_defaultが文字列ではないことについて文句を言います。

attrs project を使用することもできます。これはdataclassesに影響を与えたプロジェクトです。別の継承マージ戦略を使用します。サブクラスのオーバーライドされたフィールドをフィールドリストの最後にプルするため、Parentクラスの['name', 'age', 'ugly']Childクラスの['name', 'age', 'school', 'ugly']になります。フィールドをデフォルトでオーバーライドすることにより、attrsはMROダンスを行う必要なくオーバーライドを許可します。

attrsは、タイプヒントのないフィールドの定義をサポートしますが、auto_attribs=Trueを設定することで サポートされているタイプヒンティングモード に固執することができます。

import attr

@attr.s(auto_attribs=True)
class Parent:
    name: str
    age: int
    ugly: bool = False

    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f"The Name is {self.name} and {self.name} is {self.age} year old")

@attr.s(auto_attribs=True)
class Child(Parent):
    school: str
    ugly: bool = True
35
Martijn Pieters

デフォルト値のない引数がデフォルト値のある引数の後に追加されているため、このエラーが表示されます。継承されたフィールドのデータクラスへの挿入順序は Method Resolution Order の逆です。つまり、Parentフィールドは、後で子によって上書きされた場合でも最初に来ることを意味します。

PEP-557-データクラス の例:

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

フィールドの最後のリストは、順番に、x, y, zxの最終型は、クラスintで指定されているCです。

残念ながら、これを回避する方法はないと思います。私の理解では、親クラスにデフォルト引数がある場合、子クラスはデフォルト以外の引数を持つことはできません。

7
Patrick Haugh

martijn Pietersソリューションに基づいて、次のことを行いました。

1)post_initを実装するミキシングを作成する

from dataclasses import dataclass

no_default = object()


@dataclass
class NoDefaultAttributesPostInitMixin:

    def __post_init__(self):
        for key, value in self.__dict__.items():
            if value is no_default:
                raise TypeError(
                    f"__init__ missing 1 required argument: '{key}'"
                )

2)次に、継承の問題があるクラスで:

from src.utils import no_default, NoDefaultAttributesChild

@dataclass
class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin):
    attr1: str = no_default
4
Daniel Albarral

以下のアプローチは、純粋なpython dataclassesを使用し、多くの定型コードなしでこの問題を処理します。

_ugly_init: dataclasses.InitVar[bool]_は pseudo-field として機能し、初期化を行うのに役立ちます。インスタンスが作成されると失われます。 ugly: bool = field(init=False)はインスタンスメンバであり、___init___メソッドでは初期化されませんが、___post_init___メソッドを使用して初期化することもできます(詳細は here を参照してください)。 )。

_from dataclasses import dataclass, field

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = field(init=False)
    ugly_init: dataclasses.InitVar[bool]

    def __post_init__(self, ugly_init: bool):
        self.ugly = ugly_init

    def print_name(self):
        print(self.name)

    def print_age(self):
        print(self.age)

    def print_id(self):
        print(f'The Name is {self.name} and {self.name} is {self.age} year old')

@dataclass
class Child(Parent):
    school: str

jack = Parent('jack snr', 32, ugly_init=True)
jack_son = Child('jack jnr', 12, school='havard', ugly_init=True)

jack.print_id()
jack_son.print_id()
_
3