Python 3.7 データクラスにおけるクラス継承 質問する

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

これを修正するにはどうすればいいでしょうか?

ベストアンサー1

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

これは、属性が MRO の一番下から始めて最初に出現した順序で属性の順序付きリストを構築することによって結合されるためです。オーバーライドは元の場所に保持されます。つまり、 は ( はデフォルトを持つ)Parentで始まり、次に はそのリストの末尾に追加します( はすでにリスト内にあります)。つまり、 となり、 にはデフォルトがないため、 の無効な引数リストになります。['name', 'age', 'ugly']uglyChild['school']ugly['name', 'age', 'ugly', 'school']school__init__

これは文書化されているPEP-557データクラス、 下継承:

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

そして以下仕様:

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(_ChildDefaultsBase, Parent, _ChildBase):
    pass

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

_ParentBase
_ChildBase
_ParentDefaultsBase
Parent
_ChildDefaultsBase

Parentは新しいフィールドを設定しませんが、 からフィールドを継承し_ParentDefaultsBase、フィールドのリスト順序で が「最後」にならないことに注意してください。 上記の順序では が最後になるため、そのフィールドが「勝ちます」。 データクラスのルールも満たされています。 デフォルトのないフィールドを持つクラス (および)_ChildDefaultsBaseは、デフォルトのフィールドを持つクラス (および) よりも前になります。_ParentBase_ChildBase_ParentDefaultsBase_ChildDefaultsBase

結果はParent、 とChildのクラスが、正常なフィールドを持つより古いものになりますが、 は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)

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

_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'")

しかし、これによりフィールドの順序が変更され、schoolのようになりますugly

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

型ヒントチェッカーは文字列ではないとエラーを出します_no_default

また、attrsプロジェクトは、 のインスピレーションの元となったプロジェクトですdataclasses。 は異なる継承マージ戦略を使用しており、サブクラス内のオーバーライドされたフィールドをフィールド リストの末尾に移動するため、クラス['name', 'age', 'ugly']の はクラスの にParentなります。フィールドをデフォルトでオーバーライドすることで、MRO ダンスを行わなくてもオーバーライドが可能になります。['name', 'age', 'school', 'ugly']Childattrs

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

おすすめ記事