Наслідування класів у класах даних 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

Як це виправити?

Відповіді:


127

Спосіб поєднання атрибутів класів даних перешкоджає можливості використовувати атрибути за замовчуванням у базовому класі, а потім використовувати атрибути без типових (позиційні атрибути) у підкласі.

Це тому, що атрибути комбінуються, починаючи з нижньої частини 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 (ігнорування 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
Велике спасибі за детальну відповідь
Mysterio

Це дуже корисно. Я збентежений щодо mro. Запуск print (Child.mro ()) Я отримую: [<class ' main .Child'>, <class ' main .Parent'>, <class ' main ._ChildDefaultsBase'>, <class ' main ._ParentDefaultsBase'>, < class ' main ._ChildBase'>, <class ' main ._ParentBase'>, <class 'object'>] Тож бази за замовчуванням не передують базовим класам?
Оллі

1
@Ollie це правильний порядок; зауважте, що я вказав це у своїй відповіді. Коли у вас є кілька базових класів, вам потрібен спосіб лінеаризації залучених класів, щоб вирішити, які класи постають перед іншими під час успадкування. Python використовує метод лінеаризації C3, і моя відповідь використовує переваги того, як це працює, щоб атрибути зі значеннями за замовчуванням завжди надходили після всіх атрибутів без параметрів за замовчуванням.
Мартін Пітерс

Насправді атрибути можуть працювати, але вам потрібно використовувати attr.ib(kw_only=True), див. Github.com/python-attrs/attrs/issues/38
laike9m

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. Остаточний тип xis int, як зазначено в класі C.

На жаль, я не думаю, що це можна обійти. Я розумію, що якщо в батьківському класі є аргумент за замовчуванням, то жоден дочірній клас не може мати аргументів, що не є типовими.


Я розумію, що аргумент не за замовчуванням повинен стояти перед типовим, але як це може статися, коли батьківські аргументи ініціалізуються перед додаванням дочірніх аргументів?
Mysterio

3
Я не думаю, що, на жаль, це можна обійти. Я розумію, що якщо в батьківському класі є аргумент за замовчуванням, то жоден дочірній клас не може мати аргументів, що не є типовими.
Patrick Haugh

1
Чи можете ви додати цю інформацію до відповіді, перш ніж я її позначу? Колись це допоможе комусь. Дуже прикро, що обмеження класів даних. Візуалізує мій поточний проект python. Приємно бачити такі реалізації
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.
Нільс Бенгтссон,

5

на основі рішення Мартіна Пітерса я зробив наступне:

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

Ви мали намір написати "клас MyDataclass (DataclassWithDefaults, NoDefaultAttributesPostInitMixin)" вище в 2)?
Скотт П.,

4

Наведений нижче підхід розглядає цю проблему під час використання чистого python 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 зараз є обов’язковим параметром без типових параметрів
Вадим Тєміров

2

Я повернувся до цього питання після виявлення цих класів даних можуть отримувати параметр декоратора, що дозволяє переупорядковувати поля. Це, безумовно, перспективний розвиток, хоча, здається, розвиток цієї функції дещо зупинився.

Зараз ви можете отримати таку поведінку, а також деякі інші приємності, використовуючи dataclassy , мою реімплементацію класів даних, яка долає такі розчарування. Використання from dataclassyзамість from dataclassesв оригінальному прикладі означає, що він працює без помилок.

За допомогою перевірити для друку підпису 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, але це, мабуть, занадто брудно.

Хоча у виправленні мавп відсутні деякі особливості успадкування, його все ще можна використовувати для додавання методів до всіх псевдо-дочірніх класів.

Для більш детального контролю встановіть значення за замовчуванням за допомогою 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без потворних хаків.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.