Support for Cooperative Multi-inheritance¶
Note
This section is not about sub-classing from more that one Qt class.
Cooperative multi-inheritance is a technique for implementing classes that inherit multiple super-classes - typically a main super-class and one or more mixin classes that add additional behaviour. It makes it easy to add new mixins at a later date to further extend the behavior, without needing to change either the implementation of the class or any existing code that creates an instance of the class.
The technique requires that all the super-class’s __init__
methods follow
the same pattern in the way that they handle unrecognised keyword arguments and
use super()
to invoke their own super-class’s __init__
methods.
PyQt5’s classes follow this pattern.
See Raymond Hettinger’s Python’s super() considered super! blog post for some more background on the subject.
As an example, let’s say we have a class that represents a person, and that a person has a name. The following might be an initial implementation:
class Person(QObject):
def __init__(self, name, parent=None)
QObject.__init__(self, parent)
self.name = name
An instance would normally be created in one of the following ways:
person = Person("Joe")
person = Person("Joe", some_parent)
This approach has some limitations:
- Only a sub-set of the
QObject
API is exposed. For example you cannot set the value of a Qt property or connect a signal by passing appropriate keyword arguments toPerson.__init__
. - Adding another class to
Person
’s list of super-classes means that its__init__
implementation needs to be changed. If the new mixin takes non-optional arguments then every call to create aPerson
instance will need changing.
Consider this alternative implementation:
class Person(QObject):
def __init__(self, name, **kwds):
super().__init__(**kwds)
self.name = name
The difference is that we only handle arguments that are used by the Person
class itself and we punt all the other arguments to the super-classes by
calling super()
.
With this implementation an instance would normally be created in one of the following ways:
person = Person("Joe")
person = Person("Joe", parent=some_parent)
Here the difference is that we are using keyword arguments to specify any
arguments that are not handled by the Person
class itself. Note that we
could use keyword arguments for all arguments - whether or not you do so is
down to personal choice.
The limitations of the first implementation no longer apply. For example, without any further changes we can also do this:
person = Person("Joe", destroyed=some_callable)
Let’s say we now want to extend the behaviour of the Person
class by adding
a mixin that handles a person’s age. The implementation of the mixin would be
as follows:
class Age(object):
def __init__(self, age=0, **kwds):
super().__init__(**kwds)
self.age = age
This follows a similar pattern to our Person
implementation, but notice
that we have provided the age
argument with a default value.
The following is our new Person
implementation:
class Person(QObject, Age):
def __init__(self, name, **kwds):
super().__init__(**kwds)
self.name = name
The only change we have had to make is to add Age
to Person
’s list of
super-classes. More importantly we do not need to change any call to create a
Person
instance.
If we do want to create a Person
instance with a non-default age then we
simply pass it as a keyword argument as follows:
person = Person("Joe", age=38)
This technique increases the use of keyword arguments - while this means a bit more typing, it significantly increases the readability of application code.