from dataclasses import dataclass, field
from typing import Dict
@dataclass
class A:
a: Dict[str, int] = field(default_factory=dict)
def __post_init__(self):
self.a = {'a1': 0, 'a2': 0}
def add_key_a(self, key):
self.a['key'] = 0
@dataclass
class B:
b: Dict[str, int] = field(default_factory=dict)
def __post_init__(self):
self.b = {'b1': 0, 'b2': 0}
def add_key_b(self, key):
self.b['key'] = 0
@dataclass
class C(A, B):
pass
user = C()
print(user)
# C(b={}, a={'a1': 0, 'a2': 0})
I get an empty 'b' dictionary, but expected to get "{'b1': 0, 'b2': 0}". I did a search on the Internet and I didn't find a proper explanation and solution to this problem (probably should've search better). So, I ask you guys to help me find out how to solve this problem.
Using multiple inheritance requires classes to be cooperative by calling their super()
methods in appropriate places. Just like __init__
should defer to super().__init__
, __post_init__
should defer to super() .__post_init__
.
Since dataclass
es do not have a common baseclass, deferring to a super
method must be defensive; getattr
with a no-op function can be used to skip the super call as needed.
@dataclass
class A:
a: Dict[str, int] = field(default_factory=dict)
def __post_init__(self):
getattr(super(), "__post_init__", lambda: None)()
self.a = {'a1': 0, 'a2': 0}
def add_key_a(self, key):
self.a['key'] = 0
@dataclass
class B:
b: Dict[str, int] = field(default_factory=dict)
def __post_init__(self):
getattr(super(), "__post_init__", lambda: None)()
self.b = {'b1': 0, 'b2': 0}
def add_key_b(self, key):
self.b['key'] = 0
Naively, one would just use super().__post_init__()
to call __post_init__
of the super
class. But since dataclass
works via code generation instead of inheritance, the super class is object
– which has no __post_init__
method! Thus, the final lookup will fail:
>>> c = C()
>>> super(C, c).__post_init__ # initial __post_init__ used by C instances
<bound method A.__post_init__ of C(b={}, a={'a1': 0, 'a2': 0})>
>>> super(A, c).__post_init__ # second __post_init__ used by C
<bound method B.__post_init__ of C(b={}, a={'a1': 0, 'a2': 0})>
>>> super(B, c).__post_init__ # final __post_init__ used by C
...
AttributeError: 'super' object has no attribute '__post_init__'
The way to fix this is straightforward: just catch the AttributeError
if it occurs and do nothing in that case. We could do that with try:
except:
blocks, but there is a terser way.
The builtin getattr
function allows to get an attribute or a default. Instead of a.b
, we can use getattr(a, "b", default)
. Since we are getting a method to call, a useful default is a callable that does nothing.
>>> lambda : None # callable that does nothing
<function __main__.<lambda>()>
>>> # definition | call
>>> (lambda: None)() # calling does nothing
>>> # getattr fetches attribute/method...
>>> getattr(super(A, c), "__post_init__")
<bound method B.__post_init__ of C(b={}, a={'a1': 0, 'a2': 0})>
>>> # ... and can handle a default
>>> getattr(super(B, c), "__post_init__", lambda: None)
<function __main__.<lambda>()>
Putting this in action, we replace the ….__post_init__
with getattr
. Notably, just as we needed ()
for the call after the ….__post_init__
lookup, we still need ()
for the call after the getattr
lookup.
super().__post_init__()
#super | method | call
# |super | | method | | default | | call
getattr(super(), "__post_init__", lambda: None)()