Subclassing and Inheritance

How to put the pieces together to build a complex system without repeating code.

Inheritance

In object-oriented programming (OOP), inheritance is a way to reuse the code of existing objects, or to establish a subtype from an existing object.

Objects are defined by classes. Classes can inherit attributes and behavior from pre-existing classes called base classes or super classes.

The resulting classes are known as derived classes or subclasses.

(http://en.wikipedia.org/wiki/Inheritance_%28object-oriented_programming%29)

Subclassing

A subclass “inherits” all the attributes (methods, etc) of the parent class. This means that a subclass will have everything that its “parents” have.

You can then change (“override”) some or all of the attributes to change the behavior. You can also add new attributes to extend the behavior.

You create a subclass by passing the superclass to the class statement.

The simplest subclass in Python:

class A_subclass(The_superclass):
    pass

A_subclass now has exactly the same behavior as The_superclass – all the same attributes and methods.

Overriding attributes

Overriding is as simple as creating a new attribute with the same name:

In [1]:     class Circle:
   ...:         color = "red"
   ...:

We now have a class with a class attribute, color, with the value: “red”. All instances of Circle will be red:

In [2]: c = Circle()

In [3]: c.color
Out[3]: 'red'

If we create a subclass of Circle, and set that same class attribute:

In [4]:     class NewCircle(Circle):
   ...:         color = "blue"
   ...:

In [5]: nc = NewCircle()

In [6]: nc.color
Out[6]: 'blue'

We now have a class that is all the same, except that its instances have the color blue.

Note that any methods that refer to that attribute, will get the new value, even if the methods themselves have not changed:

In [10]: class Circle:
    ...:     color = "red"
    ...:
    ...:     def describe(self):
    ...:         return f"I am a {self.color} circle"
    ...:

In [11]: class NewCircle(Circle):
    ...:     color = "blue"
    ...:

In [12]: c = Circle()

In [13]: c.describe()
Out[13]: 'I am a red circle'

In [14]: nc = NewCircle()

In [15]: nc.describe()
Out[15]: 'I am a blue circle'

Note that this is why self is passed in to every method – when you write the method, you don’t know exactly what class self will be – it is an instance of the class at the time the method is called.

Overriding methods

Overriding methods is exactly the same thing, but with methods (remember, a method is an attribute in Python – one that happens to be a function)

class Circle:
...
    def grow(self, factor=2):
        """grows the circle's diameter by factor"""
        self.diameter = self.diameter * factor
...

class NewCircle(Circle):
...
    def grow(self, factor=2):
        """grows the area by factor..."""
        self.diameter = self.diameter * math.sqrt(2)

all the instances of the new class will have the new method – similar, but different, behavior. Note that both these methods are requiring that the class instance has a diameter attribute.

Here’s a program design suggestion:

Whenever you override a method, the interface of the new method should be the same as the old. It should take the same parameters, return the same type, and obey the same preconditions and postconditions.

If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand. If you violate this rule, your code will collapse like (sorry) a house of cards.

– from Think Python

Overriding __init__

__init__ is a common method to override.

You often need to call the super class __init__ as well, so that any initialization required is performed:

class Circle:
    color = "red"
    def __init__(self, diameter):
        self.diameter = diameter
...
class CircleR(Circle):
    def __init__(self, radius):
        diameter = radius*2
        Circle.__init__(self, diameter)

Exception to: “don’t change the method signature” rule.

Often when you override __init__, the new class may take an extra parameter or two. In this case, you will want to keep the signature as similar as possible, and cleanly define what is part of the subclass. A common idiom in this case is this:

class A_Subclass(A_Superclass):

    def __init__(self, param1, param2, *args, **kwargs):
        self.param1 = param1
        self.init_something(param2)
        super().__init__(*args, **kwargs)

That is:

  • Put the extra parameters in the beginning of the list – usually as required positional parameters.

  • Accept *args and **kwargs

  • Pass everything else on to the superclass’ __init__

Using *args and **kwargs is a way to make it clear that the rest is simply the signature of the superclass. It is also flexible if the superclass (or others up in the hierarchy) changes – it could completely change its signature, and this subclass would still work.

Using the superclass’ methods

In a subclass, you can access everything in the superclass: all attributes and other methods:

class Circle:
...
    def get_area(self, diameter):
        return math.pi * (diameter/2.0)**2


class CircleR2(Circle):
...
    def get_area(self):
        return Circle.get_area(self, self.radius*2)

Note that there is nothing special about __init__ except that it gets called automatically when you instantiate an instance. Otherwise, it is the same as any other method – it gets self as the first argument, it can or can not call the superclass’ methods, etc.

“Favor Object Composition Over Class Inheritance”

That is a quotation from the “Design Patterns” book – one of the gospels of OO programming.

But what does it mean?

There are essentially two ways to add multiple functionalities to a class:

Subclassing

and

Composition

As we have just learned about subclassing, you might be tempted to do it a lot. But you need to be careful of over-using subclassing:

https://en.wikipedia.org/wiki/Composition_over_inheritance

Composition is when your classes have attributes of various types that they use to gain functionality – “delegate” functionality to – “Delegation” is a related concept in OO.

“Is a” vs “Has a”

Thinking about “is a” vs “has a” can help you sort this out.

For example, you may have a class that needs to accumulate an arbitrary number of objects.

A list can do that – so maybe you should subclass list?

To help decide – Ask yourself:

Is your class a list (with some extra functionality)?

or

– Does you class have a list?

You only want to subclass list if your class could be used anywhere a list can be used. In fact this is a really good way to think about subclassing in general – subclasses should be specialized versions of the superclass. “Kind of” the same, but with a little different functionality.

Attribute Resolution Order

Once there is a potentially large hierarchy of subclasses, how do you know which one will be used?

When you access an attribute:

an_instance.something

Python looks for it in this order:

  • Is it an instance attribute ?

  • Is it a class attribute ?

  • Is it a superclass attribute ?

  • Is it a super-superclass attribute ?

It can get more complicated, particularly when there are multiple superclasses (multiple inheritance), but when there is a simple inheritance structure (the usual case) – it’s fairly straightforward.

This is often referred to as “method resolution order” (MRO), because it’s more complicated with methods, and in some languages, methods and attributes are more distinct than in Python. In Python, it can be thought of as “name resolution” – everything in Python is about names and namespaces.

If you want to know more of the gory details – here’s some reading:

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

http://python-history.blogspot.com/2010/06/method-resolution-order.html

What are Python classes, really?

Putting aside the OO theory…

Python classes feature:

  • Namespaces

    • One for the class object

    • One for each instance

  • Attribute resolution order – how do you find an attribute.

  • Auto tacking-on of self when methods are called

  • automatically calling __init__ when the class object is called.

That’s about it – really!

(Well, not really, there is more fancy stuff going on under the hood – but this basic structure will get you far).

Type-Based Dispatch

Occasionally you’ll see code that looks like this:

if isinstance(other, A_Class):
    Do_something_with_other
else:
    Do_something_else

When it’s called for, Python provides these utilities:

  • isinstance()

  • issubclass()

But it is very rarely called for! Between Duck Typing, polymorphism, and EAFP, you rarely need to check for type directly.

Wrap Up

Thinking OO in Python:

Think about what makes sense for your code:

  • Code re-use

  • Clean APIs

  • Separation of Concerns

OO can be a very powerful approach, but don’t be a slave to what OO is supposed to look like.

Let OO work for you, not create work for you.

And the biggest way to do that is to support code re-use.