La forma en que las clases de datos combinan atributos le impide poder usar atributos con valores predeterminados en una clase base y luego usar atributos sin un valor predeterminado (atributos posicionales) en una subclase.
Esto se debe a que los atributos se combinan comenzando desde la parte inferior del MRO y creando una lista ordenada de los atributos en el orden en que se ven por primera vez; las anulaciones se mantienen en su ubicación original. Entonces Parent
comienza con ['name', 'age', 'ugly']
, where ugly
tiene un valor predeterminado, y luego se Child
agrega ['school']
al final de esa lista (con ugly
ya en la lista). Esto significa que termina con ['name', 'age', 'ugly', 'school']
y debido a school
que no tiene un valor predeterminado, esto da como resultado una lista de argumentos no válidos para __init__
.
Esto está documentado en PEP-557 Dataclasses , bajo herencia :
Cuando el @dataclass
decorador crea la clase de datos , examina todas las clases base de la clase en MRO inverso (es decir, comenzando en object
) y, para cada clase de datos que encuentra, agrega los campos de esa clase base a un orden mapeo de campos. Después de agregar todos los campos de la clase base, agrega sus propios campos al mapeo ordenado. Todos los métodos generados utilizarán este mapeo ordenado calculado combinado de campos. Como los campos están en orden de inserción, las clases derivadas anulan las clases base.
y bajo Especificación :
TypeError
se generará si un campo sin un valor predeterminado sigue a un campo con un valor predeterminado. Esto es cierto cuando esto ocurre en una sola clase o como resultado de la herencia de clases.
Tiene algunas opciones aquí para evitar este problema.
La primera opción es usar clases base separadas para forzar los campos con valores predeterminados a una posición posterior en la orden MRO. A toda costa, evite establecer campos directamente en clases que se utilizarán como clases base, como Parent
.
La siguiente jerarquía de clases funciona:
@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
@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
Al extraer campos en clases base separadas con campos sin valores predeterminados y campos con valores predeterminados, y un orden de herencia cuidadosamente seleccionado, puede producir un MRO que coloca todos los campos sin valores predeterminados antes que aquellos con valores predeterminados. El MRO invertido (ignorando object
) para Child
es:
_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent
Tenga en cuenta que Parent
no establece ningún campo nuevo, por lo que no importa aquí que termine como 'último' en el orden de lista de campos. Las clases con campos sin valores predeterminados ( _ParentBase
y _ChildBase
) preceden a las clases con campos con valores predeterminados ( _ParentDefaultsBase
y _ChildDefaultsBase
).
El resultado es Parent
y Child
clases con un campo sano más antiguo, mientras que Child
sigue siendo una subclase 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
y así puedes crear instancias de ambas clases:
>>> 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)
Otra opción es usar solo campos con valores predeterminados; aún puede cometer un error al no proporcionar un school
valor, aumentando uno en __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'")
pero esto hace alterar el orden de los campos; school
termina después de ugly
:
<Signature (name: str, age: int, ugly: bool = True, school: str = <object object at 0x1101d1210>) -> None>
y un verificador de sugerencias de tipo se quejará de que _no_default
no es una cadena.
También puede utilizar el attrs
proyecto , que fue el proyecto que inspiró dataclasses
. Utiliza una estrategia de fusión de herencia diferente; tira de los campos anulados en una subclase al final de la lista de campos, por lo que ['name', 'age', 'ugly']
en la Parent
clase se convierte ['name', 'age', 'school', 'ugly']
en la Child
clase; anulando el campo con un valor predeterminado, attrs
permite la anulación sin necesidad de hacer un baile MRO.
attrs
admite la definición de campos sin sugerencias de tipo, pero sigamos con el modo de sugerencia de tipo admitido configurando 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