Python3.7データクラスでのクラス継承


90

私は現在、Python3.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

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


ugly: bool = True= rekt :)
four432

回答:


138

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

これは、MROの下部から開始し、最初に表示された順序で属性の順序付きリストを作成することにより、属性が結合されるためです。オーバーライドは元の場所に保持されます。そうParentで始まり['name', 'age', 'ugly']どこ、uglyデフォルトを持って、その後、Child追加します['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(Parent, _ChildDefaultsBase, _ChildBase):
    pass

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

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

もう1つのオプションは、デフォルトのフィールドのみを使用することです。次のschool値を上げることで、値を指定しないというエラーを犯す可能性があります__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、クラスとなっ['name', 'age', 'school', 'ugly']Childクラス。デフォルトでフィールドをオーバーライドする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

1
詳細な回答をありがとうございました
ミステリオ

これは非常に役立ちます。でもmroについては混乱しています。実行中の印刷(Child.mro())私が手:[<クラスのメイン.Child '>、<クラスのメイン.Parent'>、<クラスのメイン._ChildDefaultsBase '>、<クラスのメイン._ParentDefaultsBase'>、<クラスのメイン._ChildBase '>、<クラスのメイン._ParentBase'>、<クラス「オブジェクト」>]だから、デフォルトの塩基は基底クラスの前にいませんか?
Ollie第

1
@Ollieそれは正しい順序です。私はそれを私の答えにリストしたことに注意してください。複数の基本クラスがある場合、継承するときに他のクラスよりも優先されるクラスを決定するために、関連するクラスを線形化する方法が必要です。PythonはC3線形化メソッドを使用し、私の答えはこれがどのように機能するかを利用して、デフォルトのある属性が常にデフォルトのないすべての属性の後に来るようにします。
MartijnPieters

実際には、attrsは機能しますが、使用する必要があります。github.com/ pythonattr.ib(kw_only=True)
38 –laike9m20年

8

デフォルト値のない引数がデフォルト値のある引数の後に追加されているため、このエラーが表示されます。継承されたフィールドのデータクラスへの挿入順序は、メソッド解決順序の逆です。つまり、後で子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, zです。クラスで指定されてxいるようにint、の最後のタイプはですC

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


デフォルト以外の引数はデフォルトの引数の前に来る必要があると思いますが、子引数を追加する前に親引数が初期化されるとどうすればよいですか?
ミステリオ

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

1
マークを付ける前に、その情報を回答に追加できますか?それはいつか誰かを助けるでしょう。データクラスの制限は非常に残念です。それをレンダリングして、現在のpythonプロジェクトを無効にします。そのような実装を見るのは素晴らしいことです
Mysterio

6

init関数から属性を除外すると、親クラスでデフォルトの属性を使用できます。initでデフォルトをオーバーライドする可能性が必要な場合は、PraveenKulkarniの回答でコードを拡張してください。

from dataclasses import dataclass, field

@dataclass
class Parent:
    name: str
    age: int
    ugly: bool = field(default=False, init=False)

@dataclass
class Child(Parent):
    school: str

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

この答えはもっと認識されるべきだと思います。親クラスにデフォルトフィールドがあるという問題が解決され、TypeErrorが削除されました。
NilsBengtsson20年

5

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

編集:

しばらくすると、mypyでこのソリューションの問題も見つかりました。次のコードで、問題を修正します。

from dataclasses import dataclass
from typing import TypeVar, Generic, Union

T = TypeVar("T")


class NoDefault(Generic[T]):
    ...


NoDefaultVar = Union[NoDefault[T], T]
no_default: NoDefault = NoDefault()


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


@dataclass
class Parent(NoDefaultAttributesPostInitMixin):
    a: str = ""

@dataclass
class Child(Foo):
    b: NoDefaultVar[str] = no_default

上記の2)で「classMyDataclass(DataclassWithDefaults、NoDefaultAttributesPostInitMixin)」を書くつもりでしたか?
スコット

5

以下のアプローチは、純粋なPythonを使用しdataclasses、ボイラープレートコードをあまり使用せずに、この問題に対処します。

ugly_init: dataclasses.InitVar[bool]となって擬似フィールドだけで私たちは、初期化を行うと、インスタンスが作成されると失われます支援します。whileugly: bool = field(init=False)は、__init__メソッドによって初期化されないインスタンスメンバーですが、メソッドを使用して初期化する__post_init__こともできます(詳細はこちらをご覧ください)。

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()

ugly_initオプションのパターンを使用する場合はugly_init、オプションのパラメーターとして次を含む親のクラスメソッドを定義できます。

from dataclasses import dataclass, field, InitVar

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

    def __post_init__(self, ugly_init: bool):
        self.ugly = ugly_init
    
    @classmethod
    def create(cls, ugly_init=True, **kwargs):
        return cls(ugly_init=ugly_init, **kwargs)

    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.create(name='jack snr', age=32, ugly_init=False)
jack_son = Child.create(name='jack jnr', age=12, school='harvard')

jack.print_id()
jack_son.print_id()

これで、createclassメソッドを、デフォルト値がugly_init。の親/子クラスを作成するためのファクトリメソッドとして使用できます。このアプローチを機能させるには、名前付きパラメーターを使用する必要があることに注意してください。


ugly_initは、デフォルトのない必須パラメーターになりました
VadymTyemirov20年

2

データクラスが可能性があることを発見した後、私はこの質問に戻りましたフィールドの並べ替えを可能にするデコレータパラメータを取得しているがあることを。これは確かに有望な開発ですが、この機能の開発はやや行き詰まっているようです。

今、あなたはこの動作を得ることができ、加えて使用することにより、他のいくつかの細かな点、dataclassyを、このような不満を解消するデータクラスの私の再実装。元の例のfrom dataclassy代わりに使用するとfrom dataclasses、エラーなしで実行されます。

inspectを使用ての署名を印刷するとChild、何が起こっているのかが明確になります。結果は(name: str, age: int, school: str, ugly: bool = True)です。初期化子のパラメーターで、デフォルト値のあるフィールドが、デフォルト値のないフィールドの後に来るように、フィールドは常に並べ替えられます。両方のリスト(デフォルトのないフィールドとデフォルトのあるフィールド)は、引き続き定義順に並べられています。

この問題に直面したことが、データクラスの代替を書くように促した要因の1つでした。ここで詳しく説明する回避策は、有用ではありますが、データクラスの素朴なアプローチ(フィールドの順序付けは簡単に予測可能)が提供する可読性の利点を完全に打ち消す程度にコードをゆがめる必要があります。


1

考えられる回避策は、モンキーパッチを使用して親フィールドを追加することです。

import dataclasses as dc

def add_args(parent): 
    def decorator(orig):
        "Append parent's fields AFTER orig's fields"

        # Aggregate fields
        ff  = [(f.name, f.type, f) for f in dc.fields(dc.dataclass(orig))]
        ff += [(f.name, f.type, f) for f in dc.fields(dc.dataclass(parent))]

        new = dc.make_dataclass(orig.__name__, ff)
        new.__doc__ = orig.__doc__

        return new
    return decorator

class Animal:
    age: int = 0 

@add_args(Animal)
class Dog:
    name: str
    noise: str = "Woof!"

@add_args(Animal)
class Bird:
    name: str
    can_fly: bool = True

Dog("Dusty", 2)               # --> Dog(name='Dusty', noise=2, age=0)
b = Bird("Donald", False, 40) # --> Bird(name='Donald', can_fly=False, age=40)

をチェックすることで、デフォルト以外のフィールドを前に付けることもできますif f.default is dc.MISSINGが、これはおそらく汚れすぎています。

モンキーパッチには継承の機能がいくつか欠けていますが、それでもすべての疑似子クラスにメソッドを追加するために使用できます。

よりきめ細かい制御を行うには、を使用してデフォルト値を設定します dc.field(compare=False, repr=True, ...)


1

データクラスの変更バージョンを使用できます。これにより、キーワードのみの__init__メソッドが生成されます。

import dataclasses


def _init_fn(fields, frozen, has_post_init, self_name):
    # fields contains both real fields and InitVar pseudo-fields.
    globals = {'MISSING': dataclasses.MISSING,
               '_HAS_DEFAULT_FACTORY': dataclasses._HAS_DEFAULT_FACTORY}

    body_lines = []
    for f in fields:
        line = dataclasses._field_init(f, frozen, globals, self_name)
        # line is None means that this field doesn't require
        # initialization (it's a pseudo-field).  Just skip it.
        if line:
            body_lines.append(line)

    # Does this class have a post-init function?
    if has_post_init:
        params_str = ','.join(f.name for f in fields
                              if f._field_type is dataclasses._FIELD_INITVAR)
        body_lines.append(f'{self_name}.{dataclasses._POST_INIT_NAME}({params_str})')

    # If no body lines, use 'pass'.
    if not body_lines:
        body_lines = ['pass']

    locals = {f'_type_{f.name}': f.type for f in fields}
    return dataclasses._create_fn('__init__',
                      [self_name, '*'] + [dataclasses._init_param(f) for f in fields if f.init],
                      body_lines,
                      locals=locals,
                      globals=globals,
                      return_type=None)


def add_init(cls, frozen):
    fields = getattr(cls, dataclasses._FIELDS)

    # Does this class have a post-init function?
    has_post_init = hasattr(cls, dataclasses._POST_INIT_NAME)

    # Include InitVars and regular fields (so, not ClassVars).
    flds = [f for f in fields.values()
            if f._field_type in (dataclasses._FIELD, dataclasses._FIELD_INITVAR)]
    dataclasses._set_new_attribute(cls, '__init__',
                       _init_fn(flds,
                                frozen,
                                has_post_init,
                                # The name to use for the "self"
                                # param in __init__.  Use "self"
                                # if possible.
                                '__dataclass_self__' if 'self' in fields
                                else 'self',
                                ))

    return cls


# a dataclass with a constructor that only takes keyword arguments
def dataclass_keyword_only(_cls=None, *, repr=True, eq=True, order=False,
              unsafe_hash=False, frozen=False):
    def wrap(cls):
        cls = dataclasses.dataclass(
            cls, init=False, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)
        return add_init(cls, frozen)

    # See if we're being called as @dataclass or @dataclass().
    if _cls is None:
        # We're called with parens.
        return wrap

    # We're called as @dataclass without parens.
    return wrap(_cls)

要点として投稿され、Python 3.6バックポートでテストされています)

これには、子クラスを次のように定義する必要があります。

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

そして生成します__init__(self, *, name:str, age:int, ugly:bool=True, school:str)(これは有効なPythonです)。ここでの唯一の注意点は、位置引数を使用してオブジェクトを初期化できないことですが、それ以外の場合dataclassは、醜いハッキングがなく、完全に規則的です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.