252 votes

Héritage de classe dans Python 3.7 dataclasses

Je suis actuellement en train de m'essayer aux nouvelles constructions de classes de données introduites dans Python 3.7. Je suis actuellement bloqué en essayant de faire de l'héritage d'une classe parent. Il semblerait que l'ordre des arguments ne soit pas respecté par mon approche actuelle, de sorte que le paramètre bool de la classe enfant est passé avant les autres paramètres. Cela 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()

Lorsque j'exécute ce code, j'obtiens ceci TypeError :

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

Comment puis-je réparer cela ?

11 votes

ugly: bool = True = rekt :)

3 votes

Je pense qu'il est utile de noter que dans le paradigme python typé attrs / dataclass, la composition est généralement préférée à l'héritage. L'extension de la classe de votre sous-classe __init__ comme cela est vaguement une violation de LSP car vos différentes sous-classes ne seront pas interchangeables. Pour être clair, je pense que cette façon de faire est souvent pratique, mais au cas où vous n'auriez pas envisagé d'utiliser la composition : il pourrait également être judicieux de faire un fichier Child qui n'hérite pas de la classe de données, et ensuite avoir une child de l'attribut Parent classe.

318voto

Martijn Pieters Points 271458

La façon dont les classes de données combinent les attributs vous empêche de pouvoir 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 positionnels) dans une sous-classe.

En effet, les attributs sont combinés en commençant par le bas du MRO, et en construisant une liste ordonnée des attributs dans l'ordre de leur apparition ; les remplacements sont conservés dans leur emplacement d'origine. Ainsi, Parent commence par ['name', 'age', 'ugly'] , donde ugly a un défaut, et ensuite 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, ce qui entraîne une liste d'arguments invalide pour l'utilisateur. __init__ .

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

Lorsque la classe de données est créée par le programme @dataclass il parcourt toutes les classes de base de la classe en sens inverse du MRO (c'est-à-dire en commençant à object ) et, pour chaque classe de données qu'il trouve, ajoute les champs de cette classe de base à un mappage ordonné des champs. Après avoir ajouté tous les champs de la classe de base, il ajoute ses propres champs au mappage ordonné. Toutes les méthodes générées utiliseront ce mappage ordonné combiné et calculé des champs. Comme les champs sont dans l'ordre d'insertion, les classes dérivées remplacent les classes de base.

et sous Spécifications :

TypeError sera soulevée 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 seule classe, soit à la suite d'un héritage de classes.

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

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

La hiérarchie de classe 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 retirant les champs dans séparé En utilisant des classes de base avec des champs sans valeur par défaut et des champs avec valeur par défaut, et un ordre d'héritage soigneusement sélectionné, vous pouvez produire une MRO qui place tous les champs sans valeur par défaut avant ceux avec valeur par défaut. La MRO inversée (ignorant object ) para Child est :

_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent

Notez que Parent ne définit pas de nouveaux champs, de sorte que le fait qu'il se retrouve "dernier" dans l'ordre de la liste des champs n'a pas d'importance ici. Les classes avec des champs sans valeur par défaut ( _ParentBase y _ChildBase ) précèdent les classes avec des champs avec des valeurs par défaut ( _ParentDefaultsBase y _ChildDefaultsBase ).

Le résultat est Parent y Child avec un champ sain plus âgé, tandis 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 vous pouvez donc 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 en ne fournissant pas un school en en relevant 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 fait modifier l'ordre des champs ; school se retrouve 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 sera se plaindre de _no_default n'étant pas une chaîne de caractères.

Vous pouvez également utiliser le attrs projet qui est le projet qui a inspiré dataclasses . Elle utilise une stratégie différente de fusion de l'héritage ; elle place les champs surchargés dans une sous-classe à la fin de la liste des champs, de sorte que ['name', 'age', 'ugly'] dans le Parent La classe devient ['name', 'age', 'school', 'ugly'] dans le Child en remplaçant le champ par un champ par défaut, attrs permet de passer outre sans avoir besoin de faire une danse MRO.

attrs permet de définir des champs sans indication de type, mais restons-en à l'approche de l'option mode d'indication du type supporté en fixant 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 votes

Merci beaucoup pour cette réponse détaillée

0 votes

C'est très utile. Je ne comprends pas bien la question de la mro. En exécutant print(Child.mro()) j'obtiens : [<classe ' principal .Child'>, <classe ' principal .Parent'>, <classe ' principal ._ChildDefaultsBase'>, <classe ' principal ._ParentDefaultsBase'>, <classe ' principal ._ChildBase'>, <classe ' principal ._ParentBase'>, <classe 'object'>] Les bases par défaut ne précèdent-elles pas les classes de base ?

1 votes

@Ollie c'est l'ordre correct ; notez que je l'ai indiqué dans ma réponse. Lorsque vous avez plusieurs classes de base, vous avez besoin d'un moyen pour linéariser les classes concernées pour décider quelles classes passent avant les autres lorsqu'elles héritent. Python utilise le Méthode de linéarisation C3 et ma réponse tire parti de ce fonctionnement pour garantir que les attributs avec des valeurs par défaut viennent toujours après tous les attributs sans valeur par défaut.

86voto

SimonMarcin Points 31

Vous pouvez utiliser des attributs avec des valeurs par défaut dans les classes parentes si vous les excluez de la fonction init. Si vous avez besoin de la possibilité de remplacer la valeur par défaut à init, étendez le code avec la réponse de 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

Ou même

@dataclass
class Child(Parent):
    school: str
    ugly = True
    # This does not work
    # ugly: bool = True

jack_son = Child('jack jnr', 12, school = 'havard')
assert jack_son.ugly

10 votes

Je pense que cette réponse devrait être davantage reconnue. Elle a résolu le problème de l'existence d'un champ par défaut dans la classe parente, ce qui supprime l'erreur de type.

0 votes

@SimonMarcin, c'est une excellente réponse !

0 votes

C'est la bonne réponse. A moins que vous ne supportiez la nouvelle et brillante version (>= 3.10), ceci résout le problème ! +1

15voto

L'approche ci-dessous permet de résoudre ce problème en utilisant du pur python. dataclasses et sans beaucoup de code passe-partout.

Le site ugly_init: dataclasses.InitVar[bool] sert de pseudo-champ juste pour nous aider à faire l'initialisation et sera perdue une fois l'instance créée. Alors que ugly: bool = field(init=False) est un membre d'instance qui ne sera pas initialisé par __init__ mais peut aussi être initialisé en utilisant la méthode __post_init__ (vous pouvez trouver plus aquí .).

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

Si vous voulez utiliser un modèle où ugly_init est facultatif, vous pouvez définir une méthode de classe sur le Parent qui inclut ugly_init comme paramètre facultatif :

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

Vous pouvez maintenant utiliser le create comme une méthode d'usine pour créer des classes Parent/Child avec une valeur par défaut pour ugly_init . Notez que vous devez utiliser des paramètres nommés pour que cette approche fonctionne.

1 votes

Ugly_init est maintenant un paramètre obligatoire sans défaut.

10voto

Patrick Haugh Points 27866

Vous voyez cette erreur parce qu'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 l'ordre d'insertion de la classe de données. Méthode Résolution Ordre ce qui signifie que le Parent Les champs sont prioritaires, 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 es int selon les spécifications de la classe C .

Malheureusement, je ne pense pas qu'il y ait un moyen de contourner cela. Si je comprends bien, si la classe parent a un argument par défaut, alors aucune classe enfant ne peut avoir d'argument autre que celui par défaut.

0 votes

Je comprends que l'argument non par défaut doit venir avant l'argument par défaut mais comment est-ce possible lorsque les arguments parents s'initialisent avant d'ajouter les arguments enfants ?

4 votes

Je ne pense pas qu'il y ait de moyen de contourner ce problème, malheureusement. Si je comprends bien, si la classe parent a un argument par défaut, alors aucune classe enfant ne peut avoir d'arguments autres que ceux par défaut.

1 votes

Pouvez-vous ajouter cette information à la réponse avant que je ne la marque ? Cela aidera quelqu'un un jour. C'est assez malheureux que la limitation des classes de données. Cela rend mon projet python actuel sans intérêt. Il est agréable de voir de telles implémentations tho.

7voto

Daniel Albarral Points 73

En me basant sur la solution de Martijn Pieters, j'ai fait ce qui suit :

1) Créer un mélange 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 de l'héritage :

from src.utils import no_default, NoDefaultAttributesChild

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

EDITAR:

Après un certain temps, j'ai aussi trouvé des problèmes avec cette solution avec mypy, le code suivant résout le problème.

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

0 votes

Aviez-vous l'intention d'écrire "class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin)" ci-dessus en 2) ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X