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.
class Card:
'''Represents a standard playing card.
Attributes:\n
rank: int value [0, 13] for [None, A, 2, ..., K]
suit: int value [0, 3] for [C, D, H, S]
'''
# Class attributes for mapping rand and suit
# to string representations.abs
ranks = [None, 'Ace', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen', 'King']
suits = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
def __init__(self, rank = 2, suit = 0):
self.rank = rank
self.suit = suit
def __str__(self):
return '{} of {}'.format(Card.ranks[self.rank], Card.suits[self.suit])
def __lt__(self, other):
return (self.suit, self.rank) < (other.suit, other.rank)
The initializer initializes the object to the 2 of clubs.
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.
twoClubs = Card(2, 0)
print(twoClubs)
2 of Clubs
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.
twoSpades = Card(2, 3)
print(twoClubs < twoSpades)
True
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.
import random
class Deck:
'''Represents a deck of 52 playing cards.'''
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))
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)
def popCard(self):
# Return the last element in the list.
return self.cards.pop()
def addCard(self, card):
# Append the new card to the end of the list.
self.cards.append(card)
def shuffle(self):
# Shuffle the cards.
random.shuffle(self.cards)
def dealCards(self, hand, count):
# Deal a hand of count cards.
for _ in range(count):
hand.addCard(self.popCard())
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.
deck = Deck()
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.
print(deck)
Ace of Clubs 2 of Clubs 3 of Clubs 4 of Clubs 5 of Clubs 6 of Clubs 7 of Clubs 8 of Clubs 9 of Clubs 10 of Clubs Jack of Clubs Queen of Clubs King of Clubs Ace of Diamonds 2 of Diamonds 3 of Diamonds 4 of Diamonds 5 of Diamonds 6 of Diamonds 7 of Diamonds 8 of Diamonds 9 of Diamonds 10 of Diamonds Jack of Diamonds Queen of Diamonds King of Diamonds Ace of Hearts 2 of Hearts 3 of Hearts 4 of Hearts 5 of Hearts 6 of Hearts 7 of Hearts 8 of Hearts 9 of Hearts 10 of Hearts Jack of Hearts Queen of Hearts King of Hearts Ace of Spades 2 of Spades 3 of Spades 4 of Spades 5 of Spades 6 of Spades 7 of Spades 8 of Spades 9 of Spades 10 of Spades Jack of Spades Queen of Spades King of Spades
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.
dealCard = deck.popCard()
print(dealCard)
King of Spades
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.
deck.addCard(Card(13, 3))
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.
deck.shuffle()
print(deck)
9 of Clubs 7 of Spades 10 of Clubs Jack of Clubs 5 of Spades 7 of Hearts Queen of Clubs 10 of Hearts Jack of Hearts Ace of Spades 8 of Clubs 3 of Spades 7 of Clubs 2 of Spades Ace of Clubs 6 of Diamonds 10 of Diamonds King of Clubs 3 of Hearts 9 of Hearts 6 of Spades 10 of Spades 4 of Clubs 5 of Hearts 8 of Hearts Ace of Hearts Queen of Hearts 5 of Clubs King of Hearts 6 of Hearts 7 of Diamonds 4 of Diamonds 3 of Clubs King of Spades 5 of Diamonds 4 of Spades Jack of Spades 9 of Spades 6 of Clubs Ace of Diamonds Queen of Diamonds 2 of Hearts 8 of Spades 2 of Clubs Queen of Spades 9 of Diamonds 8 of Diamonds Jack of Diamonds 4 of Hearts King of Diamonds 2 of Diamonds 3 of Diamonds
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.
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:
class Hand(Deck):
'''Represents a hand of playing cards.'''
def __init__(self, label = ''):
self.cards = []
self.label = label
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.
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.
player1 = Hand('player 1')
print(player1.label, player1.cards, sep = ': ')
player 1: []
Let's deal two cards out and see how that works.
deck.dealCards(player1, 2)
print(player1.label)
print(player1)
player 1 3 of Diamonds 2 of Diamonds
Create a separate Python source file (.py) in VSC to complete each exercise.
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.