'Python dataclasses inheritance and default values
Given the following hierarchy of python dataclasses:
@dataclass
class A:
a: str
aa: str
@dataclass
class B(A):
b: str
@dataclass
class C(A):
c: str
@dataclass
class D(B, C):
d: str
Is there any general method that allows you to insert an attribute with a default value in this inheritance hierarchy? For instance, the following produces a Non-default argument(s) follows default argument(s) defined in 'B'
error for class D
.
@dataclass
class A:
a: str
aa: str
@dataclass
class B(A):
b: str = 'b' # <-- default value
@dataclass
class C(A):
c: str
@dataclass
class D(B, C):
d: str
I suppose one solution could be to give all attributes in the hierarchy a default value of None
, and add a __post_init__()
validation which raises some exception if any attributes are None
, but this doesn't seem right and you would need it in every dataclass in the hierarchy.
Class inheritance in Python 3.7 dataclasses proposes a solution where you create two separate branches of the inheritance hierarchy; one for default values and one without. The drawback here is that the hierarchy quickly becomes mangled with a lot of classes, but if there are no better options, I suppose this is as good as it gets.
Edit
I've come up with a meta_dataclass
which does some hackery to at least solve my problem. I'm sure it breaks all kinds of rules, but perhaps someone can improve upon it.
It allows a hierarchy of meta_dataclasses
where any attribute in the hierarchy can have a default value. It is motivated by the solution presented in Class inheritance in Python 3.7 dataclasses. It effectively creates two inheritance branches, one with default values and one without, but the creation of the extra classes required to achieve this is encapsulated in the mete_dataclass
decorator. A current drawback is that a meta_dataclass
can only inherit from other meta_dataclasses
.
Full code:
from dataclasses import make_dataclass, MISSING, field
class Dataclass:
pass
class Declarations:
pass
class Definitions:
pass
def meta_dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False):
def wrap(cls):
declaration_bases = []
definition_bases = []
for base in cls.__bases__:
if issubclass(base, Dataclass):
declaration_bases += [c for c in base.__bases__ if issubclass(c, Declarations)]
definition_bases += [c for c in base.__bases__ if issubclass(c, Definitions)]
elif len(cls.__bases__) == 1 and base != object:
raise ValueError(f'meta_dataclasses can only inherit from other meta_dataclasses. '
f'{cls} inherits from {base}')
declaration_bases.append(Declarations)
definition_bases.append(Definitions)
fields = []
if hasattr(cls, '__annotations__'):
for field_name, field_type in cls.__annotations__.items():
f = field(default=cls.__dict__[field_name]) if field_name in cls.__dict__ else field()
fields.append((field_name, field_type, f))
declarations = make_dataclass(cls_name=f'{cls.__name__}_Declarations',
bases=tuple(declaration_bases),
fields=[f for f in fields if isinstance(f[2].default, type(MISSING))],
init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)
definitions = make_dataclass(cls_name=f'{cls.__name__}_Definitions',
bases=tuple(definition_bases),
fields=[f for f in fields if not isinstance(f[2].default, type(MISSING))],
init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)
cls_wrapper = make_dataclass(cls_name=cls.__name__, fields=[], bases=(Dataclass, definitions, declarations),
namespace=cls.__dict__, init=init, repr=repr, eq=eq, order=order,
unsafe_hash=unsafe_hash, frozen=frozen)
return cls_wrapper
if cls is None:
return wrap
else:
return wrap(cls)
Example:
@meta_dataclass
class A:
a: str
aa: str
@meta_dataclass
class B(A):
b: str = 'b'
@meta_dataclass
class C(A):
c: str
@meta_dataclass
class D(B, C):
d: str
>>> help(D)
Help on class D in module types:
class D(__main__.Dataclass, D_Definitions, D_Declarations)
| D(a: str, aa: str, c: str, d: str, b: str = 'b') -> None
|
| D(a: str, aa: str, c: str, d: str, b: str = 'b')
|
| Method resolution order:
| D
| __main__.Dataclass
| D_Definitions
| B_Definitions
| C_Definitions
| A_Definitions
| __main__.Definitions
| D_Declarations
| B_Declarations
| C_Declarations
| A_Declarations
| __main__.Declarations
| builtins.object
Illustration:
The below image illustrates what the @meta_dataclass
decorator does to a single class.
Solution 1:[1]
This is a well-known issue for data classes, there are several workarounds but this is solved every elegantly in Python 3.10 in the PR that solved the issue 43532.
It would work the following way:
from dataclasses import dataclass
@dataclass(kw_only=true)
class Base:
type: str
counter: int = 0
@dataclass(kw_only=true)
class Foo(Base):
id: int
This is deeply explained at: https://medium.com/@aniscampos/python-dataclass-inheritance-finally-686eaf60fbb5
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Rafael Aguilar |