In the programming world, duplicate code is considered evil. We should not have multiple copies of the same, or similar code in different places.
There are many ways to merge similar pieces of code or objects with similar functionality. In this chapter, we'll be covering the most famous object-oriented principle: inheritance. As discussed in Chapter 1, inheritance allows us to create "is a" relationships between two or more classes, abstracting common details into superclasses and storing specific ones in the subclass. In particular, we'll be covering the Python syntax and principles for:
Technically, every class we create uses inheritance. All Python classes are subclasses of the special class named object
. This class provides very little in terms of data and behaviors (those behaviors it does provide are all double-underscore methods intended for internal use only), but it does allow Python to treat all objects in the same way.
If we don't explicitly inherit from a different class, our classes will automatically inherit from object
. However, we can openly state that our class derives from object
using the following syntax:
class MySubClass(object): pass
This is inheritance! This example is, technically, no different from our very first example in Chapter 2, since Python 3 automatically inherits from object
if we don't explicitly provide a different superclass. A superclass, or parent class, is a class that is being inherited from. A subclass is a class that is inheriting from a superclass. In this case, the superclass is object
, and MySubClass
is the subclass. A subclass is also said to be derived from its parent class or that the subclass extends the parent.
As you've probably figured out from the example, inheritance requires a minimal amount of extra syntax over a basic class definition. Simply include the name of the parent class inside a pair of parentheses after the class name, but before the colon terminating the class definition. This is all we have to do to tell Python that the new class should be derived from the given superclass.
How do we apply inheritance in practice? The simplest and most obvious use of inheritance is to add functionality to an existing class. Let's start with a simple contact manager that tracks the name and e-mail address of several people. The contact class is responsible for maintaining a list of all contacts in a class variable, and for initializing the name and address, in this simple class:
class Contact: all_contacts = [] def __init__(self, name, email): self.name = name self.email = email Contact.all_contacts.append(self)
This example introduces us to class variables. The all_contacts
list, because it is part of the class definition, is actually shared by all instances of this class. This means that there is only one
Contact.all_contacts
list, and if we call self.all_contacts
on any one object, it will refer to that single list. The code in the initializer ensures that whenever we create a new contact, the list will automatically have the new object added. Be careful with this syntax, for if you ever set the variable using self.all_contacts
, you will actually be creating a new instance variable on the object; the class variable will still be unchanged and accessible as Contact.all_contacts
.
This is a very simple class that allows us to track a couple pieces of data about our contacts. But what if some of our contacts are also suppliers that we need to order supplies from? We could add an order
method to the Contact
class, but that would allow people to accidentally order things from contacts who are customers or family friends. Instead, let's create a new Supplier
class that acts like a Contact
, but has an additional order
method:
class Supplier(Contact):
def order(self, order):
print("If this were a real system we would send "
"{} order to {}".format(order, self.name))
Now, if we test this class in our trusty interpreter, we see that all contacts, including suppliers, accept a name and e-mail address in their __init__
, but only suppliers have a functional order method:
>>> c = Contact("Some Body", "[email protected]")
>>> s = Supplier("Sup Plier", "[email protected]")
>>> print(c.name, c.email, s.name, s.email)
Some Body [email protected] Sup Plier [email protected]
>>> c.all_contacts
[<__main__.Contact object at 0xb7375ecc>,
<__main__.Supplier object at 0xb7375f8c>]
>>> c.order("Ineed pliers")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Contact' object has no attribute 'order'
>>> s.order("I need pliers")
If this were a real system we would send I need pliers order to
Supplier
>>>
So now our Supplier
class can do everything a Contact
can do (including adding itself to the list of all_contacts)
and all the special things it needs to handle as a supplier. This is the beauty of inheritance.
One of the most interesting uses of this kind of inheritance is adding functionality to built-in classes. In the Contact
class seen earlier, we are adding contacts to a list of all contacts. What if we also wanted to search that list by name? Well, we could add a method on the Contact
class to search it, but it feels like this method actually belongs on the list itself. We can do this using inheritance:
class ContactList(list): def search(self, name): '''Return all contacts that contain the search value in their name.''' matching_contacts = [] for contact in self: if name in contact.name: matching_contacts.append(contact) return matching_contacts class Contact: all_contacts = ContactList() def __init__(self, name, email): self.name = name self.email = email self.all_contacts.append(self)
Instead of instantiating a normal list as our class variable, we create a new ContactList
class that extends the built-in list
. Then we instantiate this subclass as our all_contacts
list. We can test the new search functionality as follows:
>>> c1 = Contact("John A", "[email protected]")
>>> c2 = Contact("John B", "[email protected]")
>>> c3 = Contact("Jenna C", "[email protected]")
>>> [c.name for c in Contact.all_contacts.search('John')]
['John A', 'John B']
>>>
Are you wondering how we changed the built-in syntax []
into something we can inherit from? Creating an empty list with []
is actually a shorthand for creating an empty list using list()
; the two syntaxes are identical:
>>> [] == list()
True
So, the list
data type is like a class that we can extend, not unlike object
.
As a second example, we can extend the dict
class, which is the long way of creating a dictionary (the {:}
syntax).
class LongNameDict(dict): def longest_key(self): longest = None for key in self: if not longest or len(key) > len(longest): longest = key return longest
This is easy to test in the interactive interpreter:
>>> longkeys = LongNameDict()
>>> longkeys['hello'] = 1
>>> longkeys['longest yet'] = 5
>>> longkeys['hello2'] = 'world'
>>> longkeys.longest_key()
'longest yet'
Most built-in types can be similarly extended. Commonly extended built-ins are object, list, set, dict, file
, and str
. Numerical types such as int
and float
are also occasionally inherited from.
So inheritance is great for adding new behavior to existing classes, but what about changing behavior? Our contact
class allows only a name and an e-mail address. This may be sufficient for most contacts, but what if we want to add a phone number for our close friends?
As we saw in Chapter 2, we can do this easily by just setting a phone attribute on the contact after it is constructed. But if we want to make this third variable available on initialization, we have to override
__init__
. Overriding is altering or replacing a method of the superclass with a new method (with the same name) in the subclass. No special syntax is needed to do this; the subclass's newly created method is automatically called instead of the superclass's method. For example:
class Friend(Contact): def __init__(self, name, email, phone): self.name = name self.email = email self.phone = phone
Any method can be overridden, not just __init__
. Before we go on, however, we need to correct some problems in this example. Our Contact
and Friend
classes have duplicate code to set up the name
and email
properties; this can make maintenance complicated, as we have to update the code in two or more places. More alarmingly, our Friend
class is neglecting to add itself to the all_contacts
list we have created on the Contact
class.
What we really need is a way to call code on the parent class. This is what the super
function does; it returns the object as an instance of the parent class, allowing us to call the parent method directly:
class Friend(Contact):
def __init__(self, name, email, phone):
super().__init__(name, email)
self.phone = phone
This example first gets the instance of the parent object using super
, and calls __init__
on that object, passing in the expected arguments. It then does its own initialization, namely setting the phone
attribute.
A super()
call can be made inside any method, not just __init__
. This means all methods can be modified via overriding and calls to super
. The call to super can also be made at any point in the method; we don't have to make the call as the first line in the method. For example, we may need to manipulate the incoming parameters before forwarding them to the superclass.
18.191.233.43