Inheritance

Basic Inheritance

Python classes support inheritance. Inheritance in computer science is an “is-a” relationship. For example, a dog is an animal. If we defined a class Animal, then we could also define a subclass Dog, that inherits the data and methods of its parent class, Animal. The parent class is also often referred to as the superclass.

To use inheritance, we can make a subclass. Let’s make a NamedAccount class which subclasses our BankAccount class:

class NamedAccount(BankAccount):

    """Bank account with an account name."""

    def __init__(self, *args, **kwargs):
        """Open a named bank account."""
        self.name = kwargs.pop('name', None)
        super().__init__(*args, **kwargs)

    def print_balance(self):
        """Print the account name and current account balance."""
        print(f'Account "{self.name}" balance is ${self.balance}')

    def __str__(self):
        """Return account name and balance."""
        return f'Account "{self.name}" with balance of ${self.balance}'

    def __repr__(self):
        """Return developer-readable representation of our named account."""
        return f'NamedAccount(balance={self.balance}, name={repr(self.name)})'

The special super function allows us to call methods from parent classes. In our __init__ method we popped the argument name from the keyword argument dictionary, because that is the only one we are using in our method. Then the other arguments are passed on to the __init__ method of the parent class.

Right now there is only one other argument for the BankAccount class. Catching *args and **kwargs and passing them on to the super class ensures that nothing will break if the BankAccount class changes the arguments it accepts.

Now let’s confirm that our class methods work as expected:

>>> from bank_account import NamedAccount
>>> nameless_account = NamedAccount()
Account opened.
Account "None" balance is $0
>>> nameless_account
NamedAccount(balance=0, name=None)
>>> nameless_account.name
>>> nameless_account.name is None
True
>>> trey_account = NamedAccount(name="Trey")
Account opened.
Account "Trey" balance is $0
>>> trey_account.name
'Trey'
>>> trey_account
NamedAccount(balance=0, name='Trey')

Because NamedAccount is a subclass of BankAccount, we can also use the methods of BankAccount on the NamedAccount objects:

>>> trey_account.deposit(45)
$45 deposited.
Account "Trey" balance is $45
>>> trey_account.deposit(145)
$145 deposited.
Account "Trey" balance is $190
>>> trey_account.withdraw(25)
$25 withdrawn.
Account "Trey" balance is $165

Note that because the methods were implemented using the print_balance method, we get the account name printed when the balance is printed after the transaction is completed. This is because the subclass definition of a method overrides the parent class’s definition. When Python sees a method call on an object, it looks for the method on the object first and only checks base classes afterward.

Multiple Inheritance

Python also allows for multiple inheritance. This means that a class can have multiple base classes. This feature is somewhat advanced and understanding how super works in this scenario is a little tricky.

We’ll look at an example by re-implementing our NamedAccount with a NamedObjectMixin and a BankAccount:

class NamedObjectMixin(object):
    def __init__(self, *args, **kwargs):
        """Open a named bank account."""
        self.name = kwargs.pop('name', None)
        super().__init__(*args, **kwargs)


class NamedAccount(NamedObjectMixin, BankAccount):

    """Bank account with an account name."""

    def print_balance(self):
        """Print the account name and current account balance."""
        print(f'Account "{self.name}" balance is ${self.balance}')

    def __str__(self):
        """Return account name and balance."""
        return f'Account "{self.name}" with balance of ${self.balance}'

    def __repr__(self):
        """Return developer-readable representation of our named account."""
        return f'NamedAccount(balance={self.balance}, name={name=repr(self.name})'

Inheritance Exercises

Minimum Balance

This is the MinimumBalanceAccount exercise in classes.py.

Create a class MinimumBalanceAccount which subclasses BankAccount. This new class should raise an exception whenever the user attempts to withdraw so much money that their account goes below 0.

>>> from bank_account import MinimumBalanceAccount
>>> my_account = MinimumBalanceAccount()
Account opened.
Account balance is $0.
>>> my_account.deposit(100)
$100 deposited.
Account balance is $100.
>>> my_account.withdraw(200)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "bank_account.py", line 45, in withdraw
    raise ValueError("Balance cannot be less than $0")
ValueError: Balance cannot be less than $0

Alphabetic String

This is the AlphaString exercise in classes.py.

Make a custom string object AlphaString which only allows strings consisting entirely of alphabetic characters (A-Z).

Hint

Inherit from collections.UserString to implement this class.

Doubly-Linked Node

This is the DoublyLinkedNode exercise in classes.py.

The Node class represents a node with multiple child nodes attached.

Printing a node displays the “ancestry” of the node, starting with the root or base node, separated by / between the nodes, and ending with the node requested.

Example of creating a root note and child nodes:

>>> root = Node('A')
>>> child1 = root.make_child('1')
>>> grandchild1 = child1.make_child('a')
>>> grandchild2 = child1.make_child('b')
>>> child2 = root.make_child('2')

Examples of the string representations of these nodes:

>>> print(child1)
A / 1
>>> print(grandchild1)
A / 1 / a
>>> child1.name = '9'
>>> print(grandchild2)
A / 9 / b
>>> print(child2)
A / 2
>>> grandchild1.name
'a'

More examples:

>>> print(Node("Universe")
          .make_child("Milky Way")
          .make_child("Solar System")
          .make_child("Earth")
... )
Universe / Milky Way / Solar System / Earth

>>> red_panda = (
...     Node("Animalia")
...     .make_child("Chordata")
...     .make_child("Mammalia")
...     .make_child("Carnivora")
...     .make_child("Ailuridae")
...     .make_child("Ailurus")
...     .make_child("A. fulgens")
... )
>>> print(red_panda)
Animalia / Chordata / Mammalia / Carnivora / Ailuridae / Ailurus / A. fulgens

I’d like you to make a DoublyLinkedNode class that extends the Node class. The method leaves returns all the children of the DoublyLinkedNode, or if there are no children, it returns a list containing only itself.

>>> from classes import DoublyLinkedNode
>>> root = DoublyLinkedNode('A')
>>> child1 = root.make_child('1')
>>> grandchild1 = child1.make_child('a')
>>> grandchild2 = child1.make_child('b')
>>> child2 = root.make_child('2')
>>> [node.name for node in root.leaves()]
['a', 'b', '2']
>>> [node.name for node in child1.leaves()]
['a', 'b']
>>> [node.name for node in child2.leaves()]
['2']

Last Updated Dictionary

This is the LastUpdatedDictionary exercise in classes.py.

Make a class LastUpdatedDictionary that maintains its items in last-updated order.

Normally dictionaries maintain their items in insertion order. When a key is inserted, it’s added to the end, but when a key is updated nothing is moved or rearranged.

Unlike a typical dictionary, LastUpdatedDictionary objects should move keys to the end of the dictionary whenever they’re updated.

Example:

>>> d = LastUpdatedDictionary({'a': 1, 'b': 2, 'c': 3})
>>> d
LastUpdatedDictionary([('a', 1), ('b', 2), ('c', 3)])
>>> d['b'] = 1
>>> d
LastUpdatedDictionary([('a', 1), ('c', 3), ('b', 1)])

Hint

Consider inheriting from the collections.OrderedDict class which maintains its values in insertion order.

OrderedCounter

This is the OrderedCounter exercise in classes.py.

Make a class OrderedCounter that acts like collections.Counter except that it maintains its items in last-updated order. You can think of this as a mashup between collections.Counter and LastUpdatedDictionary.

Example:

>>> d = OrderedCounter('hello there')
>>> d
OrderedCounter([('a', 1), ('b', 2), ('c', 3)])
>>> d['b'] = 1
>>> d
OrderedCounter([('a', 1), ('c', 3), ('b', 1)])

Hint

Consider inheriting from the collections.OrderedDict class which maintains its values in insertion order.