简体   繁体   中英

multiple python class inheritance

I am trying to understand python's class inheritance methods and I have some troubles figuring out how to do the following:

How can I inherit a method from a class conditional on the child's input?

I have tried the following code below without much success.

class A(object):
    def __init__(self, path):
        self.path = path

    def something(self):
        print("Function %s" % self.path)   


class B(object):
    def __init__(self, path):
        self.path = path
        self.c = 'something'

    def something(self):
        print('%s function with %s' % (self.path, self.c))


class C(A, B):
    def __init__(self, path):
        # super(C, self).__init__(path)

        if path=='A':
            A.__init__(self, path)
        if path=='B':
            B.__init__(self, path)
        print('class: %s' % self.path)


if __name__ == '__main__':
    C('A')
    out = C('B')
    out.something()

I get the following output:

class: A
class: B
Function B

While I would like to see:

class: A
class: B
B function with something

I guess the reason why A.something() is used (instead of B.something() ) has to do with the python's MRO.

Calling __init__ on either parent class does not change the inheritance structure of your classes, no. You are only changing what initialiser method is run in addition to C.__init__ when an instance is created. C inherits from both A and B , and all methods of B are shadowed by those on A due to the order of inheritance.

If you need to alter class inheritance based on a value in the constructor, create two separate classes , with different structures. Then provide a different callable as the API to create an instance:

class CA(A):
    # just inherit __init__, no need to override

class CB(B):
    # just inherit __init__, no need to override

def C(path):
    # create an instance of a class based on the value of path
    class_map = {'A': CA, 'B': CB}
    return class_map[path](path)

The user of your API still has name C() to call; C('A') produces an instance of a different class from C('B') , but they both implement the same interface so this doesn't matter to the caller.

If you have to have a common 'C' class to use in isinstance() or issubclass() tests, you could mix one in, and use the __new__ method to override what subclass is returned:

class C:
    def __new__(cls, path):
        if cls is not C:
            # for inherited classes, not C itself
            return super().__new__(cls)
        class_map = {'A': CA, 'B': CB}
        cls = class_map[path]
        # this is a subclass of C, so __init__ will be called on it
        return cls.__new__(cls, path)

class CA(C, A):
    # just inherit __init__, no need to override
    pass

class CB(C, B):
    # just inherit __init__, no need to override
    pass

__new__ is called to construct the new instance object; if the __new__ method returns an instance of the class (or a subclass thereof) then __init__ will automatically be called on that new instance object. This is why C.__new__() returns the result of CA.__new__() or CB.__new__() ; __init__ is going to be called for you.

Demo of the latter:

>>> C('A').something()
Function A
>>> C('B').something()
B function with something
>>> isinstance(C('A'), C)
True
>>> isinstance(C('B'), C)
True
>>> isinstance(C('A'), A)
True
>>> isinstance(C('A'), B)
False

If neither of these options are workable for your specific usecase, you'd have to add more routing in a new somemethod() implementation on C , which then calls either A.something(self) or B.something(self) based on self.path . This becomes cumbersome really quickly when you have to do this for every single method , but a decorator could help there:

from functools import wraps

def pathrouted(f):
    @wraps
    def wrapped(self, *args, **kwargs):
        # call the wrapped version first, ignore return value, in case this
        # sets self.path or has other side effects
        f(self, *args, **kwargs)
        # then pick the class from the MRO as named by path, and call the
        # original version
        cls = next(c for c in type(self).__mro__ if c.__name__ == self.path)
        return getattr(cls, f.__name__)(self, *args, **kwargs)
    return wrapped

then use that on empty methods on your class:

class C(A, B):
    @pathrouted
    def __init__(self, path):
        self.path = path
        # either A.__init__ or B.__init__ will be called next

    @pathrouted
    def something(self):
        pass  # doesn't matter, A.something or B.something is called too

This is, however, becoming very unpythonic and ugly.

While Martijn's answer is (as usual) close to perfect, I'd just like to point out that from a design POV, inheritance is the wrong tool here.

Remember that implementation inheritance is actually a static and somehow restricted kind of composition/delegation, so as soon as you want something more dynamic the proper design is to eschew inheritance and go for full composition/delegation, canonical examples being the State and the Strategy patterns. Applied to your example, this might look something like:

class C(object):
    def __init__(self, strategy):
        self.strategy = strategy

    def something(self):
        return self.strategy.something(self)

class AStrategy(object):
    def something(self, owner):
        print("Function A")

class BStrategy(object):
    def __init__(self):
        self.c = "something"

    def something(self, owner):
        print("B function with %s" % self.c)


if __name__ == '__main__':
    a = C(AStrategy())
    a.something()
    b = C(BStrategy())
    b.something()

Then if you need to allow the user to specify the strategy by name (as string), you can add the factory pattern to the solution

STRATEGIES = {
    "A": AStrategy,
    "B": BStrategy, 
    }

def cfactory(strategy_name):
  try:
      strategy_class = STRATEGIES[strategy_name]
  except KeyError:
      raise ValueError("'%s' is not a valid strategy" % strategy_name)
  return C(strategy_class())

if __name__ == '__main__':
    a = cfactory("A")
    a.something()
    b = cfactory("B")
    b.something()

Martijn's answer explained how to choose an object inheriting from one of two classes. Python also allows to easily forward a method to a different class:

>>> class C:
    parents = { 'A': A, 'B': B }
    def __init__(self, path):
        self.parent = C.parents[path]
        self.parent.__init__(self, path)                 # forward object initialization
    def something(self):
        self.parent.something(self)                      # forward something method


>>> ca = C('A')
>>> cb = C('B')
>>> ca.something()
Function A
>>> cb.something()
B function with something
>>> ca.path
'A'
>>> cb.path
'B'
>>> cb.c
'something'
>>> ca.c
Traceback (most recent call last):
  File "<pyshell#46>", line 1, in <module>
    ca.c
AttributeError: 'C' object has no attribute 'c'
>>> 

But here class C does not inherit from A or B:

>>> C.__mro__
(<class '__main__.C'>, <class 'object'>)

Below is my original solution using monkey patching :

>>> class C:
    parents = { 'A': A, 'B': B }
    def __init__(self, path):
        parent = C.parents[path]
        parent.__init__(self, path)                      # forward object initialization
        self.something = lambda : parent.something(self) # "borrow" something method

it avoids the parent attribute in C class, but is less readable...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM