Наследование классов в Python 3.7 dataclasses

В настоящее время я пробую свои силы на новых конструкциях класса данных, представленных в 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'], где ugly значение по умолчанию, а затем Child добавляет ['school'] в конец этого списка (с уже ugly в списке). Это означает, что вы в конечном итоге получите ['name', 'age', 'ugly', 'school'] и поскольку school не имеет значения по умолчанию, это приводит к неверному списку аргументов для __init__.

Это задокументировано в классах данных PEP-557 при наследовании:

Когда класс данных @dataclass декоратором @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)

Другой вариант - использовать только поля со значениями по умолчанию; вы все равно можете сделать ошибку, чтобы не __post_init__ значение 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 типов, устанавливая 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

Ответ 2

Вы видите эту ошибку, потому что после аргумента со значением по умолчанию добавляется аргумент без значения по умолчанию. Порядок вставки унаследованных полей в класс данных является обратным порядку разрешения метода, что означает, что 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

на основе решения 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

Приведенный ниже подход решает эту проблему при использовании чистых dataclasses Python и без большого количества стандартного кода.

ugly_init: dataclasses.InitVar[bool] служит псевдополем только для того, чтобы помочь нам выполнить инициализацию и будет потеряно после создания экземпляра. Хотя 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()

Ответ 5

Возможный обходной путь - использовать monkey-patching для добавления родительских полей

import dataclasses as dc

def add_args(parent): 
    def decorator(orig):
        "Append parent fields AFTER orig 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, ...)