

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_cardadd_cardto 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 Handhand 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.