Endophage
Endophage

Reputation: 21473

Cooperative multiple inheritance issue

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

Answers (3)

Andrei Pozolotin
Andrei Pozolotin

Reputation: 937

Perhaps the following base class makes multi-coop pattern more usable:

  • clearly spells the intent: self.__init_next__(super())
  • collects back consumed keyword arguments
  • enforces the use of keyword arguments
  • provides automatic property inject

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

Barmaley
Barmaley

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

ievans3024
ievans3024

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

Related Questions