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.
CodePudding user response:
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)()