Multiple inheritance is a touchy subject. In principle, it's simple: a subclass that inherits from more than one parent class is able to access functionality from both of them. In practice, this is less useful than it sounds and many expert programmers recommend against using it.
The simplest and most useful form of multiple inheritance is called a mixin. A mixin is a superclass that is not intended to exist on its own, but is meant to be inherited by some other class to provide extra functionality. For example, let's say we wanted to add functionality to our Contact class that allows sending an email to self.email. Sending email is a common task that we might want to use on many other classes. So, we can write a simple mixin class to do the emailing for us:
class MailSender: def send_mail(self, message): print("Sending mail to " + self.email) # Add e-mail logic here
For brevity, we won't include the actual email logic here; if you're interested in studying how it's done, see the smtplib module in the Python standard library.
This class doesn't do anything special (in fact, it can barely function as a standalone class), but it does allow us to define a new class that describes both a Contact and a MailSender, using multiple inheritance:
class EmailableContact(Contact, MailSender): pass
The syntax for multiple inheritance looks like a parameter list in the class definition. Instead of including one base class inside the parentheses, we include two (or more), separated by a comma. We can test this new hybrid to see the mixin at work:
>>> e = EmailableContact("John Smith", "[email protected]") >>> Contact.all_contacts [<__main__.EmailableContact object at 0xb7205fac>] >>> e.send_mail("Hello, test e-mail here") Sending mail to [email protected]
The Contact initializer is still adding the new contact to the all_contacts list, and the mixin is able to send mail to self.email, so we know that everything is working.
This wasn't so hard, and you're probably wondering what the dire warnings about multiple inheritance are. We'll get into the complexities in a minute, but let's consider some other options we had for this example, rather than using a mixin:
- We could have used single inheritance and added the send_mail function to the subclass. The disadvantage here is that the email functionality then has to be duplicated for any other classes that need an email.
- We can create a standalone Python function for sending an email, and just call that function with the correct email address supplied as a parameter when the email needs to be sent (this would be my choice).
- We could have explored a few ways of using composition instead of inheritance. For example, EmailableContact could have a MailSender object as a property instead of inheriting from it.
- We could monkey patch (we'll briefly cover monkey patching in Chapter 7, Python Object-Oriented Shortcuts) the Contact class to have a send_mail method after the class has been created. This is done by defining a function that accepts the self argument, and setting it as an attribute on an existing class.
Multiple inheritance works all right when mixing methods from different classes, but it gets very messy when we have to call methods on the superclass. There are multiple superclasses. How do we know which one to call? How do we know what order to call them in?
Let's explore these questions by adding a home address to our Friend class. There are a few approaches we might take. An address is a collection of strings representing the street, city, country, and other related details of the contact. We could pass each of these strings as a parameter into the Friend class's __init__ method. We could also store these strings in a tuple, dictionary, or dataclass (we'll discuss dataclasses in Chapter 6, Python Data Structures) and pass them into __init__ as a single argument. This is probably the best course of action if there are no methods that need to be added to the address.
Another option would be to create a new Address class to hold those strings together, and then pass an instance of this class into the __init__ method in our Friend class. The advantage of this solution is that we can add behavior (say, a method to give directions or to print a map) to the data instead of just storing it statically. This is an example of composition, as we discussed in Chapter 1, Object-Oriented Design. The has a relationship of composition is a perfectly viable solution to this problem and allows us to reuse Address classes in other entities, such as buildings, businesses, or organizations.
However, inheritance is also a viable solution, and that's what we want to explore. Let's add a new class that holds an address. We'll call this new class AddressHolder instead of Address because inheritance defines an is a relationship. It is not correct to say a Friend class is an Address class, but since a friend can have an Address class, we can argue that a Friend class is an AddressHolder class. Later, we could create other entities (companies, buildings) that also hold addresses. Then again, such convoluted naming is a decent indication we should be sticking with composition, rather than inheritance. But for pedagogical purposes, we'll stick with inheritance. Here's our AddressHolder class:
class AddressHolder: def __init__(self, street, city, state, code): self.street = street self.city = city self.state = state self.code = code
We just take all the data and toss it into instance variables upon initialization.