Prof. Andrea Gallegati
One of the most important features of object-oriented programming is inheritance.
The ability to define a new class that is a modified version of an existing class.
We will use classes representing playing cards, decks of cards, and poker hands!
( If you don’t play poker, you can read about it at http://en.wikipedia.org/wiki/Poker )
Depending on the game, Ace may be higher than King or lower than 2!
To represent a playing card with an object, it is:
One possibility is to use strings containing ranks names.
One problem with this implementation is that it would not be that easy to compare cards (higher rank/suit).
3 |
1 |
As alternative use integers to encode ranks and suits defining a mapping. |
||||||
2 |
0 |
This kind of encoding is not meant to be a secret (“encryption”). |
Thus, it's easy to compare cards. Higher suits map to higher numbers: we can compare suits by comparing their codes.
11 |
||||
The ranks mapping is even more obvious. Each of the numerical ranks maps to the corresponding integer, and for face cards: |
12 |
|||
13 |
The Card
class definition looks like this:
class Card:
"""Represents a standard playing card."""
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
The default card thus, is the 2 of Clubs.
Otherwise, pass the optional parameters for each attribute to the constructor.
queen_of_diamonds = Card(1, 12)
To print Card
objects in an easy way, let's map from integer codes to corresponding ranks and suits.
class Card:
"""Represents a standard playing card."""
suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7',
'8', '9', '10', 'Jack', 'Queen', 'King']
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank],
Card.suit_names[self.suit])
For example, with above lists of strings, assigned to class attributes.
class attributes like suit_names
and rank_names
, are defined:
being associated with the class object Card
itself.
instance attributes like suit
and rank
, contrary are associated with a particular instance.
Every card has its own suit and rank, but there is only one copy of suit_names and rank_names.
Both kinds of attribute are accessed using dot notation.
For Example, Card.rank_names[self.rank]
means:
“Use attribute rank from the object self as an index to select from the list rank_names from the class Card.”
The place-keeper None
for the first element of rank_names
is because there is no card with rank zero.
This way, we get a mapping with the nice property that the index 2
maps to the string 2, and so on.
Otherwise, use a dictionary instead of a list.
Let's create and print a card:
card1 = Card(2, 11)
print(card1)
Jack of Hearts
For built-in types, relational operators (<
, >
, ==
, etc.) compare values.
For programmer-defined types, we can override built-in operators by providing a (“less than”) __lt__
method, for example.
__lt__
takes two parameters and return True
if self
is strictly less than other
.
Correct ordering for cards is not obvious. The answer might depend on the game.
To keep things simple, we’ll make an arbitrary choice: suit is more important.
With that decided, we can write
class Card:
"""Represents a standard playing card."""
suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7',
'8', '9', '10', 'Jack', 'Queen', 'King']
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank],
Card.suit_names[self.suit])
def __lt__(self, other):
# check the suits
if self.suit < other.suit: return True
if self.suit > other.suit: return False
# suits are the same... check ranks
return self.rank < other.rank
... or more concisely (with tuple comparison)
class Card:
"""Represents a standard playing card."""
suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7',
'8', '9', '10', 'Jack', 'Queen', 'King']
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank
def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank],
Card.suit_names[self.suit])
def __lt__(self, other):
t1 = self.suit, self.rank
t2 = other.suit, other.rank
return t1 < t2
Once we have Cards
, let's define Decks
.
A deck is made up of cards: it's natural for each Deck to contain a list of cards as an attribute.
The following is a class definition for Deck. The init method creates the attribute cards and generates the standard set of 52 cards:
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)
This nested loop populates the deck with the standard set of 52 cards:
suits
from 0
to 3
.ranks
from 1
to 13
.Each iteration appends a new Card
to self.cards
.
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)
def __str__(self):
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)
__str__
method print Deck
objects, as a large string, in an efficient way.
It builds a list of strings (invoking __str__
on each card) and then invoke the string method join on a newline character \n
to separate Card
objects representation.
deck = Deck()
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
... this is just one long string that contains (52) newlines!
To deal cards, we need a method to remove/return a card from the deck. The pop
list method it's a convenient way to do that.
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)
def __str__(self):
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)
def pop_card(self):
return self.cards.pop()
This (pop
) removes the last card in the list thus, we are dealing from the bottom of the deck.
To add a card, list method append
is the most appropriate one:
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)
def __str__(self):
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)
def pop_card(self):
return self.cards.pop()
def add_card(self, card):
self.cards.append(card)
... methods like these are called a veneer, just using another method without doing much work.
It comes from woodworking: a veneer is a thin layer of good quality wood glued to the surface of a cheaper piece of wood to improve the appearance.
Here add_card
is a “thin” method expressing a list operation in appropriate terms.
It improves the appearance thus, the interface, of the implementation.
Then, after importing random module, we can use the shuffle function to define a Deck
method.
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)
def __str__(self):
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)
def pop_card(self):
return self.cards.pop()
def add_card(self, card):
self.cards.append(card)
def shuffle(self):
random.shuffle(self.cards)
the ability to define a new class that is a modified version of an existing class.
This relationship between classes – similar, but different – lends itself to inheritance.
Class representing “hands” (cards held by players) is similar to a deck:
But it is also different.
Some operations on hands don’t make sense for a deck:
A new class (aka child) that inherits from an existing class (aka parent) is defined as follows:
class Hand(Deck):
"""Represents a hand of playing cards."""
putting the name of the parent class into parentheses.
With this definition Hand
inherits from Deck
methods like
pop_card
add_card
to be used by Hands
as well as Decks
.
Thus, Hand
inherits the __init__
method too, from Deck
.
But instead of populating the hand with all the 52
cards, it should initialize it with an empty list.
Providing for an __init__
method, overrides the one in the parent class.
class Hand(Deck):
"""Represents a hand of playing cards."""
def __init__(self, label=''):
self.cards = []
self.label = label
Creating an Hand
, Python
invokes this __init__
method:
hand = Hand('new hand')
hand.cards
hand.label
'new hand'
... and the other methods (pop_card
and add_card
) are inherited too, to deal a card:
deck = Deck()
card = deck.pop_card()
hand.add_card(card)
print(hand)
King of Spades
It is then natural to encapsulate it in a move_cards
method:
class Hand(Deck):
"""Represents a hand of playing cards."""
def __init__(self, label=''):
self.cards = []
self.label = label
def move_cards(self, hand, num):
for i in range(num):
hand.add_card(self.pop_card())
It takes two arguments:
Hand
objectmodifying both self
, hand
and returning None
.
With move_cards
, depending on the game, cards can be moved:
This because:
self
can be either a Deck
or a Hand
hand
can even be a Deck
(despite its name!)Inheritance is a useful feature:
But, it makes programs difficult to read.
When invoking a method, sometimes, it's not clear where to find its definition.
Relevant code may be spread across several modules.
Many things done with inheritance can be done – as well or better – without it!
Snapshots that change as the program executes:
Sometimes, too detailed.
Several kinds of relationship between classes:
For example, “a Rectangle has a Point.”
For example, “a Hand is a kind of a Deck.”
*
(any number)Built-in types (as list/dict) are usually not shown.