Inheritance-8 - BPP Python
Inheritance-8 - BPP Python
8. Inheritance
                 In this chapter we look at a larger example using object oriented programming and learn about the
                 very useful OOP feature of inheritance.
                 8.1. Composition
                 By now, you have seen several examples of composition. One of the first examples was using a
                 method invocation as part of an expression. Another example is the nested structure of statements;
                 you can put an if statement within a while loop, within another if statement, and so on.
                 Having seen this pattern, and having learned about lists and objects, you should not be surprised to
                 learn that you can create lists of objects. You can also create objects that contain lists (as attributes);
                 you can create lists that contain lists; you can create objects that contain objects; and so on.
                 In this chapter we will look at some examples of these combinations, using Card objects as an exam-
                 ple.
                 If we want to define a new object to represent a playing card, it is obvious what the attributes should
                 be: rank and suit. It is not as obvious what type the attributes should be. One possibility is to use
                 strings containing words like "Spade" for suits and "Queen" for ranks. One problem with this imple-
                 mentation is that it would not be easy to compare cards to see which had a higher rank or suit.
                 An alternative is to use integers to encode the ranks and suits. By encode, we do not mean what
                 some people think, which is to encrypt or translate into a secret code. What a computer scientist
                 means by encode is to define a mapping between a sequence of numbers and the items I want to
                 represent. For example:
                  Spades     -->        3
                  Hearts     -->        2
                  Diamonds   -->        1
                  Clubs      -->        0
                 An obvious feature of this mapping is that the suits map to integers in order, so we can compare
                 suits by comparing integers. The mapping for ranks is fairly obvious; each of the numerical ranks
                 maps to the corresponding integer, and for face cards:
                  Jack     -->     11
                  Queen    -->     12
                  King     -->     13
                 The reason we are using mathematical notation for these mappings is that they are not part of the
                 Python program. They are part of the program design, but they never appear explicitly in the code.
                 The class definition for the Card type looks like this:
                  class Card:
                      def __init__(self, suit=0, rank=0):
                          self.suit = suit
                          self.rank = rank
As usual, we provide an initialization method that takes an optional parameter for each attribute.
1 of 12                                                                                                          16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                       https://www.openbookproject.net/books/bpp4aw...
three_of_clubs = Card(0, 3)
                  class Card:
                      SUITS = ('Clubs', 'Diamonds', 'Hearts', 'Spades')
                      RANKS = ('narf', 'Ace', '2', '3', '4', '5', '6', '7',
                               '8', '9', '10', 'Jack', 'Queen', 'King']
                       def __str__(self):
                           """
                             >>> print(Card(2, 11))
                             Queen of Hearts
                           """
                           return '{0} of {1}'.format(Card.RANKS[self.rank],
                                                      Card.SUITS[self.suit])
                  if __name__ == '__main__':
                      import doctest
                      doctest.testmod()
                 Class attributes like Card.SUITS and Card.RANKS are defined outside of any method, and can be ac-
                 cessed from any of the methods in the class.
                 Inside __str__, we can use SUITS and RANKS to map the numerical values of suit and rank to
                 strings. For example, the expression Card.SUITS[self.suit] means use the attribute suit from the
                 object self as an index into the class attribute named SUITS, and select the appropriate string.
                 The reason for the "narf" in the first element in ranks is to act as a place keeper for the zero-eth el-
                 ement of the list, which will never be used. The only valid ranks are 1 to 13. This wasted item is not
                 entirely necessary. We could have started at 0, as usual, but it is less confusing to encode 2 as 2, 3
                 as 3, and so on.
                 We have a doctest in the __str__ method to confirm that Card(2, 11) will display as “Queen of
                 Hearts”.
                 Some types are completely ordered, which means that you can compare any two elements and tell
                 which is bigger. For example, the integers and the floating-point numbers are completely ordered.
                 Some sets are unordered, which means that there is no meaningful way to say that one element is
                 bigger than another. For example, the fruits are unordered, which is why you cannot compare apples
                 and oranges.
                 The set of playing cards is partially ordered, which means that sometimes you can compare cards
                 and sometimes not. For example, you know that the 3 of Clubs is higher than the 2 of Clubs, and the
2 of 12                                                                                                        16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                        https://www.openbookproject.net/books/bpp4aw...
                 3 of Diamonds is higher than the 3 of Clubs. But which is better, the 3 of Clubs or the 2 of Diamonds?
                 One has a higher rank, but the other has a higher suit.
                 In order to make cards comparable, you have to decide which is more important, rank or suit. To be
                 honest, the choice is arbitrary. For the sake of choosing, we will say that suit is more important, be-
                 cause a new deck of cards comes sorted with all the Clubs together, followed by all the Diamonds,
                 and so on.
                 8.5. Decks
                 Now that we have objects to represent Cards, the next logical step is to define a class to represent a
                 Deck. Of course, a deck is made up of cards, so each Deck object will contain a list of cards as an at-
                 tribute.
                 The following is a class definition for the Deck class. The initialization method creates the attribute
                 cards and generates the standard set of fifty-two cards:
                  class Deck:
                      def __init__(self):
                          self.cards = []
                          for suit in range(4):
                              for rank in range(1, 14):
                                  self.cards.append(Card(suit, rank))
                 The easiest way to populate the deck is with a nested loop. The outer loop enumerates the suits
                 from 0 to 3. The inner loop enumerates the ranks from 1 to 13. Since the outer loop iterates four
                 times, and the inner loop iterates thirteen times, the total number of times the body is executed is
                 fifty-two (thirteen times four). Each iteration creates a new instance of Card with the current suit and
                 rank, and appends that card to the cards list.
                  class Deck:
                      ...
                      def print_deck(self):
                          for card in self.cards:
                              print(card)
                 Here, and from now on, the ellipsis ( ...) indicates that we have omitted the other methods in the
                 class.
                 As an alternative to print_deck, we could write a __str__ method for the Deck class. The advantage
                 of __str__ is that it is more flexible. Rather than just printing the contents of the object, it generates
                 a string representation that other parts of the program can manipulate before printing, or store for
                 later use.
3 of 12                                                                                                         16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                         https://www.openbookproject.net/books/bpp4aw...
                 Here is a version of __str__ that returns a string representation of a Deck. To add a bit of pizzazz, it
                 arranges the cards in a cascade where each card is indented one space more than the previous card:
                  class Deck:
                      ...
                      def __str__(self):
                          s = ""
                          for i in range(len(self.cards)):
                              s += " " * i + str(self.cards[i]) + "\n"
                          return s
                 This example demonstrates several features. First, instead of traversing self.cards and assigning
                 each card to a variable, we are using i as a loop variable and an index into the list of cards.
                 Second, we are using the string multiplication operator to indent each card by one more space than
                 the last. The expression " " * i yields a number of spaces equal to the current value of i.
                 Third, instead of using the print function to print the cards, we use the str function. Passing an ob-
                 ject as an argument to str is equivalent to invoking the __str__ method on the object.
                 Finally, we are using the variable s as an accumulator. Initially, s is the empty string. Each time
                 through the loop, a new string is generated and concatenated with the old value of s to get the new
                 value. When the loop ends, s contains the complete string representation of the Deck, which looks
                 like this:
And so on. Even though the result appears on 52 lines, it is one long string that contains newlines.
                 To shuffle the deck, we will use the randrange function from the random module. With two integer ar-
                 guments, a and b, randrange chooses a random integer in the range a <= x < b. Since the upper
                 bound is strictly less than b, we can use the length of a list as the second parameter, and we are
                 guaranteed to get a legal index. For example, this expression chooses the index of a random card in
                 a deck:
random.randrange(0, len(self.cards))
                 An easy way to shuffle the deck is by traversing the cards and swapping each card with a randomly
                 chosen one. It is possible that the card will be swapped with itself, but that is fine. In fact, if we pre-
                 cluded that possibility, the order of the cards would be less than entirely random:
                  class Deck:
                      ...
                      def shuffle(self):
                          import random
                          num_cards = len(self.cards)
4 of 12                                                                                                            16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                        https://www.openbookproject.net/books/bpp4aw...
                            for i in range(num_cards):
                                j = random.randrange(i, num_cards)
                                self.cards[i], self.cards[j] = self.cards[j], self.cards[i]
                 Rather than assume that there are fifty-two cards in the deck, we get the actual length of the list and
                 store it in num_cards.
                 For each card in the deck, we choose a random card from among the cards that haven’t been shuf-
                 fled yet. Then we swap the current card ( i) with the selected card ( j). To swap the cards we use a
                 tuple assignment:
                  class Deck:
                      ...
                      def remove(self, card):
                          if card in self.cards:
                              self.cards.remove(card)
                              return True
                          else:
                              return False
                 The in operator returns True if the first operand is in the second, which must be a list or a tuple. If
                 the first operand is an object, Python uses the object’s __cmp__ method to determine equality with
                 items in the list. Since the __cmp__ in the Card class checks for deep equality, the remove method
                 checks for deep equality.
                 To deal cards, we want to remove and return the top card. The list method pop provides a convenient
                 way to do that:
                  class Deck:
                      ...
                      def pop(self):
                          return self.cards.pop()
                 Actually, pop removes the last card in the list, so we are in effect dealing from the bottom of the
                 deck.
                 One more operation that we are likely to want is the boolean function is_empty, which returns true if
                 the deck contains no cards:
                  class Deck:
                      ...
                      def is_empty(self):
                          return (len(self.cards) == 0)
                 8.9. Inheritance
                 The language feature most often associated with object-oriented programming is inheritance.
                 Inheritance is the ability to define a new class that is a modified version of an existing class.
                 The primary advantage of this feature is that you can add new methods to a class without modifying
                 the existing class. It is called inheritance because the new class inherits all of the methods of the ex-
                 isting class. Extending this metaphor, the existing class is sometimes called the parent class. The
                 new class may be called the child class or sometimes subclass.
                 Inheritance is a powerful feature. Some programs that would be complicated without inheritance can
                 be written concisely and simply with it. Also, inheritance can facilitate code reuse, since you can cus-
                 tomize the behavior of parent classes without having to modify them. In some cases, the inheritance
5 of 12                                                                                                          16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                        https://www.openbookproject.net/books/bpp4aw...
                 structure reflects the natural structure of the problem, which makes the program easier to under-
                 stand.
                 On the other hand, inheritance can make programs difficult to read. When a method is invoked, it is
                 sometimes not clear where to find its definition. The relevant code may be scattered among several
                 modules. Also, many of the things that can be done using inheritance can be done as elegantly (or
                 more so) without it. If the natural structure of the problem does not lend itself to inheritance, this
                 style of programming can do more harm than good.
                 In this chapter we will demonstrate the use of inheritance as part of a program that plays the card
                 game Old Maid. One of our goals is to write code that could be reused to implement other card
                 games.
                 A hand is also different from a deck. Depending on the game being played, we might want to per-
                 form some operations on hands that don’t make sense for a deck. For example, in poker we might
                 classify a hand (straight, flush, etc.) or compare it with another hand. In bridge, we might want to
                 compute a score for a hand in order to make a bid.
                 This situation suggests the use of inheritance. If Hand is a subclass of Deck, it will have all the meth-
                 ods of Deck, and new methods can be added.
In the class definition, the name of the parent class appears in parentheses:
                  class Hand(Deck):
                      pass
This statement indicates that the new Hand class inherits from the existing Deck class.
                 The Hand constructor initializes the attributes for the hand, which are name and cards. The string
                 name identifies this hand, probably by the name of the player that holds it. The name is an optional
                 parameter with the empty string as a default value. cards is the list of cards in the hand, initialized
                 to the empty list:
                  class Hand(Deck):
                      def __init__(self, name=""):
                         self.cards = []
                         self.name = name
                 For just about any card game, it is necessary to add and remove cards from the deck. Removing
                 cards is already taken care of, since Hand inherits remove from Deck. But we have to write add:
                  class Hand(Deck):
                      ...
                      def add(self,card):
                          self.cards.append(card)
                 Again, the ellipsis indicates that we have omitted other methods. The list append method adds the
                 new card to the end of the list of cards.
                 deal should be fairly general, since different games will have different requirements. We may want
                 to deal out the entire deck at once or add one card to each hand.
6 of 12                                                                                                          16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                        https://www.openbookproject.net/books/bpp4aw...
                 deal takes two parameters, a list (or tuple) of hands and the total number of cards to deal. If there
                 are not enough cards in the deck, the method deals out all of the cards and stops:
                  class Deck :
                      ...
                      def deal(self, hands, num_cards=999):
                          num_hands = len(hands)
                          for i in range(num_cards):
                               if self.is_empty(): break   #          break if out of cards
                               card = self.pop()           #          take the top card
                               hand = hands[i % num_hands] #          whose turn is next?
                               hand.add(card)              #          add the card to the hand
                 The second parameter, num_cards, is optional; the default is a large number, which effectively
                 means that all of the cards in the deck will get dealt.
                 The loop variable i goes from 0 to nCards-1. Each time through the loop, a card is removed from the
                 deck using the list method pop, which removes and returns the last item in the list.
                 The modulus operator ( %) allows us to deal cards in a round robin (one card at a time to each hand).
                 When i is equal to the number of hands in the list, the expression i % nHands wraps around to the
                 beginning of the list (index 0).
It’s not a great hand, but it has the makings of a straight flush.
                 Although it is convenient to inherit the existing methods, there is additional information in a Hand ob-
                 ject we might want to include when we print one. To do that, we can provide a __str__ method in
                 the Hand class that overrides the one in the Deck class:
                  class Hand(Deck)
                      ...
                      def __str__(self):
                          s = "Hand " + self.name
                          if self.is_empty():
                              s = s + " is empty\n"
                          else:
                              s = s + " contains\n"
                          return s + Deck.__str__(self)
                 Initially, s is a string that identifies the hand. If the hand is empty, the program appends the words
                 is empty and returns s.
                 Otherwise, the program appends the word contains and the string representation of the Deck, com-
                 puted by invoking the __str__ method in the Deck class on self.
                 It may seem odd to send self, which refers to the current Hand, to a Deck method, until you remem-
                 ber that a Hand is a kind of Deck. Hand objects can do everything Deck objects can, so it is legal to
                 send a Hand to a Deck method.
In general, it is always legal to use an instance of a subclass in place of an instance of a parent class.
7 of 12                                                                                                         16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                        https://www.openbookproject.net/books/bpp4aw...
                  class CardGame:
                      def __init__(self):
                          self.deck = Deck()
                          self.deck.shuffle()
                 This is the first case we have seen where the initialization method performs a significant computa-
                 tion, beyond initializing attributes.
                 To implement specific games, we can inherit from CardGame and add features for the new game. As
                 an example, we’ll write a simulation of Old Maid.
                 The object of Old Maid is to get rid of cards in your hand. You do this by matching cards by rank and
                 color. For example, the 4 of Clubs matches the 4 of Spades since both suits are black. The Jack of
                 Hearts matches the Jack of Diamonds since both are red.
                 To begin the game, the Queen of Clubs is removed from the deck so that the Queen of Spades has
                 no match. The fifty-one remaining cards are dealt to the players in a round robin. After the deal, all
                 players match and discard as many cards as possible.
                 When no more matches can be made, play begins. In turn, each player picks a card (without looking)
                 from the closest neighbor to the left who still has cards. If the chosen card matches a card in the
                 player’s hand, the pair is removed. Otherwise, the card is added to the player’s hand. Eventually all
                 possible matches are made, leaving only the Queen of Spades in the loser’s hand.
                 In our computer simulation of the game, the computer plays all hands. Unfortunately, some nuances
                 of the real game are lost. In a real game, the player with the Old Maid goes to some effort to get
                 their neighbor to pick that card, by displaying it a little more prominently, or perhaps failing to dis-
                 play it more prominently, or even failing to fail to display that card more prominently. The computer
                 simply picks a neighbor’s card at random.
                  class OldMaidHand(Hand):
                      def remove_matches(self):
                          count = 0
                          original_cards = self.cards[:]
                          for card in original_cards:
                              match = Card(3 - card.suit, card.rank)
                              if match in self.cards:
                                  self.cards.remove(card)
                                  self.cards.remove(match)
                                  print("Hand {0}: {1} matches {2}".format(self.name, card, match)
                                  count = count + 1
                          return count
                 We start by making a copy of the list of cards, so that we can traverse the copy while removing cards
                 from the original. Since self.cards is modified in the loop, we don’t want to use it to control the tra-
                 versal. Python can get quite confused if it is traversing a list that is changing!
                 For each card in the hand, we figure out what the matching card is and go looking for it. The match
                 card has the same rank and the other suit of the same color. The expression 3 - card.suit turns a
                 Club (suit 0) into a Spade (suit 3) and a Diamond (suit 1) into a Heart (suit 2). You should satisfy
                 yourself that the opposite operations also work. If the match card is also in the hand, both cards are
                 removed.
8 of 12                                                                                                          16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                        https://www.openbookproject.net/books/bpp4aw...
Notice that there is no __init__ method for the OldMaidHand class. We inherit it from Hand.
Since __init__ is inherited from CardGame, a new OldMaidGame object contains a new shuffled deck:
                  class OldMaidGame(CardGame):
                      def play(self, names):
                          # remove Queen of Clubs
                          self.deck.remove(Card(0,12))
9 of 12                                                                                                    16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                       https://www.openbookproject.net/books/bpp4aw...
                 Some of the steps of the game have been separated into methods. remove_all_matches traverses
                 the list of hands and invokes remove_matches on each:
                  class OldMaidGame(CardGame):
                      ...
                      def remove_all_matches(self):
                          count = 0
                          for hand in self.hands:
                              count = count + hand.remove_matches()
                          return count
count is an accumulator that adds up the number of matches in each hand and returns the total.
                 When the total number of matches reaches twenty-five, fifty cards have been removed from the
                 hands, which means that only one card is left and the game is over.
                 The variable turn keeps track of which player’s turn it is. It starts at 0 and increases by one each
                 time; when it reaches numHands, the modulus operator wraps it back around to 0.
                 The method playOneTurn takes a parameter that indicates whose turn it is. The return value is the
                 number of matches made during this turn:
                  class OldMaidGame(CardGame):
                      ...
                      def play_one_turn(self, i):
                          if self.hands[i].is_empty():
                              return 0
                          neighbor = self.find_neighbor(i)
                          pickedCard = self.hands[neighbor].popCard()
                          self.hands[i].add(pickedCard)
                          print("Hand", self.hands[i].name, "picked", pickedCard)
                          count = self.hands[i].remove_matches()
                          self.hands[i].shuffle()
                          return count
If a player’s hand is empty, that player is out of the game, so he or she does nothing and returns 0.
                 Otherwise, a turn consists of finding the first player on the left that has cards, taking one card from
                 the neighbor, and checking for matches. Before returning, the cards in the hand are shuffled so that
                 the next player’s choice is random.
                 The method find_neighbor starts with the player to the immediate left and continues around the
                 circle until it finds a player that still has cards:
                  class OldMaidGame(CardGame):
                      ...
                      def find_neighbor(self, i):
                          numHands = len(self.hands)
                          for next in range(1,numHands):
                              neighbor = (i + next) % numHands
                              if not self.hands[neighbor].is_empty():
                                  return neighbor
                 If find_neighbor ever went all the way around the circle without finding cards, it would return None
                 and cause an error elsewhere in the program. Fortunately, we can prove that that will never happen
                 (as long as the end of the game is detected correctly).
We have omitted the print_hands method. You can write that one yourself.
                 The following output is from a truncated form of the game where only the top fifteen cards (tens and
                 higher) were dealt to three players. With this small deck, play stops after seven matches instead of
                 twenty-five.
10 of 12                                                                                                        16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                    https://www.openbookproject.net/books/bpp4aw...
So Jeff loses.
                 8.16. Glossary
                 encode
                     To represent one set of values using another set of values by constructing a mapping between
11 of 12                                                                                                 16/01/2023, 12:24
8. Inheritance — Beginning Python Programmin...                       https://www.openbookproject.net/books/bpp4aw...
them.
                 class attribute
                     A variable that is defined inside a class definition but outside any method. Class attributes are
                     accessible from any method in the class and are shared by all instances of the class.
                 accumulator
                     A variable used in a loop to accumulate a series of values, such as by concatenating them onto
                     a string or adding them to a running sum.
                 inheritance
                     The ability to define a new class that is a modified version of a previously defined class.
                 parent class
                     The class from which a child class inherits.
                 child class
                     A new class created by inheriting from an existing class; also called a subclass.
                 8.17. Exercises
                      Chapter 8 Exercise Set 0: Chapter Review
                      Chapter 8 exercise set 1
12 of 12 16/01/2023, 12:24