Herencia de clase en dataclasses de Python 3.7

Actualmente estoy probando las nuevas construcciones de clase de datos introducidas en Python 3.7. Actualmente estoy atascado en tratar de hacer alguna herencia de una clase padre. Parece que el orden de los argumentos se ve frustrado por mi enfoque actual, por lo que el parámetro bool en la clase secundaria se pasa antes que los otros parámetros. Esto está causando un error de tipo.

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

Cuando ejecuto este código obtengo este TypeError :

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

¿Cómo puedo solucionar esto?

La forma en que las clases de datos combinan los atributos le impide poder usar los atributos con valores predeterminados en una clase base y luego usar los atributos sin un valor predeterminado (atributos posicionales) en una subclase.

Esto se debe a que los atributos se combinan comenzando desde la parte inferior de la MRO y construyendo una lista ordenada de los atributos en el primer orden visto; Las anulaciones se guardan en su ubicación original. Entonces, el Parent comienza con ['name', 'age', 'ugly'] , donde ugly tiene un valor predeterminado, y luego el Child agrega ['school'] al final de esa lista (con ugly ya en la lista). Esto significa que terminas con ['name', 'age', 'ugly', 'school'] y debido a que la school no tiene un valor predeterminado, esto resulta en una lista de argumentos no válida para __init__ .

Esto se documenta en las clases de datos PEP-557 , bajo herencia :

Cuando el decorador de @dataclass crea la @dataclass , examina todas las clases base en el MRO inverso (es decir, comienza en el object ) y, para cada clase de datos que encuentra, agrega los campos de esa clase base a un mapeo ordenado de campos. Después de agregar todos los campos de clase base, agrega sus propios campos a la asignación ordenada. Todos los métodos generados utilizarán esta asignación ordenada, combinada y calculada de campos. Debido a que los campos están en orden de inserción, las clases derivadas anulan las clases base.

y bajo especificación :

TypeError generará si un campo sin un valor predeterminado sigue a un campo con un valor predeterminado. Esto es cierto ya sea cuando esto ocurre en una sola clase, o como resultado de la herencia de la clase.

Tienes algunas opciones aquí para evitar este problema.

La primera opción es usar clases base separadas para forzar campos con valores predeterminados a una posición posterior en el orden MRO. A toda costa, evite establecer campos directamente en las clases que se utilizarán como clases base, como Parent .

La siguiente jerarquía de clases funciona:

 # 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 

Al extraer los campos en clases de base separadas con los campos sin valores predeterminados y los 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 el object ) para el 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 ‘último’ en el orden de listado del campo. Las clases con campos sin valores predeterminados ( _ParentBase y _ChildBase ) preceden a las clases con campos con valores predeterminados ( _ParentDefaultsBase y _ChildDefaultsBase ).

El resultado son las clases Parent y Child con un campo sano más antiguo, mientras que Child sigue siendo una subclase de Parent :

 >>> from inspect import signature >>> signature(Parent)  None> >>> signature(Child)  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 para no proporcionar un valor school , 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 altera el orden del campo; school termina después de lo ugly :

 ) -> None> 

y un comprobador de sugerencias de tipo se quejará de que _no_default no es una cadena.

También puede usar el proyecto attrs , que fue el proyecto que inspiró las dataclasses . Utiliza una estrategia de fusión de herencia diferente; arrastra los campos anulados en una subclase hasta el final de la lista de campos, por lo que ['name', 'age', 'ugly'] en la clase Parent convierte en ['name', 'age', 'school', 'ugly'] en la clase Child ; Al anular el campo con un valor predeterminado, attrs permite la anulación sin necesidad de hacer una danza MRO.

attrs admite la definición de campos sin sugerencias de tipo, pero nos auto_attribs=True modo de sugerencias 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 

Está viendo este error porque se está agregando un argumento sin un valor predeterminado después de un argumento con un valor predeterminado. El orden de inserción de los campos heredados en la clase de datos es el reverso del Orden de resolución de métodos , lo que significa que los campos Parent son los primeros, incluso si sus hijos los sobrescriben más tarde.

Un ejemplo de PEP-557 – Clases de datos :

 @dataclass class Base: x: Any = 15.0 y: int = 0 @dataclass class C(Base): z: int = 10 x: int = 15 

La lista final de campos es, en orden, x, y, z . El tipo final de x es int , como se especifica en la clase C

Desafortunadamente, no creo que haya ninguna manera de evitar esto. Mi entendimiento es que si la clase principal tiene un argumento predeterminado, entonces ninguna clase secundaria puede tener argumentos no predeterminados.

Basándome en la solución de Martijn Pieters hice lo siguiente:

1) Crear una mezcla implementando el 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) Luego en las clases con el problema de herencia:

 from src.utils import no_default, NoDefaultAttributesChild @dataclass class MyDataclass(DataclassWithDefaults, NoDefaultAttributesPostInitMixin): attr1: str = no_default