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