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
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 fichierChild
qui n'hérite pas de la classe de données, et ensuite avoir unechild
de l'attributParent
classe.