← 学习库 Think Python 2e 目录

Chapter 19

> 来源: Think Python 2e (Allen B. Downey)

> 原页: https://greenteapress.com/thinkpython/html/thinkpython019.html

\

[插图缺失:thinkpython018.html]
[ [](thinkpython020.html)

------------------------------------------------------------------------

Chapter 18   Inheritance

In this chapter I present classes to represent 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, but you don’t have to; I’ll tell you what you need to know for the exercises. Code examples from this chapter are available from http://thinkpython.com/code/Card.py.

If you are not familiar with Anglo-American playing cards, you can read about them at http://en.wikipedia.org/wiki/Playing_cards.

18.1   Card objects

There are fifty-two cards in a deck, each of which belongs to one of four suits and one of thirteen ranks. The suits are Spades, Hearts, Diamonds, and Clubs (in descending order in bridge). The ranks are Ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, and King. Depending on the game that you are playing, an Ace may be higher than King or lower than 2.

If we want to define a new object to represent a playing card, it is obvious what the attributes should be: rank and rank. It is not as obvious what type the attributes should be. One possibility is to use strings containing words like rank000 for suits and rank000 for ranks. One problem with this implementation 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. In this context, “encode” means that we are going to define a mapping between numbers and suits, or between numbers and ranks. This kind of encoding is not meant to be a secret (that would be “encryption”).

For example, this table shows the suits and the corresponding integer codes:

Spades

3

Hearts

2

Diamonds

1

Clubs

0

This code makes it easy to compare cards; because higher suits map to higher numbers, we can compare suits by comparing their codes.

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

I am using the ↦ symbol to make it clear that these mappings are not part of the Python program. They are part of the program design, but they don’t appear explicitly in the code.

The class definition for rank looks like this:

rank00013

As usual, the init method takes an optional parameter for each attribute. The default card is the 2 of Clubs.

To create a Card, you call rank with the suit and rank of the card you want.

rank00015

18.2   Class attributes

In order to print Card objects in a way that people can easily read, we need a mapping from the integer codes to the corresponding ranks and suits. A natural way to do that is with lists of strings. We assign these lists to class attributes:

rank00016

Variables like rank00017 and rank00018, which are defined inside a class but outside of any method, are called class attributes because they are associated with the class object rank.

This term distinguishes them from variables like rank and rank, which are called instance attributes because they are associated with a particular instance.

Both kinds of attribute are accessed using dot notation. For example, in rank000, rank is a Card object, and rank00024 is its rank. Similarly, rank is a class object, and rank00026 is a list of strings associated with the class.

Every card has its own rank and rank, but there is only one copy of rank00029 and rank00030.

Putting it all together, the expression rank00031 means “use the attribute rank from the object rank as an index into the list rank00034 from the class rank, and select the appropriate string.”

The first element of rank00036 is rank because there is no card with rank zero. By including rank as a place-keeper, we get a mapping with the nice property that the index 2 maps to the string '2', and so on. To avoid this tweak, we could have used a dictionary instead of a list.

With the methods we have so far, we can create and print cards:

rank00040


[插图缺失:thinkpython026.png]

Figure 18.1: Object diagram.


Figure 18.1 is a diagram of the rank class object and one Card instance. rank is a class object, so it has type rank. rank0 has type rank. (To save space, I didn’t draw the contents of rank00046 and rank00047).

18.3   Comparing cards

For built-in types, there are relational operators (rank, rank, ==, etc.) that compare values and determine when one is greater than, less than, or equal to another. For user-defined types, we can override the behavior of the built-in operators by providing a method named rank000.

rank000 takes two parameters, rank and rank0, and returns a positive number if the first object is greater, a negative number if the second object is greater, and 0 if they are equal to each other.

The correct ordering for cards is not obvious. For example, 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 compare cards, you have to decide whether rank or suit is more important.

The answer might depend on what game you are playing, but to keep things simple, we’ll make the arbitrary choice that suit is more important, so all of the Spades outrank all of the Diamonds, and so on.

With that decided, we can write rank000:

rank00056

You can write this more concisely using tuple comparison:

rank00057

The built-in function '2' has the same interface as the method rank000: it takes two values and returns a positive number if the first is larger, a negative number if the second is larger, and 0 if they are equal.

In Python 3, '2' no longer exists, and the rank000 method is not supported. Instead you should provide rank00, which returns rank if rank is less than rank0. You can implement rank00 using tuples and the rank operator.

Exercise 1  

Write a rank000 method for Time objects. Hint: you can use tuple comparison, but you also might consider using integer subtraction.

18.4   Decks

Now that we have Cards, the next step is to define Decks. Since a deck is made up of cards, it is natural for each Deck to contain a list of cards as an attribute.

The following is a class definition for rank. The init method creates the attribute rank0 and generates the standard set of fifty-two cards:

rank00071

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. Each iteration creates a new Card with the current suit and rank, and appends it to rank00072.

18.5   Printing the deck

Here is a rank000 method for rank:

rank00075

This method demonstrates an efficient way to accumulate a large string: building a list of strings and then using rank. The built-in function '2' invokes the rank000 method on each card and returns the string representation.

Since we invoke rank on a newline character, the cards are separated by newlines. Here’s what the result looks like:

rank00080

Even though the result appears on 52 lines, it is one long string that contains newlines.

18.6   Add, remove, shuffle and sort

To deal cards, we would like a method that removes a card from the deck and returns it. The list method '2' provides a convenient way to do that:

rank00082

Since '2' removes the last card in the list, we are dealing from the bottom of the deck. In real life “bottom dealing” is frowned upon, but in this context it’s ok.

To add a card, we can use the list method rank00:

rank00085

A method like this that uses another function without doing much real work is sometimes called a veneer. The metaphor comes from woodworking, where it is common to glue a thin layer of good quality wood to the surface of a cheaper piece of wood.

In this case we are defining a “thin” method that expresses a list operation in terms that are appropriate for decks.

As another example, we can write a Deck method named rank000 using the function rank000 from the rank00 module:

rank00089

Don’t forget to import rank00.

Exercise 2  

Write a Deck method named rank that uses the list method rank to sort the cards in a rank. rank uses the rank000 method we defined to determine sort order.

18.7   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.

It is called “inheritance” because the new class inherits the methods of the existing class. Extending this metaphor, the existing class is called the parent and the new class is called the child.

As an example, let’s say we want a class to represent a “hand,” that is, the set of cards held by one player. A hand is similar to a deck: both are made up of a set of cards, and both require operations like adding and removing cards.

A hand is also different from a deck; there are operations we want for hands that don’t make sense for a deck. For example, in poker we might compare two hands to see which one wins. In bridge, we might compute a score for a hand in order to make a bid.

This relationship between classes—similar, but different—lends itself to inheritance.

The definition of a child class is like other class definitions, but the name of the parent class appears in parentheses:

rank00096

This definition indicates that rank inherits from rank; that means we can use methods like rank0009 and rank0010 for Hands as well as Decks.

rank also inherits rank0010 from rank, but it doesn’t really do what we want: instead of populating the hand with 52 new cards, the init method for Hands should initialize rank0 with an empty list.

If we provide an init method in the rank class, it overrides the one in the rank class:

rank00107

So when you create a Hand, Python invokes this init method:

rank00108

But the other methods are inherited from rank, so we can use rank0011 and rank0011 to deal a card:

rank00112

A natural next step is to encapsulate this code in a method called rank00113:

rank00114

rank00115 takes two arguments, a Hand object and the number of cards to deal. It modifies both rank and rank, and returns rank.

In some games, cards are moved from one hand to another, or from a hand back to the deck. You can use rank00119 for any of these operations: rank can be either a Deck or a Hand, and rank, despite the name, can also be a rank.

Exercise 3  

Write a Deck method called rank00123 that takes two parameters, the number of hands and the number of cards per hand, and that creates new Hand objects, deals the appropriate number of cards per hand, and returns a list of Hand objects.

Inheritance is a useful feature. Some programs that would be repetitive without inheritance can be written more elegantly with it. Inheritance can facilitate code reuse, since you can customize the behavior of parent classes without having to modify them. In some cases, the inheritance structure reflects the natural structure of the problem, which makes the program easier to understand.

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 well or better without it.

18.8   Class diagrams

So far we have seen stack diagrams, which show the state of a program, and object diagrams, which show the attributes of an object and their values. These diagrams represent a snapshot in the execution of a program, so they change as the program runs.

They are also highly detailed; for some purposes, too detailed. A class diagram is a more abstract representation of the structure of a program. Instead of showing individual objects, it shows classes and the relationships between them.

There are several kinds of relationship between classes:

A class diagram is a graphical representation of these relationships. For example, Figure 18.2 shows the relationships between rank, rank and rank.


[插图缺失:thinkpython027.png]

Figure 18.2: Class diagram.


The arrow with a hollow triangle head represents an IS-A relationship; in this case it indicates that Hand inherits from Deck.

The standard arrow head represents a HAS-A relationship; in this case a Deck has references to Card objects.

The star (*) near the arrow head is a multiplicity; it indicates how many Cards a Deck has. A multiplicity can be a simple number, like ==, a range, like rank or a star, which indicates that a Deck can have any number of Cards.

A more detailed diagram might show that a Deck actually contains a list of Cards, but built-in types like list and dict are usually not included in class diagrams.

Exercise 4  

Read rank00130, rank0013 and rank00 and draw a class diagram that shows the relationships among the classes defined there.

18.9   Debugging

Inheritance can make debugging a challenge because when you invoke a method on an object, you might not know which method will be invoked.

Suppose you are writing a function that works with Hand objects. You would like it to work with all kinds of Hands, like PokerHands, BridgeHands, etc. If you invoke a method like rank001, you might get the one defined in rank, but if any of the subclasses override this method, you’ll get that version instead.

Any time you are unsure about the flow of execution through your program, the simplest solution is to add print statements at the beginning of the relevant methods. If rank00135 prints a message that says something like rank00136, then as the program runs it traces the flow of execution.

As an alternative, you could use this function, which takes an object and a method name (as a string) and returns the class that provides the definition of the method:

rank00137

Here’s an example:

rank00138

So the rank001 method for this Hand is the one in rank.

rank00141 uses the '2' method to get the list of class objects (types) that will be searched for methods. “MRO” stands for “method resolution order.”

Here’s a program design suggestion: whenever you override a method, the interface of the new method should be the same as the old. It should take the same parameters, return the same type, and obey the same preconditions and postconditions. If you obey this rule, you will find that any function designed to work with an instance of a superclass, like a Deck, will also work with instances of subclasses like a Hand or PokerHand.

If you violate this rule, your code will collapse like (sorry) a house of cards.

18.10   Data encapsulation

Chapter 16 demonstrates a development plan we might call “object-oriented design.” We identified objects we needed—rank, rank0 and rank00145—and defined classes to represent them. In each case there is an obvious correspondence between the object and some entity in the real world (or at least a mathematical world).

But sometimes it is less obvious what objects you need and how they should interact. In that case you need a different development plan. In the same way that we discovered function interfaces by encapsulation and generalization, we can discover class interfaces by data encapsulation.

Markov analysis, from Section 13.8, provides a good example. If you download my code from rank00146, you’ll see that it uses two global variables—rank00147 and rank00—that are read and written from several functions.

rank00149

Because these variables are global we can only run one analysis at a time. If we read two texts, their prefixes and suffixes would be added to the same data structures (which makes for some interesting generated text).

To run multiple analyses, and keep them separate, we can encapsulate the state of each analysis in an object. Here’s what that looks like:

rank00150

Next, we transform the functions into methods. For example, here’s rank00151:

rank00152

Transforming a program like this—changing the design without changing the function—is another example of refactoring (see Section 4.7).

This example suggests a development plan for designing objects and methods:

  1. Start by writing functions that read and write global variables (when necessary).
  2. Once you get the program working, look for associations between global variables and the functions that use them.
  3. Encapsulate related variables as attributes of an object.
  4. Transform the associated functions into methods of the new class.

Exercise 5  

Download my code from Section 13.8 (rank00153), and follow the steps described above to encapsulate the global variables as attributes of a new class called rank00. Solution: rank00155 (note the capital M).

18.11   Glossary

encode:

To represent one set of values using another set of values by constructing a mapping between them.

class attribute:

An attribute associated with a class object. Class attributes are defined inside a class definition but outside any method.

instance attribute:

An attribute associated with an instance of a class.

veneer:

A method or function that provides a different interface to another function without doing much computation.

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.”

IS-A relationship:

The relationship between a child class and its parent class.

HAS-A relationship:

The relationship between two classes where instances of one class contain references to instances of the other.

class diagram:

A diagram that shows the classes in a program and the relationships between them.

multiplicity:

A notation in a class diagram that shows, for a HAS-A relationship, how many references there are to instances of another class.

18.12   Exercises

Exercise 6  

The following are the possible hands in poker, in increasing order of value (and decreasing order of probability):

pair:

two cards with the same rank

two pair:

two pairs of cards with the same rank

three of a kind:

three cards with the same rank

straight:

five cards with ranks in sequence (aces can be high or low, so rank00156 is a straight and so is rank00157, but rank00158 is not.)

flush:

five cards with the same suit

full house:

three cards with one rank, two cards with another

four of a kind:

four cards with the same rank

straight flush:

five cards in sequence (as defined above) and with the same suit

The goal of these exercises is to estimate the probability of drawing these various hands.

  1. Download the following files from rank00159:

    rank001

    : A complete version of the rank, rank and rank classes in this chapter.

    rank00164

    : An incomplete implementation of a class that represents a poker hand, and some code that tests it.

  2. If you run rank00165, it deals seven 7-card poker hands and checks to see if any of them contains a flush. Read this code carefully before you go on.
  3. Add methods to rank00166 named rank0016, rank00168, etc. that return True or False according to whether or not the hand meets the relevant criteria. Your code should work correctly for “hands” that contain any number of cards (although 5 and 7 are the most common sizes).
  4. Write a method named rank0016 that figures out the highest-value classification for a hand and sets the rank0 attribute accordingly. For example, a 7-card hand might contain a flush and a pair; it should be labeled “flush”.
  5. When you are convinced that your classification methods are working, the next step is to estimate the probabilities of the various hands. Write a function in rank00171 that shuffles a deck of cards, divides it into hands, classifies the hands, and counts the number of times various classifications appear.
  6. Print a table of the classifications and their probabilities. Run your program with larger and larger numbers of hands until the output values converge to a reasonable degree of accuracy. Compare your results to the values at rank00172.

Solution: rank00173.

Exercise 7  

This exercise uses TurtleWorld from Chapter 4. You will write code that makes Turtles play tag. If you are not familiar with the rules of tag, see rank00174.

  1. Download rank00175 and run it. You should see a TurtleWorld with three Turtles. If you press the Run button, the Turtles wander at random.
  2. Read the code and make sure you understand how it works. The rank001 class inherits from rank00, which means that the rank00 methods ==, ==, == and == work on Wobblers.

    The rank method gets invoked by TurtleWorld. It invokes rank0, which turns the Turtle in the desired direction, rank00, which makes a random turn in proportion to the Turtle’s clumsiness, and rank, which moves forward a few pixels, depending on the Turtle’s speed.

  3. Create a file named rank00187. Import everything from rank001, then define a class named rank00 that inherits from rank001. Call rank00191 passing the rank00 class object as an argument.
  4. Add a rank0 method to rank00 to override the one in rank001. As a starting place, write a version that always points the Turtle toward the origin. Hint: use the math function rank0 and the Turtle attributes *, * and rank001.
  5. Modify rank0 so that the Turtles stay in bounds. For debugging, you might want to use the Step button, which invokes rank once on each Turtle.
  6. Modify rank0 so that each Turtle points toward its nearest neighbor. Hint: Turtles have an attribute, rank0, that is a reference to the TurtleWorld they live in, and the TurtleWorld has an attribute, rank002, that is a list of all Turtles in the world.
  7. Modify rank0 so the Turtles play tag. You can add methods to rank00 and you can override rank0 and rank0020, but you may not modify or override rank, rank00 or rank. Also, rank0 is allowed to change the heading of the Turtle but not the position.

    Adjust the rules and your rank0 method for good quality play; for example, it should be possible for the slow Turtle to tag the faster Turtles eventually.

Solution: rank00214.

Contribute

If you would like to make a contribution to support my books, you can use the button below. Thank you!

Pay what you want:

Small $1.00 USD Medium $5.00 USD Large $10.00 USD X-Large $20.00 USD XX-Large $50.00 USD

Are you using one of our books in a class?

We'd like to know about it. Please consider filling out this short survey.


------------------------------------------------------------------------

\

[插图缺失:thinkpython018.html]
[ [](thinkpython020.html)

---

← Chapter 18Chapter 20 →