Reputation: 21473
This is an extension to this question and raises a problem, which you, my fellow StackOverflowers, will hopefully be able to help me with. From the referenced question, consider the final code sample:
class A(object):
def __init__(self):
print "entering A"
print "leaving A"
class B(object):
def __init__(self):
print "entering B"
super(B, self).__init__()
print "leaving B"
class C(A,B):
def __init__(self):
print "entering c"
super(C, self).__init__()
print "leaving c"
As noted by the poster, when initialising C, the __init__
for B is never called. On considering Raymond Hettinger's post, the code for class A
should be modified to also call super().__init__()
:
class A(object):
def __init__(self):
print "entering A"
super(A, self).__init__()
print "leaving A"
We're all good so far. However, what if our class' __init__
functions accept arguments? Let's assume that all the __init__
functions accept a single argument and for consistency, we'll simply call it foo
, the code is now:
class A(object):
def __init__(self, foo):
print "entering A"
super(A, self).__init__(foo)
print "leaving A"
class B(object):
def __init__(self, foo):
print "entering B"
super(B, self).__init__(foo)
print "leaving B"
class C(A,B):
def __init__(self, foo):
print "entering c"
super(C, self).__init__(foo)
print "leaving c"
Here we hit a snag. When initialising any of the classes A, B or C, we eventually call object.__init__
with a single argument, foo
, which errors with TypeError: object.__init__() takes no parameters
. However, to remove one of the super().__init__
functions would mean the classes are no longer cooperative in the case of multiple inheritance.
After all that, my question is how does one get around this problem? It appears that multiple inheritance is broken in anything but cases where no arguments are passed to the __init__
functions.
UPDATE:
Rob in the comments suggests stripping keyword arguments (referenced in Raymond H's post). This actually works very well in the case of multiple inheritance until your change you code. If one of your functions no longer uses one of the keyword arguments and ceases to strip it without modifying the calling function, You will still receive the TypeError noted above. As such, this seems like a fragile solution for large projects.
Upvotes: 9
Views: 2346
Reputation: 937
Perhaps the following base class makes multi-coop pattern more usable:
self.__init_next__(super())
Base class:
#
# https://stackoverflow.com/questions/16305177/cooperative-multiple-inheritance-issue
#
import inspect
from typing import Any
from typing import MutableMapping # @UnusedImport
from typing import Type
from typing import _AnnotatedAlias # type:ignore[reportAttributeAccessIssue] @UnresolvedImport
from typing import _UnionGenericAlias # type:ignore[reportAttributeAccessIssue] @UnresolvedImport
class BasicHabit_SYS: # cooperative multiple inheritance trait
_USE_DEV_HABIT_ = False
_USE_HABIT_INJECT_ = False
@property # inheritance stops before python::object
def habit_base_type_list(self) -> tuple[type, ...]:
return self.__class__.__mro__[0:-1]
# permit attribute override in __init__()
def habit_has_inject_ignore(self, attr_name:str, attr_type:Type) -> bool:
if hasattr(self, attr_name): # contract: alredy was set
return True
match attr_type:
case _AnnotatedAlias(): # contract: promise to be set
return True
return False
# permit inject None for missing context atruments
def habit_has_inject_optional(self, attr_name:str, attr_type:Type) -> bool: # @UnusedVariable
match attr_type:
case _UnionGenericAlias(_name=type_name) if type_name == "Optional":
return True
return False
# auto-inject instance properties with simple type annotations
def habit_inject_init_args(self, **init_args_dict:MutableMapping) -> None:
self_type = self.__class__.__name__
for base_type in self.habit_base_type_list:
for attr_name, attr_type in base_type.__annotations__.items():
if self.habit_has_inject_ignore(attr_name, attr_type):
continue
if self._USE_DEV_HABIT_: print(f"HABIT INJECT: {self_type=} {attr_name=} {attr_type=}")
if self.habit_has_inject_optional(attr_name, attr_type):
attr_value = init_args_dict.get(attr_name, None)
else:
attr_value = init_args_dict[attr_name] # inject required
setattr(self, attr_name, attr_value)
# collect consumed arguments and call next __init__ in __mro__ chain
def habit_invoke_next_init(self, super_entry:Any) -> None:
assert isinstance(super_entry, super), f"expecting super(): {super_entry=}"
stack_frame_list = inspect.stack()
stack_frame_back = stack_frame_list[1] # contract: call here in derived __init__
origin_value_dict = stack_frame_back.frame.f_locals
origin_class = super_entry.__thisclass__ # type:ignore[reportAttributeAccessIssue] super() internal
origin_init_func = origin_class.__init__
origin_signature = inspect.signature(origin_init_func)
origin_param_dict = origin_signature.parameters
result_param_dict = dict()
self_type = self.__class__.__name__
origin_type = origin_class.__name__
if self._USE_DEV_HABIT_: print(f"HABIT SUPER: {self_type=} {origin_type=}")
for param_entry in origin_param_dict.values():
param_name = param_entry.name
param_kind = param_entry.kind
if param_name == "self":
continue
if self._USE_DEV_HABIT_: print(f"HABIT SUPER: {self_type=} {param_name=} {param_kind=}")
match param_kind:
case inspect.Parameter.KEYWORD_ONLY:
result_param_dict[param_name] = origin_value_dict[param_name]
case inspect.Parameter.VAR_KEYWORD:
result_param_dict.update(origin_value_dict[param_name])
case _:
raise ValueError(f"expecting keyword argument: {param_name=} {param_kind=}")
super_entry.__init__(**result_param_dict)
def __init__(self, **habit_args_dict:MutableMapping) -> None:
if self._USE_HABIT_INJECT_:
self.habit_inject_init_args(**habit_args_dict)
__init_next__ = habit_invoke_next_init
def __repr__(self) -> str:
klaz_name = self.__class__.__name__
args_list = []
for base_type in self.habit_base_type_list:
for attr_name in base_type.__annotations__:
attr_value = getattr(self, attr_name, None)
args_entry = f"{attr_name}='{attr_value}'"
args_list.append(args_entry)
args_text = ",".join(args_list)
return f"{klaz_name}({args_text})"
#
#
#
Usage sample:
#
#
#
from typing import Annotated
from typing import Optional
from habit import BasicHabit_SYS
class ColorKind(
BasicHabit_SYS,
):
color_name:str
color_size:Optional[int]
def __init__(self, *,
color_name:str,
**_context_
):
self.color_name = color_name
self.__init_next__(super())
class SpaceKind(
BasicHabit_SYS,
):
space_name:Optional[str]
space_kind:Annotated[str, "skip this"]
space_size:int
def __init__(self, *,
space_size,
**_context_
):
self.space_size = space_size
self.__init_next__(super())
class MatterCompound(
ColorKind,
SpaceKind,
BasicHabit_SYS,
):
matter_name:str
matter_volume:float
def __init__(self, *,
matter_name:str,
matter_volume:float,
**_context_
):
self.matter_name = matter_name
self.matter_volume = matter_volume
self.__init_next__(super())
BasicHabit_SYS._USE_DEV_HABIT_ = True # enable debug print
BasicHabit_SYS._USE_HABIT_INJECT_ = True # enable magic inject
color_result = ColorKind(
color_name="red",
)
print(color_result)
assert color_result.color_name == "red"
assert color_result.color_size == None # magic inject
matter_result = MatterCompound(
color_name="green", # used by ColorKind
color_size=12, # used by ColorKind, magic inject
space_name="empty", # used by SpaceKind, magic inject
space_size=7, # used by SpaceKind
# space_kind # magic inject to None
matter_name="steel", # used by MatterCompound
matter_volume="expanding", # used by MatterCompound
extra_param="unused", # ignored by this class family
)
print(matter_result)
assert matter_result.color_size == 12 # magic inject
assert matter_result.space_name == "empty" # magic inject
#
#
#
Usage output:
HABIT SUPER: self_type='ColorKind' origin_type='ColorKind'
HABIT SUPER: self_type='ColorKind' param_name='color_name' param_kind=<_ParameterKind.KEYWORD_ONLY: 3>
HABIT SUPER: self_type='ColorKind' param_name='_context_' param_kind=<_ParameterKind.VAR_KEYWORD: 4>
HABIT INJECT: self_type='ColorKind' attr_name='color_size' attr_type=typing.Optional[int]
ColorKind(color_name='red',color_size='None')
HABIT SUPER: self_type='MatterCompound' origin_type='MatterCompound'
HABIT SUPER: self_type='MatterCompound' param_name='matter_name' param_kind=<_ParameterKind.KEYWORD_ONLY: 3>
HABIT SUPER: self_type='MatterCompound' param_name='matter_volume' param_kind=<_ParameterKind.KEYWORD_ONLY: 3>
HABIT SUPER: self_type='MatterCompound' param_name='_context_' param_kind=<_ParameterKind.VAR_KEYWORD: 4>
HABIT SUPER: self_type='MatterCompound' origin_type='ColorKind'
HABIT SUPER: self_type='MatterCompound' param_name='color_name' param_kind=<_ParameterKind.KEYWORD_ONLY: 3>
HABIT SUPER: self_type='MatterCompound' param_name='_context_' param_kind=<_ParameterKind.VAR_KEYWORD: 4>
HABIT SUPER: self_type='MatterCompound' origin_type='SpaceKind'
HABIT SUPER: self_type='MatterCompound' param_name='space_size' param_kind=<_ParameterKind.KEYWORD_ONLY: 3>
HABIT SUPER: self_type='MatterCompound' param_name='_context_' param_kind=<_ParameterKind.VAR_KEYWORD: 4>
HABIT INJECT: self_type='MatterCompound' attr_name='color_size' attr_type=typing.Optional[int]
HABIT INJECT: self_type='MatterCompound' attr_name='space_name' attr_type=typing.Optional[str]
MatterCompound(matter_name='steel',matter_volume='expanding',color_name='green',color_size='12',space_name='empty',space_kind='None',space_size='7')
Upvotes: 0
Reputation: 1232
My answer might be a little off. I've been looking for code to solve my particular problem. But after searching for hours I could not find good example. So I wrote this little test code. I think it is definitely cooperative multiple inheritance example. I really think someone might find it useful. So here we go!
Basically, I had a pretty big class that I wanted to split even further but due to my particular case it had to be the same class. Also all of my child classes had their own inits that I wanted to executed after base init so to speak.
Test Code
"""
Testing MRO Functionality of python 2.6 (2.7)
"""
class Base(object):
def __init__(self, base_arg, **kwargs):
print "Base Init with arg: ", str(base_arg)
super(Base, self).__init__()
def base_method1(self):
print "Base Method 1"
def base_method2(self):
print "Base Method 2"
class ChildA(Base):
def __init__(self, child_a_arg, **kwargs):
super(ChildA, self).__init__(**kwargs)
print "Child A init with arg: ", str(child_a_arg)
def base_method1(self):
print "Base Method 1 overwritten by Child A"
class ChildB(Base):
def __init__(self, child_b_arg, **kwargs):
super(ChildB, self).__init__(**kwargs)
print "Child B init with arg: ", str(child_b_arg)
def base_method2(self):
print "Base Method 2 overwritten by Child B"
class ChildC(Base):
def __init__(self, child_c_arg, **kwargs):
super(ChildC, self).__init__(**kwargs)
print "Child C init with arg: ", str(child_c_arg)
def base_method2(self):
print "Base Method 2 overwritten by Child C"
class Composite(ChildA, ChildB, ChildC):
def __init__(self):
super(Composite, self).__init__(base_arg=1, child_a_arg=2, child_b_arg=3, child_c_arg=4)
print "Congrats! Init is complete!"
if __name__ == '__main__':
print "MRO: ", str(Composite.__mro__), "\n"
print "*** Init Test ***"
test = Composite()
print "*** Base Method 1 Test ***"
test.base_method1()
print "*** Base Method 2 Test ***"
test.base_method2()
Output
MRO: (<class '__main__.Composite'>,
<class '__main__.ChildA'>,
<class '__main__.ChildB'>,
<class '__main__.ChildC'>,
<class '__main__.Base'>,
<type 'object'>)
*** Init Test ***
Base Init with arg: 1
Child C init with arg: 4
Child B init with arg: 3
Child A init with arg: 2
Congrats! Init is complete!
*** Base Method 1 Test ***
Base Method 1 overwritten by Child A
*** Base Method 2 Test ***
Base Method 2 overwritten by Child B
Upvotes: 2
Reputation: 21
Admittedly, this solution may not be the most pythonic or ideal, but creating a wrapper class for object like so allows you to pass arguments around each __init__
in the inheritance:
class Object(object):
def __init__(self,*args,**kwargs):
super(Object,self).__init__()
class A(Object):
def __init__(self,*args,**kwargs):
super(A,self).__init__(*args,**kwargs)
class B(Object):
def __init__(self,*args,**kwargs):
super(B,self).__init__(*args,**kwargs)
class C(A,B):
def __init__(self,*args,**kwargs):
super(C,self).__init__(*args,**kwargs)
Upvotes: 2