Inheritance

Inheritance is a form of code reusability and a fundamental pillar of object oriented programming, along with encapsulation and polymorphism.

It allows you to extend an existing class by adding new or modifying existing attributes, especially if you do not have access to the class itself.

The class you are extending is called the superclass. The class you are creating the subclass.

Let's demonstrate this with an example of playing cards.

Card class

We will start with the Card class to represent a playing card.

A card belongs to a suit, which can be one of Spades, Hearts, Diamonds, Clubs. This order of decreasing suite is useful for games like brdige.

A card also has a rank, which is Ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, King. In some games the Ace may be lower than 2 or higher than the King.

The most flexible way to represent the rank and suit is with an integer base encoding, i.e. a mapping from an integer to its rank or suit.

The initializer initializes the object to the 2 of clubs.

Class Attributes

In order to print a card in a traditional way, e.g. '2 of Clubs', we need another mapping to the string representations of the rank and suit.

We use class attributes for that as shown in lines: 11-12.

A class attribute belongs to the class, not the instance and thus is accessible even before an instance is created. Use the class name as the binding object, as in Card.ranks[0].

Although each instance has its own copy of a rank and a suit, all objects share the same ranks and suits mappings. In other words, only one copy of these lists exist in the class for all objects to use.

In order to make the mapping a bit more natural, we add None as the first element at index 0, thus making 2 map to index 2, 3 to index 3 and so on.

Relational operators

You were introduced to the idea of operator overloading in our previous discussion, and now we are going to continue our exploration with relational operators.

Each of the relational operators <, <=, >, >=, ==, != has an equivalent magic method, i.e. __lt__, __le__, ....

line 22-23: defines one such magic method for the < operator. The choices of order was chosen as suit followed by rank.

Deck class

Now let's see how to set up a deck of cards.

It seems reasonable enough to consider a deck as a list of 52 cards, i.e. a deck has 52 cards.

Initializer

def __init__(self):
    # The list attribute holding the cards.
    self.cards = []

    # Populate the deck by suit and rank.
    for suit in range(4):
        for rank in range(1, 14):
            self.cards.append(Card(rank, suit))

line 4-11: define the initializer for the class.

It defines an instance variable cards as a list that holds the card descriptions, e.g. '2 of Clubs'.

A for statement iterates through all four suits, and for each suit through each rank to build each of the 52 cards.

Descriptor

def __str__(self):
    # The card descriptions.
    l = []

    for card in self.cards:
        l.append(str(card))

    # Return a string with each card on a separate line.
    return '\n'.join(l)

line 13-21: define the descriptor for the class.

It builds and return a string of all card descriptions.

Each card description is added to a list first, and then each element is joined with the newline \n character to produce a listing of each card on a separate line, which is returned.

Pop a card

def popCard(self):
    # Return the last element in the list.
    return self.cards.pop()

line 23-25: define the method to deal out a card.

It returns the last element of the cards list.

Add a card

def addCard(self, card):
    # Append the new card to the end of the list.
    self.cards.append(card)

line 27-29: define the method that adds a card to the deck. It adds the card to the end of the cards list.

Shuffle the deck

def shuffle(self):
    # Shuffle the cards.
    random.shuffle(self.cards)

line 33-35: define a method that shuffles the deck of cards using the shuffle() function of the random module.

Deal a hand

def dealCards(self, hand, count):
    # Deal a hand of count cards.
    for _ in range(count):
        hand.addCard(self.popCard())

line 37-40: define a method to deal a hand out.

In any card game one must deal a number of cards out to each player or hand. The idea of a hand will be the focus of our inheritance topic below.

Inheritance

Recall that inheritance allows us to extend an existing class by subclassing it and making modifications to it.

To frame the discussion of inheritance let's look at the idea of a hand.

A hand is functionally very similar to a deck; both have a list of cards that can be shuffled, add to or removed from. So, it makes sense to use our Deck class to subclass our Hand class.

We can say that a Hand is-a Deck or better yet a more restrictive type of deck. A subclass is always a more specific version of the more general superclass.

Furthermore, a hand is different from a deck in that we can compare two hands if playing poker let's say. This similar but different juxtaposition is ideal for inheritance.

Let's take a look then:

Subclass

First thing to note is the way in which the superclass Deck is given in parentheses after the name of the subclass Hand.

This says that Hand is inheriting from Deck and has access to all attributes exposed by the superclass. This is the reusability feature of inheritance.

Initializer

line 4-6: define the initializer for the class.

A list cards will hold the cards in the hand, and a string label a description of the hand where that may be useful in certain games.

Although the __init__() method from the parent class Deck is inherited by the child class Hand, it is not appropriate as is. A hand is not made up of 52 cards, but rather a subset, thus the subclass extends the parent's behavior by overriding the initializer to suit its needs.

Dealing a hand

Let's deal two cards out and see how that works.

Practice problems

Create a separate Python source file (.py) in VSC to complete each exercise.


p1: dealHands.py

Write a Deck method called dealHands() that takes two parameters: the number of hands and the number of cards per hand. It should create the appropriate number of Hand objects, deal the appropriate number of cards per hand, and return a list of Hands.

To test your method, create a deck, shuffle it and deal out 3 hands of 5 cards each. Your output should like something like this:

Dealt hands:

hand0: 2 of Spades, 8 of Diamonds, 5 of Diamonds, 10 of Diamonds, 3 of Hearts
hand1: 5 of Clubs, Ace of Clubs, 2 of Diamonds, 6 of Diamonds, 9 of Diamonds
hand2: 9 of Clubs, Jack of Hearts, King of Clubs, 2 of Hearts, Ace of Diamonds

You may want to add the appropriate special method __str__() to the Hand class to give you the hand description shown in the sample output.

[Classes And Methods] [TOC]