Inheritance: For Better of for Worseđź”—
- multiple inheritance is supported in C++, Java but not C#. After percieved abuse in early C++ codebases, Java left it out.
- there is significant backlash against overuse of inheritance, multiple inheritance (worse), because superclasses and subclasses are tightly coupled.
- after all this bad name, its(multiple inheritance) still usefull in many practical scenarios, like in standard library, Django, Tkinter GUI toolkit.
The super() Functionđź”—
- consistent use of the
super()
built-in function is essential for maintainable object-oriented Python programs. - when subclass method overrides a method of a superclass, the overriding method usually needs to call the corresponding method of superclass.
class LastUpdatedOrderedDict(OrderedDict):
"""Store items in the order they were last updated"""
def __setitem__(self, key, value):
super().__setitem__(key, value)
self.move_to_end(key)
- there are few examples where overriding method doesn’t use
super()
but instead call method directly asOrderedDict.__setitem__(self, key, value)
. This is not recommended for reasons- hardcoding base class name
super
implements logic to handle class hierarchies with multiple inheritance.
Subclassing Built-In Types Is Trickyđź”—
- earlier versions of python didn’t support subclassing built-ins (most are implemented in C) usually doesn’t call methods overridden by user-defined classes.
- From official documentation:
- CPython has no rule at all for when exactly overridden method of subclasses of built-in types get implicitly called or not. As an approximation, these methods are never called by other built-in methods of the same object. For example, an overridden
__getitem__()
in a subclass ofdict
will not be called by e.g. the built-inget()
method.
- CPython has no rule at all for when exactly overridden method of subclasses of built-in types get implicitly called or not. As an approximation, these methods are never called by other built-in methods of the same object. For example, an overridden
class DoppelDict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2) # duplicate values when storing
dd = DoppelDict(one = 1) # constructor ignores __setitem__
dd # {'one': 1}
dd['two'] = 2 # [] operator works
dd # {'one': 1, 'two': [2,2]}
dd.update(three = 3) # update doesn't work expected
dd # {'three': 3, 'one' : 1, 'two':[2,2]}
- The built-in behaviour is a violation of a basic rule of OOP: the search for methods should always start from class of the receiver(
self
), even when the call happens inside a method implemented in a superclass. This is what is calledlate binding
which is a key feature of OOP. In any call of formx.method()
the exact method to be called must be determined at runtime, based on class of receiverx
. - The problem is not limited to calls within an instance—whether
self.get()
callsself.__getitem__()
—but also happens with overridden methods of other classes that should be called by the built-in methods.
class AnswerDict(dict):
def __getitem__(self, key): # always return 42
return 42
ad = AnswerDict(a = 'foo') # loaded with (a='foo')
ad['a'] # 42 # expected
d = {}
d.update(ad) # update normal dict d, updated with ad
d['a'] # 'foo' # but our `dict.update` ignored our original AnswerDict method
d # {'a':'foo'}
Subclassing built-in types like dict
or list
or str
directly is error-prone because the built-in methods mostly ignore user-defined overrides. Instead of subclassing the built-ins, derive your classes from the collections
module using UserDict
, UserList
, and UserString
, which are designed to be easily extended.
Multiple Inheritance and Method Resolution Orderđź”—
- Any language which implements multiple inheritance has to deal with naming conflicts when superclass implement a method by the same name, commonly known as diamond problem.
Assume following implementation
class Root:
def ping(self):
print(f'{self}.ping() in Root')
def pong(self):
print(f'{self}.pong() in Root')
def __repr__(self):
cls_name = type(self).__name__
return f'<instance of {cls_name}>'
class A(Root):
def ping(self):
print(f'{self}.ping() in A')
super().ping()
def pong(self):
print(f'{self}.pong() in A')
super().pong()
class B(Root):
def ping(self):
print(f'{self}.ping() in B')
super().ping()
def pong(self):
print(f'{self}.pong() in B')
class Leaf(A, B):
def ping(self):
print(f'{self}.ping() in Leaf')
super().ping()
leaf1 = Leaf()
leaf1.ping()
#
# <instance of Leaf>.ping() in Leaf
# <instance of Leaf>.ping() in A
# <instance of Leaf>.ping() in B
# <instance of Leaf>.ping() in Root
leaf1.pong()
# <instance of Leaf>.pong() in A
# <instance of Leaf>.pong() in B
Lets take a look at MRO of the class to find references to its superclasses
Leaf.__mro__ # doctest:+NORMALIZE_WHITESPACE
(<class 'diamond1.Leaf'>, <class 'diamond1.A'>, <class 'diamond1.B'>,
<class 'diamond1.Root'>, <class 'object'>)
- MRO only determines activation order, but when particular method will be activated in each of the classes depend on whether each implementation calls
super()
or not.
- In
pong
method. TheLeaf
doesn’t override it, therefore activated implementation of next classLeaf.__mro__
: the A class. Since methodA.pong
callssuper().pong()
. TheB
class is next in MRO, thereforeB.pong
is activated but method doesn’t callssuper.pong()
so activation stops. - NOTE: declaration
Leaf(A,B)
andLeaf(B, A)
are separate and determine relative order in MRO. - when a method calls
super()
, it is a cooperative method. Cooperative methods enable cooperative multiple inheritance. These terms are intension in order to work, multiple inheritance in Python requires cooperation of the methods involved. In theB
class,ping
cooperates butpong
doesn’t. - Python is a dynamic language, so the interaction of
super()
with the MRO is also dynamic.
from diamond import A
class U():
def ping(self):
print(f'{self}.ping() in U')
super().ping()
class LeafUA(U, A):
def ping(self):
print(f'{self}.ping() in LeafUA')
super().ping()
u = U()
u.ping()
# AttributeError: 'super' object has no attribute 'ping'
# because notice U doesn't inherit any class except object, which doesn't implement ping
leaf2 = LeafUA() # this works as MRO works fine, last Root class doesn't call super ending MRO traversal
- Classes like U are called as mixin class. Its a class intended to be used together with other cleasses in multiple inheritance, to provide additional functionality.
Mixin Classesđź”—
A mixin class is designed to be subclassed together with at least one other class in a multiple inheritance arrangement.
- A mixin is not supposed to be only base class of a concrete class, because it doesn’t provide all the functionality for a concrete class.
- only add/customizes the behaviour of child or sibling class.
Case-Insesitive Mappingsđź”—
import collections
def _upper(key): # helper function
try:
return key.upper()
except AttributeError:
return key
class UpperCaseMixin: # mixin implement 4 essential methods of mappings, always calling `super()` with the `key` uppercased
def __setitem__(self, key, item):
super().__setitem__(_upper(key), item)
def __getitem__(self, key):
return super().__getitem__(_upper(key))
def get(self, key, default=None):
return super().get(_upper(key), default)
def __contains__(self, key):
return super().__contains__(_upper(key))
# Concrete Classes using above Mixin
class UpperDict(UpperCaseMixin, collections.UserDict): # UserDict
pass
class UpperCounter(UpperCaseMixin, collections.Counter): # Counter
"""Specialized 'Counter' that uppercases string keys"""
d = UpperDict([('a', 'letter A'), (2, 'digit two')])
list(d.keys()) # ['A', 2]
d['b'] = 'letter B'
'b' in d # true
list(d.keys()) # ['A', 2, 'B']
Multiple Inheritance in the Real Worldđź”—
- In Design Patterns book, almost all code is in C++, but the only example of multiple inheritance is the Adapter Pattern, In python, multiple inhertance is not the norm either but there are important examples.
ABCs Are Mixins Toođź”—
- documentation of
collection.abc
uses the term mixin method for the concrete methods implemented in many of the collection ABCs. - The ABCs that provide mixin methods play two roles
- Interface definitions
- Mixin Classes
ThreadingMixIn and ForkingMixInđź”—
The http.server
package provides HTTPServer
and ThreadingHTTPServer
. Latter was added in 3.7+, is identical to HTTPServer
but uses threads to handle requests by using the ThreadingMixIn
. This is useful to handle web browsers pre-opening sockets, on which HTTPServer
would wait indefinitely
Complete Source Code for it is
Django Generic Views Mixinsđź”—
- In Django, a view is a callable object that takes a
request
argument—an object representing an HTTP request—and returns an object representing an HTTP response.
View
is the base class of all views (it could be an ABC), and it provides core functionality like thedispatch
method, which delegates to “handler” methods likeget
,head
,post
, etc., implemented by concrete subclasses to handle the different HTTP verbs. TheRedirectView
class inherits only fromView
, and you can see that it implementsget
,head
,post
, etc.
Multiple Inheritance in Tkinterđź”—
- Extreme example of multiple inheritance is Python’s standard library, Tkinter GUI Toolkit.
- By current standards, the class hierarchy of Tkinter is very deep. Few parts of the Python standard library have more than three or four levels of concrete classes, and the same can be said of the Java class library. However, it is interesting to note that the some of the deepest hierarchies in the Java class library are precisely in the packages related to GUI programming:
Coping with Inheritanceđź”—
What Alan Kay wrote in the epigraph remains true: there’s still no general theory about inheritance that can guide practicing programmers. What we have are rules of thumb, design patterns, “best practices,” clever acronyms, taboos, etc. Some of these provide useful guidelines, but none of them are universally accepted or always applicable.
here are a few tips to avoid spaghetti class graphs.
Favor Object Composition over Class Inheritanceđź”—
- second principle of object-oriented design from the Design Patterns book
- Favoring composition leads to more flexible designs.
- Composition and delegation can replace the use of mixins to make behaviors available to different classes, but cannot replace the use of interface inheritance to define a hierarchy of types.
Understand Why Inheritance is Used in Each Caseđź”—
- When dealing with multiple inheritance, it’s useful to keep straight the reasons why subclassing is done in each particular case. The main reasons are:
- Inheritance of interface creates a subtype, implying an “is-a” relationship. This is best done with ABCs.
- Example: Consider a scenario where you have a base class
Shape
, and you want to create specific shapes likeCircle
andRectangle
. Each shape must implement certain methods likearea()
andperimeter()
. In this case,Shape
can be an abstract base class defining these methods, andCircle
andRectangle
can be concrete subclasses implementing these methods.
- Example: Consider a scenario where you have a base class
- Inheritance of implementation avoids code duplication by reuse. Mixins can help with this.
- Suppose you have multiple classes (
A
,B
,C
) that need logging functionality. Instead of duplicating logging code in each class, you can create a mixin classLogMixin
that contains the logging functionality. Then, you can inherit fromLogMixin
in each class that needs logging capabilities.
- Suppose you have multiple classes (
- Inheritance of interface creates a subtype, implying an “is-a” relationship. This is best done with ABCs.
Make Interface Explicit with ABCsđź”—
In modern Python, if a class is intended to define an interface, it should be an explicit ABC or a typing.Protocol
subclass. An ABC should subclass only abc.ABC
or other ABCs. Multiple inheritance of ABCs is not problematic.
Use Explicit Mixins for Code Reuseđź”—
If a class is designed to provide method implementations for reuse by multiple unrelated subclasses, without implying an “is-a” relationship, it should be an explicit mixin class. Conceptually, a mixin does not define a new type; it merely bundles methods for reuse. A mixin should never be instantiated, and concrete classes should not inherit only from a mixin. Each mixin should provide a single specific behavior, implementing few and very closely related methods. Mixins should avoid keeping any internal state; i.e., a mixin class should not have instance attributes.
- There is no formal way in Python to state that a class is a mixin, so it is highly recommended that they are named with a
Mixin
suffix.
Provide Aggregate Classes to Usersđź”—
A class that is constructed primarily by inheriting from mixins and does not add its own structure or behavior is called an aggregate class.
If some combination of ABCs or mixins is particularly useful to client code, provide a class that brings them together in a sensible way.
Subclass Only Classes Designed for Subclassingđź”—
Subclassing any complex class and overriding its methods is error-prone because the superclass methods may ignore the subclass overrides in unexpected ways. As much as possible, avoid overriding methods, or at least restrain yourself to subclassing classes which are designed to be easily extended, and only in the ways in which they were designed to be extended.
he PEP introduces a @final
decorator that can be applied to classes or individual methods, so that IDEs or type checkers can report misguided attempts to subclass those classes or override those methods.
Avoid Subclassing from Concrete Classesđź”—
Subclassing concrete classes is more dangerous than subclassing ABCs and mixins, because instances of concrete classes usually have internal state that can easily be corrupted when you override methods that depend on that state. Even if your methods cooperate by calling super()
, and the internal state is held in private attributes using the __x
syntax, there are still countless ways a method override can introduce bugs.
Tkinter: The Good, the Bad, and the Uglyđź”—
Most advice in the previous section is not followed by Tkinter, with the notable exception of “Provide Aggregate Classes to Users”.
Keep in mind that Tkinter has been part of the standard library since Python 1.1 was released in 1994. Tkinter is a layer on top of the excellent Tk GUI toolkit of the Tcl language. The Tcl/Tk combo is not originally object-oriented, so the Tk API is basically a vast catalog of functions. However, the toolkit is object-oriented in its design, if not in its original Tcl implementation.
To be fair, as a Tkinter user, you don’t need to know or use multiple inheritance at all. It’s an implementation detail hidden behind the widget classes that you will instantiate or subclass in your own code. But you will suffer the consequences of excessive multiple inheritance when you type dir(tkinter.Button)
and try to find the method you need among the 214 attributes listed. And you’ll need to face the complexity if you decide to implement a new Tk widget.