web-dev-qa-db-fra.com

Héritage de classe dans Python 3.7 Dataclasses

J'essaie actuellement les nouvelles constructions de classes de données introduites dans Python 3.7. Je suis actuellement bloqué pour essayer d'hériter d'une classe parente. Il semble que l'ordre des arguments soit botched par mon approche actuelle de telle sorte que le paramètre bool de la classe enfant soit passé avant les autres paramètres, ce qui provoque une erreur de type.

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

Quand je lance ce code, je reçois ceci TypeError:

TypeError: non-default argument 'school' follows default argument

Comment puis-je réparer ça?

30
Mysterio

La façon dont les classes de données combinent des attributs vous empêche d'utiliser des attributs avec des valeurs par défaut dans une classe de base, puis d'utiliser des attributs sans valeur par défaut (attributs de position) dans une sous-classe.

En effet, les attributs sont combinés en partant du bas de la MRO et en créant une liste ordonnée des attributs dans l’ordre le plus récent; les dérogations sont conservées à leur emplacement d'origine. Donc, Parent commence avec ['name', 'age', 'ugly'], où ugly a une valeur par défaut, puis Child ajoute ['school'] à la fin de cette liste (avec ugly déjà dans la liste). Cela signifie que vous vous retrouvez avec ['name', 'age', 'ugly', 'school'] et _ parce que school n’a pas de valeur par défaut, il en résulte une liste d’arguments non valide pour __init__.

Ceci est documenté dans PEP-557 Dataclasses , sous héritage :

Lorsque la classe de données est créée par le @dataclass _ décorateur, il examine toutes les classes de base de la classe en MRO inversé (c'est-à-dire à partir de object) et, pour chaque classe de données trouvée, ajoute les champs de cette classe de base à un mappage ordonné. des champs. Une fois tous les champs de la classe de base ajoutés, il ajoute ses propres champs au mappage commandé. Toutes les méthodes générées utiliseront cette cartographie combinée, calculée et ordonnée des champs. Comme les champs sont dans l'ordre d'insertion, les classes dérivées remplacent les classes de base.

et sous Spécification :

TypeError sera levé si un champ sans valeur par défaut suit un champ avec une valeur par défaut. Cela est vrai soit lorsque cela se produit dans une classe unique, soit à la suite d'un héritage de classe.

Vous avez quelques options ici pour éviter ce problème.

La première option consiste à utiliser des classes de base distinctes pour forcer les champs avec les valeurs par défaut dans une position ultérieure dans l'ordre MRO. À tout prix, évitez de définir des champs directement sur les classes à utiliser comme classes de base, telles que Parent.

La hiérarchie de classes suivante fonctionne:

# 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

En extrayant des champs dans des classes de base séparées avec des champs sans valeurs par défaut et des champs avec des valeurs par défaut, et un ordre d'héritage sélectionné avec soin, vous pouvez produire un MRO qui met tous champs sans valeurs par défaut avant ceux avec valeurs par défaut. Le MRO inversé (en ignorant object) pour Child est:

_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent

Notez que Parent ne définit pas de nouveaux champs, il importe donc peu que ce dernier soit "dernier" dans l'ordre d'affichage des champs. Les classes avec des champs sans valeurs par défaut (_ParentBase et _ChildBase) précède les classes avec des champs avec des valeurs par défaut (_ParentDefaultsBase et _ChildDefaultsBase).

Le résultat est Parent et Child classes avec un champ sain plus ancien, alors que Child est toujours une sous-classe de 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

et ainsi vous pouvez créer des instances des deux classes:

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

Une autre option consiste à n'utiliser que des champs avec des valeurs par défaut. vous pouvez toujours faire une erreur pour ne pas fournir une valeur school, en en soulevant une dans __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'")

mais ceci change l'ordre des champs; school se termine après ugly:

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

et un vérificateur d'indices de type se plaindra à propos de _no_default n'étant pas une chaîne.

Vous pouvez également utiliser le attrs project , qui est le projet qui a inspiré dataclasses. Il utilise une stratégie de fusion d'héritage différente; il extrait les champs remplacés d'une sous-classe à la fin de la liste des champs, donc ['name', 'age', 'ugly'] dans la classe Parent devient ['name', 'age', 'school', 'ugly'] dans la classe Child; en remplaçant le champ par un paramètre par défaut, attrs permet le remplacement sans qu'il soit nécessaire d'effectuer une danse MRO.

attrs supporte la définition de champs sans indication de type, mais nous nous en tenons au mode de suggestion de type pris en charge en définissant 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
35
Martijn Pieters

Vous voyez cette erreur car un argument sans valeur par défaut est ajouté après un argument avec une valeur par défaut. L'ordre d'insertion des champs hérités dans la classe de données est l'inverse de Method Resolution Order , ce qui signifie que les champs Parent arrivent en premier, même s'ils sont écrasés plus tard par leurs enfants.

Un exemple de PEP-557 - Classes de données :

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

La liste finale des champs est, dans l'ordre, x, y, z. Le dernier type de x est int, comme spécifié dans la classe C.

Malheureusement, je ne pense pas qu'il y ait de solution. Si j'ai bien compris, si la classe parent a un argument par défaut, aucune classe enfant ne peut avoir d'argument autre que celui par défaut.

7
Patrick Haugh

sur la base de la solution Martijn Pieters, j'ai procédé comme suit:

1) Créer un mixage implémentant le 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) Puis dans les classes avec le problème d'héritage:

from src.utils import no_default, NoDefaultAttributesChild

@dataclass
class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin):
    attr1: str = no_default
4
Daniel Albarral

L'approche ci-dessous traite de ce problème en utilisant python dataclasses pur et sans beaucoup de code passe-partout.

Le ugly_init: dataclasses.InitVar[bool] Sert de pseudo-champ uniquement pour nous aider à effectuer l'initialisation et sera perdu une fois l'instance créée. Bien que ugly: bool = field(init=False) soit un membre d'instance qui ne sera pas initialisé par la méthode __init__, Mais peut également être initialisé à l'aide de la méthode __post_init__ (Vous pouvez en trouver plus ici .).

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()
3
Praveen Kulkarni