Python 3.7 데이터 클래스의 클래스 상속


84

저는 현재 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

이 문제를 어떻게 해결합니까?

답변:


125

데이터 클래스가 속성을 결합하는 방식은 기본 클래스에서 기본값이있는 속성을 사용한 다음 하위 클래스에서 기본값이없는 속성 (위치 속성)을 사용할 수 없도록합니다.

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

기본값이없는 필드와 기본값이있는 필드, 신중하게 선택된 상속 순서 를 사용하여 필드를 별도의 기본 클래스 로 가져 오면 기본값이없는 모든 필드를 기본값이있는 필드보다 먼저 배치하는 MRO를 생성 할 수 있습니다. 에 대한 반전 된 MRO (무시 object) Child는 다음과 같습니다.

_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent

Parent, 새로운 필드를 설정하지 않습니다 그래서 그것이 순서를 나열 분야에서 '마지막'끝나는 것을 여기에 문제가되지 않습니다. 기본값 (없는 필드 클래스 _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)

또 다른 옵션은 기본값이있는 필드 만 사용하는 것입니다. 에서 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에 대해 혼란 스럽습니다. print (Child.mro ()) 실행하면 [<class ' main .Child'>, <class ' main .Parent'>, <class ' main ._ChildDefaultsBase'>, <class ' main ._ParentDefaultsBase'>, < class ' main ._ChildBase'>, <class ' main ._ParentBase'>, <class 'object'>] 그러면 기본 기본이 기본 클래스 앞에 있지 않습니까?
Ollie

1
@Ollie는 올바른 순서입니다. 내 대답에 나열했습니다. 여러 기본 클래스가있는 경우 상속 할 때 다른 클래스보다 먼저 오는 클래스를 결정하기 위해 관련된 클래스 를 선형화 하는 방법이 필요합니다 . Python은 C3 선형화 방법을 사용 하며 내 대답은 이것이 작동하는 방식을 활용하여 기본값이있는 속성이 항상 기본값이없는 모든 속성 뒤에 오도록합니다.
Martijn Pieters

실제로 attrs는 작동하지만을 사용해야합니다 attr.ib(kw_only=True). github.com/python-attrs/attrs/issues/38
laike9m

8

기본값이없는 인수가 기본값이있는 인수 뒤에 추가되기 때문에이 오류가 표시됩니다. dataclass에 상속 된 필드의 삽입 순서의 역순 방법 해상도 주문 것을 의미 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입니다. 의 마지막 유형 xint클래스에 지정된대로 C.

불행히도이 문제를 해결할 방법이 없다고 생각합니다. 내 이해는 부모 클래스에 기본 인수가 있으면 자식 클래스가 기본 인수가 아닌 인수를 가질 수 없다는 것입니다.


기본 인수가 아닌 인수가 기본 인수보다 먼저 와야하지만 자식 인수를 추가하기 전에 부모 인수가 초기화 될 때 어떻게 할 수 있습니까?
Mysterio

3
안타깝게도 주변에 방법이 없다고 생각합니다. 내 이해는 부모 클래스에 기본 인수가 있으면 자식 클래스가 기본 인수가 아닌 인수를 가질 수 없다는 것입니다.
Patrick Haugh

1
내가 표시하기 전에 그 정보를 답변에 추가 할 수 있습니까? 언젠가는 누군가를 도울 것입니다. 데이터 클래스의 제한은 매우 불행합니다. 내 현재 파이썬 프로젝트를 엉망으로 만듭니다. 그런 구현을 보는 것이 좋습니다
Mysterio

5

init 함수에서 속성을 제외하면 부모 클래스에서 기본값이있는 속성을 사용할 수 있습니다. init에서 기본값을 재정의 할 가능성이 필요한 경우 Praveen Kulkarni의 답변으로 코드를 확장하십시오.

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를 제거합니다.
Nils Bengtsson

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)에서 "class MyDataclass (DataclassWithDefaults, NoDefaultAttributesPostInitMixin)"을 작성하려고 했습니까?
Scott P.

4

아래의 접근 방식은 순수한 파이썬을 사용 dataclasses하고 많은 상용구 코드없이이 문제를 처리 합니다.

ugly_init: dataclasses.InitVar[bool]역할을 의사 필드는 단지 우리가 초기화를하고 인스턴스가 생성되면 손실됩니다 도움이됩니다. While ugly: 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 지금 기본값은 없습니다 필수 매개 변수입니다
바딤 Tyemirov

2

데이터 클래스 필드를 재정렬 할 수있는 데코레이터 매개 변수를 가져올 있음 을 발견 한 후이 질문으로 돌아 왔습니다 . 이 기능에 대한 개발이 다소 지연된 것처럼 보이지만 이것은 확실히 유망한 개발입니다.

지금 당장 은 dataclassy를 사용 하여이 동작과 몇 가지 다른 장점을 얻을 수 있습니다. 이러한 좌절감을 극복하는 데이터 클래스를 다시 구현합니다. 원래 예제 from dataclassy에서 대신 사용하면 from dataclasses오류없이 실행됩니다.

inspect 를 사용 하여 서명을 인쇄하면 Child진행중인 작업이 명확 해집니다. 결과는 (name: str, age: int, school: str, ugly: bool = True)입니다. 이니셜 라이저의 매개 변수에서 기본값이없는 필드 뒤에 기본값이있는 필드가 오도록 필드는 항상 재정렬됩니다. 두 목록 (기본값이없는 필드 및 기본값이있는 필드)은 여전히 ​​정의 순서대로 정렬됩니다.

이 문제에 직면하게 된 것은 데이터 클래스를 대체하도록 저를 자극 한 요인 중 하나였습니다. 여기에 자세히 설명 된 해결 방법은 유용하지만 코드를 왜곡하여 가독성 이점 데이터 클래스의 순진한 접근 방식 (필드 순서가 사소하게 예측 가능함)이 제공하는 것을 완전히 무효화해야합니다.


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있지만 이것은 아마도 너무 더럽습니다.

monkey-patching에는 상속의 일부 기능이 없지만 모든 의사-자식 클래스에 메서드를 추가하는 데 사용할 수 있습니다.

보다 세밀하게 제어하려면 다음을 사용하여 기본값을 설정하십시오. 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)(유효한 파이썬) 생성 합니다. 여기서 유일한주의 사항은 위치 인수로 객체를 초기화 할 수 dataclass없다는 것입니다. 그렇지 않으면 추악한 해킹이없는 완전히 규칙적 입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.