Lecture Notes
Introduction to Programming and Algorithm Design (COP-1000)

Inheritance

This chapter explains how to define a new class that is a modified version of an existing class through inheritance.


Inheritance

  • One of the main advantages of  using OOP is that it results in code that is more reusable and extensible than traditional, procedural programming
  • We have seen how to create well-designed classes that encapsulate attributes and methods, and how these classes can then be used to instantiate objects
  • Now we look at how we can create new classes, adding attributes and methods, without modifying the existing class through a technique called inheritance
  • The existing class is the called the parent class (in Java, it's called the superclass); the new class is the child class (or subclass):

  • In the picture above, the class Hand will inherit from the class Deck; the new class will inherit all the existing attributes and methods from Deck, and can be given new methods of its own
  • We will use this inheritance hierarchy to create a general-purpose class Hand that is useful for a variety of computer card-playing games; in particular, the game of Old Maid will be used as an example

Card objects

  • Common deck of playing cards has:
    • 52 cards per deck
    • Each card belongs to one of four suits: Clubs, Diamonds, Hearts, Spades
    • Each card has a rank: Ace, 2, 3, ... , 10, Jack, Queen, King
  • If we are writing a card game program, one obvious choice for a class would be Card, which would have two attributes rank and suit
  • Rather than use strings to represent rank and suit, which will make comparisons between cards difficult, we can encode them with a mapping; for the suits:

        Spades   -> 3
        Hearts   -> 2
        Diamonds -> 1
        Clubs    -> 0

    And for the ranks:

        King     -> 13
        Queen    -> 12
        Jack     -> 11
        10-2     -> 10-2
        Ace      -> 1

    The notation above is not part of the Python language, but part of the program design
     
  • To implement the class Card

    class Card:
        def __init__(self, suit=0, rank=1):
            self.suit = suit
            self.rank = rank

    Then you can instantiate a Card object; for example:

    aceOfHearts = Card(2,1)

Class attributes and the __str__ method

  • To decode a Card object back into a string a user can read, we need a way to associate an integer with its suit, and an integer with its rank
  • We can do this with a dictionary, but a list is more efficient
  • We will implement these lists as class variables attributes defined outside of any method that can be accessed from any method in the class
  • See example program cards1.py
    • class attributes suitList and rankList are declared at the top of the class definition, outside of any method
    • rankList uses the placeholder "Invalid" at index 0, since there is no card whose rank maps to 0
    • the __str__ method decodes the Card object by accessing the strings at the correct suit and rank indices

Comparing Cards

  • You can't compare Card objects using the relational or equality operators (<, >, ==) in a condition, because Python doesn't have any intrinsic understanding of how one Card is less than, greater than, or equal to another
  • But you can override these operators by providing a method __cmp__ for the class that defines how to compare two objects
  • The __cmp__ method uses two object parameters and should return +1 if the first is determined to be greater than the second, 0 if they are equal, and -1 if the second is greater
  • See example program cards2.py for the implementation of a __cmp__ method
    • example places higher priority on rank than suit; text is reverse
    • aces lose to deuces in example; how would you fix this?

Decks

  • So far we have used one form of composition: the Card class has attributes that objects of a built-in class (list)
  • Now lets look at another form of composition: a Deck class that is made up of the user-defined type Card
  • Here is a class definition:

    class Deck:
        def __init__(self):
            self.cards = []        # start with empty list
            for suit in range(4):
                for rank in range(1,14):
                    self.cards.append(Card(suit, rank))
     
  • This __init__ method uses nested loops to populate the Deck object the result will be a "new" 52 Card deck, with all the clubs in order of rank, then diamonds, hearts, and spades

Printing the deck

  • See example deck1.py, that includes a  __str__ method.

Shuffling the deck

  • A deck of cards is not much use in its "new" condition; what we need is to be able to "shuffle" it into a random order
  • To do this, we will use the shuffle function in the random module, which randomizes the entries of a list in place
  • See example program deck2.py

Adding, removing and dealing cards

  • There are several other methods we might want to build into Deck object to make it more useful for a client card game program (like the one provided below):
    • popCard uses the list method pop to remove an item from a list; the parameter is defaulted to -1 (the last item is removed, so we're dealing from the bottom of the deck):

      def popCard(self):
          return self.cards.pop()
       
    • removeCard removes a specified card from the deck and returns True if the card was in the deck or False if not

      def removeCard(self, card):
          if card in self.cards:
              self.cards.remove(card)
              return True
          else:
              return False
       
    • The add_card method may be useful in adding a card to a deck:

      def add_card(self, card):
          self.cars.append(card)
       
    • Finally, here is a boolean method that returns True if there are no cards left in the deck, and False otherwise:

      def isEmpty(self):
          return (len(self.cards) == 0)
       
  • See the example program deck3.py for implementation of these methods

A hand of cards

  • A hand is similar to a deck: both contain cards (attributes), and both have operations to remove and add a card, and to determine if they are empty (methods)
  • A hand is also different from a deck: one hand can be compared to another given a set of rules (as in poker), or evaluated for its potential and bidding (like bridge)
  • If we start with the class Deck, we can create a new class Hand, inheriting the methods of Deck, and adding new ones as necessary
  • To define a class that is inheriting from another, use this syntax:

        class Child_class_name(Parent_class_name):
            body
     
    In our case, we can start the definition of Hand like this:

        class Hand(Deck):
            def __init__(self, name=""):
                self.cards = []
                self.name = name
     
  • Hand will have two attributes: a cards list, that represents the cards in the hand, which starts out empty, and a name, that represents the holder of the hand

Dealing cards

  • Now that we have defined the class Hand, we need a way for a Deck to deal a specified number of cards into a specified number of hands; we add this method:

        class Deck:
            ...
            def deal(self, hands, nCards=999):
                nHands = len(hands)
                for i in range(nCards):
                    if self.isEmpty():         # stop if no more
                        break
                    card = self.popCard()      # get next card
                    h = hands[i % nHands]      # get next hand
                    h.addCard(card)            # add card to hand

    The parameters of the method are hands (a list of Hand objects) and nCards (the maximum number of cards to deal); nCards is defaulted to a high number so that all the Cards in the Deck will be dealt by default
  • Each hand is chosen in turn with the expression

        h = hand[i % nHands]

    The modulus operator % returns a value 0 to (nHands - 1); h is an alias for the next Hand in the list

Printing a hand

  • Because a Hand is a (kind of) Deck, all of Deck's methods work with Hand objects just as if they were Deck objects; in general:

A child class object is always a member of the parent class, but a parent class object is never a member of the child class.

  • In particular, if someHand is a Hand object, then:

        print someHand

    will invoke the __str__ method, inherited from the parent class Deck
  • See the example program hand1.py
  • We can also override a method in the child class by redefining a method of the parent class, using the same method name
  • See the example program hand2.py
    • Overrides the __str__ method to add functionality
    • Uses the dot operator to invoke parent method

The CardGame class

  • Now we have all the pieces we need to support a computer card game program
  • We add one class that can be used as a parent class for all card game classes:

        class CardGame:

            def __init__(self):
                self.deck = Deck()
                self.deck.shuffle()
     
  • Now you can write a card game program by inheriting from CardGame, and you start out with a shuffled deck and all its methods print, deal, remove, etc.
  • Here is the completed "module" that can be imported into a program to access it's classes (Card, Deck, Hand, and CardGame): cardGame.py
    • Note: no main() function this will be provided by the client
    • If you want to make sure you always have access to this module, put it in Python's Lib\site-packages folder
  • Now we will use the functionality we have built into our classes to write a program to play Old Maid; the rules are:
  1. Two or more players start with a shuffled deck from which the Queen of Clubs has been removed
  2. All cards are dealt in normal fashion; depending on the number of players, some may have one more card than others
  3. Players match cards in their hands and discard all matches; a match is defined as two cards of the same rank, whose suit is the same color
  4. After all matches have been discarded, each player in turn selects one card, without looking, from the closest neighbor to the left that who still has cards; if the card matches one in the players hand, the pair is discarded; otherwise, the card is added to the hand
  5. Play proceeds until only one card remains in any hand the Queen of Clubs (which has no match) and that player is the Old Maid

OldMaidHand class

  • An Old Maid hand is much like a regular Hand, but we need an additional operation, to "match and remove"
  • See example program oldMaidHand.py
    • No __init__ method is required for class OldMaidHand (or __str__), because the ones inherited from Hand are functionally adequate

OldMaidGame class

  • Finally we are ready to program our Old Maid game
  • It will be a class itself, with a few more operations (methods) peculiar to the game we are trying to simulate, including:
    • Removing the Queen of Clubs
    • Dealing Old Maid hands to each player
    • Removing all matches from all hands
    • "Playing" the game until only one card is left (i.e., until 25 matches have been made); playing requires, for each player:
      • Finding the left neighbor
      • Choosing a card at random
      • Removing the matches
      • shuffling hand
  • See the example program oldMaidGame.py
 Updated:  12.13.2010