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
attr.ib(kw_only=True)
, consulta github.com/python-attrs/attrs/issues/38Está viendo este error porque se agrega 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 inverso del Orden de resolución del método , lo que significa que los
Parent
campos vienen primero, incluso si sus hijos los sobrescriben más tarde.Un ejemplo de PEP-557 - Clases de datos :
Desafortunadamente, no creo que haya ninguna forma de evitar esto. Tengo entendido que si la clase principal tiene un argumento predeterminado, ninguna clase secundaria puede tener argumentos no predeterminados.
fuente
Puede usar atributos con valores predeterminados en las clases principales si los excluye de la función init. Si necesita la posibilidad de anular el valor predeterminado en init, amplíe el código con la respuesta 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
fuente
basado en la solución de Martijn Pieters hice lo siguiente:
1) Crea una mezcla implementando 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
EDITAR:
Después de un tiempo también encuentro problemas con esta solución con mypy, el siguiente código soluciona el problema.
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
fuente
El siguiente enfoque trata este problema mientras se usa Python puro
dataclasses
y sin mucho código repetitivo.Los
ugly_init: dataclasses.InitVar[bool]
sirve como un pseudo-campo sólo para ayudar a hacer la inicialización y se perderán una vez que se crea la instancia. Mientrasugly: bool = field(init=False)
es un miembro de instancia que no se inicializará mediante el__init__
método, pero se puede inicializar alternativamente mediante el__post_init__
método (puede encontrar más 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()
fuente
Volví a esta pregunta después de descubrir que las clases de datos pueden estar obteniendo un parámetro de decorador que permite reordenar los campos. Este es sin duda un desarrollo prometedor, aunque el desarrollo de esta característica parece haberse estancado un poco.
En este momento, puede obtener este comportamiento, además de algunas otras sutilezas, utilizando dataclassy , mi reimplementación de clases de datos que supera frustraciones como esta. Usar
from dataclassy
en lugar defrom dataclasses
en el ejemplo original significa que se ejecuta sin errores.El uso de inspeccionar para imprimir la firma de
Child
aclara lo que está sucediendo; el resultado es(name: str, age: int, school: str, ugly: bool = True)
. Los campos siempre se reordenan para que los campos con valores predeterminados vengan después de los campos sin ellos en los parámetros del inicializador. Ambas listas (los campos sin valores predeterminados y los que los tienen) todavía están ordenados por orden de definición.Enfrentarme cara a cara con este problema fue uno de los factores que me impulsó a escribir un reemplazo para las clases de datos. Las soluciones que se detallan aquí, aunque son útiles, requieren que el código se contorsione hasta tal punto que nieguen por completo la ventaja de legibilidad que ofrece el enfoque ingenuo de las clases de datos (por el que el orden de los campos es trivialmente predecible).
fuente
Una posible solución es utilizar el parche de mono para agregar los campos principales
import dataclasses as dc def add_args(parent): def decorator(orig): "Append parent's fields AFTER orig's fields" # Aggregate fields ff = [(f.name, f.type, f) for f in dc.fields(dc.dataclass(orig))] ff += [(f.name, f.type, f) for f in dc.fields(dc.dataclass(parent))] new = dc.make_dataclass(orig.__name__, ff) new.__doc__ = orig.__doc__ return new return decorator class Animal: age: int = 0 @add_args(Animal) class Dog: name: str noise: str = "Woof!" @add_args(Animal) class Bird: name: str can_fly: bool = True Dog("Dusty", 2) # --> Dog(name='Dusty', noise=2, age=0) b = Bird("Donald", False, 40) # --> Bird(name='Donald', can_fly=False, age=40)
También es posible anteponer campos no predeterminados, marcando
if f.default is dc.MISSING
, pero esto probablemente sea demasiado sucio.Si bien el parche de mono carece de algunas características de herencia, aún se puede usar para agregar métodos a todas las clases pseudo-secundarias.
Para un control más detallado, configure los valores predeterminados usando
dc.field(compare=False, repr=True, ...)
fuente
Puede utilizar una versión modificada de clases de datos, que generará un
__init__
método de palabra clave únicamente :import dataclasses def _init_fn(fields, frozen, has_post_init, self_name): # fields contains both real fields and InitVar pseudo-fields. globals = {'MISSING': dataclasses.MISSING, '_HAS_DEFAULT_FACTORY': dataclasses._HAS_DEFAULT_FACTORY} body_lines = [] for f in fields: line = dataclasses._field_init(f, frozen, globals, self_name) # line is None means that this field doesn't require # initialization (it's a pseudo-field). Just skip it. if line: body_lines.append(line) # Does this class have a post-init function? if has_post_init: params_str = ','.join(f.name for f in fields if f._field_type is dataclasses._FIELD_INITVAR) body_lines.append(f'{self_name}.{dataclasses._POST_INIT_NAME}({params_str})') # If no body lines, use 'pass'. if not body_lines: body_lines = ['pass'] locals = {f'_type_{f.name}': f.type for f in fields} return dataclasses._create_fn('__init__', [self_name, '*'] + [dataclasses._init_param(f) for f in fields if f.init], body_lines, locals=locals, globals=globals, return_type=None) def add_init(cls, frozen): fields = getattr(cls, dataclasses._FIELDS) # Does this class have a post-init function? has_post_init = hasattr(cls, dataclasses._POST_INIT_NAME) # Include InitVars and regular fields (so, not ClassVars). flds = [f for f in fields.values() if f._field_type in (dataclasses._FIELD, dataclasses._FIELD_INITVAR)] dataclasses._set_new_attribute(cls, '__init__', _init_fn(flds, frozen, has_post_init, # The name to use for the "self" # param in __init__. Use "self" # if possible. '__dataclass_self__' if 'self' in fields else 'self', )) return cls # a dataclass with a constructor that only takes keyword arguments def dataclass_keyword_only(_cls=None, *, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False): def wrap(cls): cls = dataclasses.dataclass( cls, init=False, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen) return add_init(cls, frozen) # See if we're being called as @dataclass or @dataclass(). if _cls is None: # We're called with parens. return wrap # We're called as @dataclass without parens. return wrap(_cls)
(también publicado como esencia , probado con el backport de Python 3.6)
Esto requerirá definir la clase secundaria como
@dataclass_keyword_only class Child(Parent): school: str ugly: bool = True
Y generaría
__init__(self, *, name:str, age:int, ugly:bool=True, school:str)
(que es python válido). La única advertencia aquí es no permitir inicializar objetos con argumentos posicionales, pero por lo demás es un proceso completamente regulardataclass
sin trucos desagradables.fuente