Sunday, March 24, 2019

Cooperative Multiple Inheritance in Python: Practice (English)

It is not strickly required to read, theoretical part. The main result of it is provided here. But, If you do read theoretical part, you can skip examples, provided here.

The last example illustrate how to implement mixin in Python.


Theoretically, not for any inheritance graph $C^3$ Linearization Algorithm can be linearized. In practice, it is rarely happen, and if it did, Python will emit en Error. Another name of this algorithm is MRO - Method Resolution Order.

That's right, the Diamond problem can be solved (practically). We can use inheritance where both object has the state, and this will not cause any problem. No more virtual inheritance (as C++ did), or multiple interface inheritance (as Java did).


There is more below.
Ниже есть продолжение.


Note: We will examine below only so called new-style classes. All classes in Python 3 are new style. Everything listed here holds in Python 2 if there no classic style class involve in inheritance graph.

Note: In Python order in which classes appear in inheritance tuple is matter. That's it, if we have $class X(A,B)$, and in X there is call to super(), Python will check class A first, and then, may be after checking in some other classes (if appropriate method will not be found), it will eventually check in class B.

MRO has following basic properties:

a) Children precede their parents.
b) If a class inherits from multiple classes, they are kept in the order specified in the tuple of the base class.

It also have following properties:

* Suppose, you have new-style class definition $class X(Y_1, Y_2,,..Y_N): pass$, then MRO of X will start from X [see, proposition (0.0)]

* object has to be last in (any) MRO consists of new-style objects only. [see, proposition (2.0)]

* If we have MRO of some node $A_1$, than we chose some node in that MRO, it's MRO will be subchain of $A_1$ in MRO. [see, proposition (2.1)]

* Let G be some inheritance graph with at least one node. Let chose some node $A_1$. Let MRO of $A_1$ be $L[A_1].
If we define new class $B(A_1, A_2...A_n)$, that is class that inherits from n classes, then MRO of B will be $L[B]=B+A_1+...$.

[see, proposition (2.2)]

This means that if we have some general form class $B(A_1, A_2...A_n)$ the MRO of it will be always B, than first class in the inheritance tuple. In particular if inherit from single class, case $n=1$, the MRO will starts from B and than (single) parent class $A_1$. More over, when $n=2$ the MRO will starts from B and then first class in the inheritance tuple $A_1$. There is no guarantee that the next class will be $A_2$, actually it can be it's sub-class, see Example 2 below.

* You can always ask $X.__mro__$ and avoid explicit computing.


Example 1


Based on https://www.python.org/download/releases/2.3/mro/

Suppose, we have following class hierarchy:

$class D(O): pass$

$class E(O): pass$

$class B(D, E): pass$

where $O$ denotes $`object`$.

We can visualize the inheritance graph as follows:




By proposition (0.0) $L[B(D, E)]=B...$. By proposition (2.0) "the last class is `object` $L[B(D, E)]=...,`object`=O$. So, we know, that

$L[B(D, E)]=B,...,O$

We assume, that MRO exists, so we have 2 possibilities

1) $L[B(D, E)]=B, D, E, O
2) $L[B(D, E)]=B, E, D, O

(D, E) appears in tuple inherits of B. By) b) the order that they appears should be preserved, so D should go before E, so option 2) is impossible, that's why the answer is

$L[B(D, E)]=B, D, E, O



Example 2.

Let's look on another example. Python have standard library class Dict. There is also OrderDict that extends Dict. Python have also Counter class. Counter inherit from Dict. It means, that it count have many time specific character appear in the string, but the order that it will print out the result is unspecified.

Let's define custom class

$class OrderedCounter(Counter, OrderDict): pass$

It appears, that we can actually change the "inheritance chain" of the Counter, it will appeared as we dynamically change the immediate parent of Counter from Dict to OrderDict. More precisely, we can change MRO of Counter.

As you can see in the picture, we have diamond inheritance problem here.



We will not calculate full MRO, but we will put enough constaint on it to establish that first object in the MRO of OrderedCounter is Counter and OrderDict goes before Dict.


So, we have to prove:

I. MRO of OrderedCounter starts from OrderCounter, Counter.
II. object has to be last in MRO.
III. OrderDict goes before Dict in MRO of OrderedCounter.

I. By proposition (0.0), MRO of OrderedCounter starts from OrderCounter. OrderCounter has 2 direct children Counter and OrderDict. By a) "children precede their parents" The first element in MRO has to be Counter or OrderDict. From b) "order specified in the inheritance tuple preserves" follows that Counter has to go before OrderDict, because they list in the inheritance tuple of OrderCounter. So, Counter has to first element in MRO.

II. By proposition (2.0)

III.
So, currently we established that

(#) L[OrderCount]=OrderCounter, Counter,...,object.

We have following options:

1) L[OrderCount]=OrderCounter, Counter, Dict, OrderDict, object
2) L[OrderCount]=OrderCounter, Counter, OrderDict, Dict, object

there is no other possible that will not contradict (#) and consists of objects of given inheritance graph. Let's write (1) in a bit explicit way:

1) L[OrderCount]=OrderCounter(Counter, OrderedDict), Counter(Dict), Dict(object), OrderDict(Dict), object

On one side, Dict is listed before OrderDict, on other side, we have OrderDict(Dict) and by a) "Children precede their parents" it follows that Dict should be listed before OrderDict. Contradiction.

Assuming that the given graph is linerizable (Python doesn't emit error), it has to be 2), that is

L[OrderCount]=OrderCounter, Counter, OrderDict, Dict, object

and OrderDict is called before Dict in MRO.

***

The exact MRO algorithm is provided in theortical part. In practice, the best chance, you will never used it. First of all, you can ask the Python do it for you. $X.__mro__$ will give you result of applying Method Resolution Order algorithm. If MRO for your inheritance graph can't be computed Python will emit an Error.

But, what is more important, in order to reason about MRO it is sufficient to know about properties listed above. We just did it in 2 examples above.

***

For more elaborate example see https://en.wikipedia.org/wiki/C3_linearization or here https://www.python.org/download/releases/2.3/mro/

Raymond Hettinger claims, that using (cooperative) multiple inheritance in Python make use of Dependecy Injection unnecessary. You can watch his video here "Super considered super!" https://www.youtube.com/watch?v=EiOglTERPEo and read his blogpost here https://rhettinger.wordpress.com/2011/05/26/super-considered-super/ (Python 3 syntax http://code.activestate.com/recipes/577720-how-to-use-super-effectively/). Note, however, he use incorrect definition of inheritance. Code re-use is the feature of mixin.

Multiple inheritance in Python is cooperative.

For reorderable method calls to work, the classes need to be designed cooperatively. This presents three easily solved practical issues:

1) the method being called by super() needs to exist
2) the caller and callee need to have a matching argument signature
3) and every occurrence of the method needs to use super() [Mark as bold is mine]

1) Let’s first look at strategies for getting the caller’s arguments to match the signature of the called method. This is a little more challenging than traditional method calls where the callee is known in advance. With super(), the callee is not known at the time a class is written (because a subclass written later may introduce new classes into the MRO [for example, see example 2 above).

One approach is to stick with a fixed signature using positional arguments. [For example]... to use a fixed signature of two arguments, a key and a value...

A more flexible approach is to have every method in the ancestor tree cooperatively designed to accept keyword arguments and a keyword-arguments dictionary, to remove any arguments that it needs, and to forward the remaining arguments using **kwds, eventually leaving the dictionary empty for the final call in the chain.

Each level strips-off the keyword arguments that it needs so that the final empty dict can be sent to a method that expects no arguments at all (for example, object.__init__() expects zero arguments):
...
2) Having looked at strategies for getting the caller/callee argument patterns to match, let’s now look at how to make sure the target method exists.

...We know that object has an __init__() method and that object is always the last class in the MRO chain, so any sequence of calls to super().__init__() is guaranteed to end with a call to object.__init__() method. In other words, we’re guaranteed that the target of the super() call is guaranteed to exist and won’t fail with an AttributeError.

For cases where object doesn’t have the method of interest (a draw() method for example), we need to write a root class that is guaranteed to be called before object. The responsibility of the root class is simply to eat the method call without making a forwarding call using super().

Root.draw() can also employ defensive programming using an assertion to ensure it isn’t masking some other draw() method later in the chain. This could happen if a subclass erroneously incorporates a class that has a draw() method but doesn’t inherit from Root.
...
If subclasses want to inject other classes into the MRO, those other classes also need to inherit from Root so that no path for calling draw() can reach object without having been stopped by Root.draw(). This should be clearly documented so that someone writing new cooperating classes will know to subclass from Root...

3) The techniques shown above assure that super() calls a method that is known to exist and that the signature will be correct; however, we’re still relying on super() being called at each step so that the chain of delegation continues unbroken. This is easy to achieve if we’re designing the classes cooperatively – just add a super() call to every method in the chain.

The three techniques listed above provide the means to design cooperative classes that can be composed or reordered by subclasses.
...
Occasionally, a subclass may want to use cooperative multiple inheritance techniques with a third-party class that wasn’t designed for it (perhaps its method of interest doesn’t use super() or perhaps the class doesn’t inherit from the root class). This situation is easily remedied by creating an adapter class that plays by the rules.

For example, the following Moveable class does not make super() calls, and it has an __init__() signature that is incompatible with object.__init__(), and it does not inherit from Root:
...
If we want to use this class with our cooperatively designed ColoredShape hierarchy, we need to make an adapter with the requisite super() calls:


Python 3 syntax:
# ------- Getting the argument signatures to match --------------

class Shape:
    def __init__(self, *, shapename, **kwds):
        self.shapename = shapename
        super().__init__(**kwds)

class ColoredShape(Shape):
    def __init__(self, *, color, **kwds):
        self.color = color
        super().__init__(**kwds)

cs = ColoredShape(color='red', shapename='circle')

# -------- Making sure a root exists ----------------------------

class Root:
    def draw(self):
        # the delegation chain stops here
        assert not hasattr(super(), 'draw')

class Shape(Root):
    def __init__(self, *, shapename, **kwds):
        self.shapename = shapename
        super().__init__(**kwds)
    def draw(self):
        print('Drawing.  Setting shape to:', self.shapename)
        super().draw()

class ColoredShape(Shape):
    def __init__(self, *, color, **kwds):
        self.color = color
        super().__init__(**kwds)
    def draw(self):
        print('Drawing.  Setting color to:', self.color)
        super().draw()

ColoredShape(color='blue', shapename='square').draw()
print('-' * 20)

# ------- Show how to incorporate a non-cooperative class --------

class Moveable:
    # non-cooperative class that doesn't use super()
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def draw(self):
        print('Drawing at position:', self.x, self.y)

class MoveableAdapter(Root):
    # make a cooperative adapter class for Moveable
    def __init__(self, *, x, y, **kwds):
        self.moveable = Moveable(x, y)
        super().__init__(**kwds)
    def draw(self):
        self.moveable.draw()
        super().draw()

class MovableColoredShape(ColoredShape, MoveableAdapter):
    pass

MovableColoredShape(color='red', shapename='triangle', x=10, y=20).draw()

http://code.activestate.com/recipes/577720-how-to-use-super-effectively/

Python 2 syntax:

# ------- Getting the argument signatures to match --------------

class Shape(object):
    def __init__(self, shapename, **kwds):
        self.shapename = shapename
        super(Shape, self).__init__(**kwds)

class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color
        super(ColoredShape, self).__init__(**kwds)

cs = ColoredShape(color='red', shapename='circle')

# -------- Making sure a root exists ----------------------------

class Root(object):
    def draw(self):
        # the delegation chain stops here
        assert not hasattr(super(Root, self), 'draw')

class Shape(Root):
    def __init__(self, shapename, **kwds):
        self.shapename = shapename
        super(Shape, self).__init__(**kwds)
    def draw(self):
        print 'Drawing.  Setting shape to:', self.shapename
        super(Shape, self).draw()

class ColoredShape(Shape):
    def __init__(self, color, **kwds):
        self.color = color
        super(ColoredShape, self).__init__(**kwds)
    def draw(self):
        print 'Drawing.  Setting color to:', self.color
        super(ColoredShape, self).draw()

ColoredShape(color='blue', shapename='square').draw()
print '-' * 20

# ------- Show how to incorporate a non-cooperative class --------

class Moveable(object):
    # non-cooperative class that doesn't use super()
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def draw(self):
        print 'Drawing at position:', self.x, self.y

class MoveableAdapter(Root):
    # make a cooperative adapter class for Moveable
    def __init__(self, x, y, **kwds):
        self.moveable = Moveable(x, y)
        super(MoveableAdapter, self).__init__(**kwds)
    def draw(self):
        self.moveable.draw()
        super(MoveableAdapter, self).draw()

class MovableColoredShape(ColoredShape, MoveableAdapter):
    pass

MovableColoredShape(color='red', shapename='triangle', x=10, y=20).draw()
https://rhettinger.wordpress.com/2011/05/26/super-considered-super/

Example 3
This example shows use of (cooperative) multiple inheritance to provide mixin. See separate article on what is mixin https://www.toalexsmail.com/2019/03/mixin-diamond-problem-inheritance.html.

Based on https://en.wikipedia.org/wiki/Mixin#In_Python

In Python mixin implemented by using of inheritance.

In Python, the SocketServer module has both a UDPServer class and a TCPServer class. They act as servers for UDP and TCP socket servers, respectively. Additionally, there are two mixin classes: ForkingMixIn and ThreadingMixIn. Normally, all new connections are handled within the same process. By extending TCPServer with the ThreadingMixIn as follows:

class ThreadingTCPServer(ThreadingMixIn, TCPServer):
  pass

the ThreadingMixIn class adds functionality to the TCP server such that each new connection creates a new thread. Alternatively, using the ForkingMixIn would cause the process to be forked for each new connection. Clearly, the functionality to create a new thread or fork a process is not terribly useful as a stand-alone class.

In this usage example, the mixins provide alternative underlying functionality without affecting the functionality as a socket server.



No comments:

Post a Comment