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 Parentcomienza con ['name', 'age', 'ugly'], where uglytiene un valor predeterminado, y luego se Childagrega ['school']al final de esa lista (con uglyya en la lista). Esto significa que termina con ['name', 'age', 'ugly', 'school']y debido a schoolque 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 @dataclassdecorador 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 :
TypeErrorse 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 Childes:
_ParentBase
_ChildBase
_ParentDefaultsBase
_ChildDefaultsBase
Parent
Tenga en cuenta que Parentno 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 ( _ParentBasey _ChildBase) preceden a las clases con campos con valores predeterminados ( _ParentDefaultsBasey _ChildDefaultsBase).
El resultado es Parenty Childclases con un campo sano más antiguo, mientras que Childsigue 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 schoolvalor, 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; schooltermina 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_defaultno es una cadena.
También puede utilizar el attrsproyecto , 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 Parentclase se convierte ['name', 'age', 'school', 'ugly']en la Childclase; anulando el campo con un valor predeterminado, attrspermite la anulación sin necesidad de hacer un baile MRO.
attrsadmite 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