Object-oriented programming permits us to organize our programs around the interactions of objects. A class provides the definition of the structure and behavior of the objects; each object is an instance of a class. Consequently, a typical program is a number of class definitions and a final main function. The main function creates the objects that will perform the job of the program.
This chapter presents the basic techniques of defining classes. In Semantics we define the semantics of objects and the classes which define their attributes (instance variables) and behaviors. In Class Definition: the class Statement we show the syntax for creating class definitions; we cover the use of objects in Creating and Using Objects.
Python has some system-defined names that classes can exploit to make them behave like built-in Python classes, a few of these are introduced in Special Method Names. We provide some examples in Some Examples. Perhaps the most important part of working with objects is how they collaborate to do useful work; we introduce this in Object Collaboration
Object-oriented programming focuses software design and implementation around the definitions of and interactions between individual objects. An object is said to encapsulate a state of being and a set of behaviors; it is both data and processing. Each instance of a class has individual copies of attributes which are tightly coupled with the class-wide operations. We can understand objects by looking at four features, adapted from [Rumbaugh91].
Python’s Implementation. A class is the Python-language definition of the features of individual objects: the names of the attributes and definitions of the operations.
Python implements the general notion of attribute as a dictionary of instance variables for an object. Python implements the general idea of an operation through a collection of methods or method functions of an object’s class.
Note that all Python objects are instances of some class. This includes something as simple as None or True.
>>> type(None)
<type 'NoneType'>
>>> type(True)
<type 'bool'>
Additionally, a class also constructs new object instances for us. Once we’ve defined the class, we can then use it as a kind of factory to create new objects.
Class Definition. Python class definitions require us to provide a number of things.
We must provide a distinct name to the class.
We list the superclasses from which a subclass inherits features.
In Python 2, classes should explicitly be defined as subclasses of object. In Python 3, this will be the default.
We have multiple inheritance available in Python. This differs from the single-inheritance approach used by languages like Java.
We provide method functions which define the operations for the class. We define the behavior of each object through its method functions.
Note that the attributes of each object are created by an initialization method function (named __init__()) when the object is created.
We can define attributes as part of the class definition. If we do, these will be class-level attributes, shared by all instances of the class.
Python provides the required mechanism for unique identity. You can use the id() function to interrogate the unique identifier for each object.
Technically, a class definition creates a new class object. This Python object contains the definitions of the method functions. Additionally, a class object can also own class-level variables; these are, in effect, attributes which are shared by each individual object of that class.
We can use this class object to create class instance objects. It’s the instances that do the real work of our programs. The class is simply a template or factory for creating the instance objects.
Duck Typing. Note that our instance variables are not a formal part of the class definition. This differs from Java or C++ where the instance variables must be statically declared.
Another consequence of Python’s dynamic nature is that polymorphism is based on simple matching of method names. This is distinct from languages like Java or C++ where polymorphism depends on inheritance and precise class (or interface) relationships.
Python’s approach to polymorphism is sometimes called duck typing: “if it quacks like a duck and walks like a duck it is a duck.” If several objects have the common method names, they are effectively polymorphic with respect to those methods.
We’re All Adults. The best programming practice is to treat each object as if the internal implementation details where completely opaque. Often, a class will have “public” methods that are a well-defined and supported interface, plus it will have “private” methods that are implementation details which can be changed without notice.
All other objects within an application should use only the methods and attributes that comprise the class interface. Some languages (like C++ or Java) have a formal distinction between interface and implementation. Python has a limited mechanism for making a distinction between the defined interface and the private implementation of a class.
The Python philosophy is sometimes called We’re All Adults: there’s little need for the (childish) formality between interface and implementation. Programmers can (and should) be trusted to read the documentation for a class and use the methods appropriately.
Python offers two simple technique sfor separating interface from implementation.
An Object’s Lifecycle. Each instance of every class has a lifecycle. The following is typical of most objects.
We create a class definition with a class statement. We provide the class name, the parent classes, and the method function definitions.
class name ( parent ) :
suite
The name is the name of the class, and this name is used to create new objects that are instances of the class. Traditionally, class names are capitalized and class elements (variables and methods) are not capitalized.
The parent is the name of the parent class, from which this class can inherit attributes and operations. For simple classes, we define the parent as object. Failing to list object as a parent class is not – strictly speaking – a problem; using object as the superclass does make a few of the built-in functions a little easier to use.
Important
Python 3.0
In Python 3.0, using object as a parent class will no longer be necessary.
In Python 2.6, however, it is highly recommended.
The suite is a series of function definitions, which define the class. All of these function definitions must have a first positional argument, self, which Python uses to identify each object’s unique attribute values.
The suite can also contain assignment statements which create instance variables and provide default values.
The suite typically begins with a comment string (often a triple-quoted string) that provides basic documentation on the class. This string becomes a special attribute, called __doc__. It is available via the help() function.
For example:
import random
class Die(object):
"""Simulate a 6-sided die."""
def roll( self ):
self.value= random.randint(1,6)
return self.value
def getValue( self ):
return self.value
When the roll() method of a Die object is executed, it sets that object’s instance variable, self.value, to a random value. Since the variable name, value, is qualified by the instance variable, self, the variable is local to the specific instance of the object.
If we omitted the self qualifier, Python would create a variable in the local namespace. The local namespace ceases to exist at the end of the method function execution, removing the local variables.
Once we have a class definition, we can make objects which are instances of that class. We do this by evaluating the class as if it were a function: for example, Die(). When we make one of these class calls, two things will happpen.
Let’s create two instances of our Die class.
>>> d1= Die()
>>> d2= Die()
>>> d1.roll(), d2.roll()
(6, 5)
>>> d1.getValue(), d2.getValue()
(6, 5)
>>> d1, d2
(<__main__.Die object at 0x607bb0>, <__main__.Die object at 0x607b10>)
>>> d1.roll(), d2.roll()
(1, 3)
>>> d1.value, d2.value
(1, 3)
There are several special methods that are essential to the implementation of a class. Each of them has a name that begins and ends with double underscores. These method names are used implicitly by Python. Section 3.3 of the Python Language Reference provides the complete list of these special method names.
We’ll look at the special method names in depth in Creating or Extending Data Types. Until then, we’ll look at a few special method names that are used heavily.
The __init__() method of a class is called by Python to initialize a newly-created object.
Note that The __init__() method can accept parameters, but does not return anything. It sets the internal state of the object.
Initializing an Object with __init__(). When you create an object, Python will both create the object and also call the object’s __init__() method. This method function can create the object’s instance variables and perform any other one-time initialization. There are, typically, two kinds of instance variables that are created by the __init__() method: variables based on parameters and variables that are independent of any parameters.
Here’s an example of a company description that might be suitable for evaluating stock performance. In this example, all of the instance variables (self.name, self.symbol, self.price) are based on parameters to the __init__() method.
class Company( object ):
def __init__( self, name, symbol, stockPrice ):
self.name= name
self.symbol= symbol
self.price= stockPrice
def valueOf( self, shares ):
return shares * self.price
When we create an instance of Company, we use code like this.
c1= Company( "General Electric", "GE", 30.125 )
This will provide three values to the parameters of __init__().
String value of an object with __str__(). The __str__() method function is called whenever an instance of a class needs to be converted to a string. Typically, this occus when we use the str() function on an object. Also, when we reference object in a print statement, the str() function is evaluated. Consider this definition of the class Card.
class Card( object ):
def __init__( self, rank, suit ):
self.rank= rank
self.suit= suit
self.points= rank
def hard( self ):
return self.points
def soft( self ):
return self.points
When we try to print an instance of the class, we get something like the following.
>>> c = Card( 3, "D" )
>>> c
<__main__.Card object at 0x607fb0>
>>> str(c)
'<__main__.Card object at 0x607fb0>'
This is the default behavior for the __str__() method. We can, however, override this with a function that produces a more useful-looking result.
def __str__( self ):
return "%2d%s" % (self.rank, self.suit)
Adding this method function converts the current value of the die to a string and returns this. Now we get something much more useful.
>>> d = Card( 4, "D" )
>>> d
<__main__.Card object at 0x607ed0>
>>> str(d)
' 4D'
>>> print d
4D
Representation details with __repr__(). While the __str__() method produces a human-readable string, we sometimes want the nitty-gritty details. The __repr__() method function is evaluated whenever an instance of a class must have its detailed representation shown. This is usually done in response to evaluating the repr() function. Examples include the following:
>>> repr(c)
'<__main__.Card object at 0x607fb0>'
If we would like to produce a more useful result, we can override the __repr__() function. The objective is to produce a piece of Python programming that would reconstruct the original object.
def __repr__( self ):
return "Card(%d,%r)" % (self.rank,self.suit)
We use __repr__() to produce a clear definition of how to recreate the given object.
>>> c = Card( 5, "D" )
>>> repr(c)
"Card(5,'D')"
Special Attribute Names. In addition to the special method names, each object has a number of special attributes. These are documented in section 2.3.10 of the Python Library Reference.
We’ll look at just a few, including __dict__, __class__ and __doc__.
| __dict__: | The attribute variables of a class instance are kept in a special dictionary object named __dict__. As a consequence, when you say self.attribute= value, this has almost identical meaning to self.__dict__['attribute']= value. Combined with the % string formatting operation, this feature is handy for writing __str__() and __repr__() functions. def __str__( self ):
return "%(rank)2s%(suit)s" % self.__dict__
def __repr__( self ):
return "Card(%(rank)r,%(suit)r)" % self.__dict__
|
|---|---|
| __class__: | This is the class to which the object belongs. |
| __doc__: | The docstring from the class definition. |
We’ll look at two examples of class definitions. In the both examples, we’ll write a script which defines a class and then uses the class.
die.py
#!/usr/bin/env python
"""Define a Die and simulate rolling it a dozen times."""
import random
class Die(object):
"""Simulate a generic die."""
def __init__( self ):
self.sides= 6
self.roll()
def roll( self ):
"""roll() -> number
Updates the die with a random roll."""
self.value= 1+random.randrange(self.sides)
return self.value
def getValue( self ):
"""getValue() -> number
Return the last value set by roll()."""
retur self.value
def main():
d1, d2 = Die(), Die()
for n in range(12):
print d1.roll(), d2.roll()
main()
The __init__() method can accept arguments. This allows us to correctly initialize an object while creating it. For example:
point.py
#!/usr/bin/env python
"""Define a geometric point and a few common manipulations."""
class Point( object ):
"""A 2-D geometric point."""
def __init__( self, x, y ):
"""Create a point at (x,y)."""
self.x, self.y = x, y
def offset( self, xo, yo ):
"""Offset the point by xo parallel to the x-axis
and yo parallel to the y-axis."""
self.x += xo
self.y += yo
def offset2( self, val ):
"""Offset the point by val parallel to both axes."""
self.offset( val, val )
def __str__( self ):
"""Return a pleasant representation."""
return "(%g,%g)" % ( self.x, self.y )
def main():
obj1_corner = Point( 12, 24 )
obj2_corner = Point( 8, 16 )
obj1_corner.offset( -4, -8 )
print obj1_corner
print obj2_corner
main()
The self Variable. These examples should drive home the ubiquirty of the self variable. Within a class, we must be sure to use self. in front of the method function names as well as attribute names. For example, our offset2() function accepts a single value and calls the object’s offset() function using self.offset( val, val ).
The self variable is so important, we’ll highlight it.
Important
The self variable
In Python, the self qualifier is simply required all the time.
Programmers experienced in Java or C++ may object to seeing the explicit self. in front of all variable names and method function names. In Java and C++, there is a this. qualifier which is assumed by the compiler. Sometimes this qualifier is required to disambiguate names, other times the compiler can work out what you meant.
Some programmers complain that self is too much typing, and use another variable name like my . This is unusual, generally described as a bad policy, but it is not unheard of.
An object is a namespace; it contains the attributes. We can call the attributes instance variables to distinguish them from global variables and free variables.
| Instance Variables: | |
|---|---|
These are part of an object’s namespace. Within the method functions of a class, these variables are qualified by self. Outside the method functions of the class, these variables are qualified by the object’s name. In die.py, the main() function would refer to d1.value to get the value attribute of object d1. |
|
| Global Variables: | |
Global variables are pare of a special global namespace. The global statement creates the variable name in the global namespace instead of the local namespace. See The global Statement for more information. While it’s easy to refer to global variables, it’s not as easy to create them. |
|
| Free Variables: | Within a method function, a variable that is not qualified by self., nor marked by global is a free variable. Python checks the local namespace, then the global namespace for this variable. This ambiguity is, generally, not a good idea. |
Object-oriented programming helps us by encapsulating data and processing into a tidy class definition. This encapsulation assures us that our data is processed correctly. It also helps us understand what a program does by allowing us to ignore the details of an object’s implementation.
When we combine multiple objects into a collaboration, we exploit the power of ecapsulation. We’ll look at a simple example of creating a composite object, which has a number of detailed objects inside it.
Defining Collaboration. Defining a collaboration means that we are creating a class which depends on one or more other classes. Here’s a new class, Dice, which uses instances of our Die class. We can now work with a Dice collection, and not worry about the details of the individual Die objects.
dice.py - part 1
class Dice( object ):
"""Simulate a pair of dice."""
def __init__( self ):
"Create the two Die objects."
self.myDice = ( Die(), Die() )
def roll( self ):
"Return a random roll of the dice."
for d in self.myDice:
d.roll()
def getTotal( self ):
"Return the total of two dice."
t= 0
for d in self.myDice:
t += d.getValue()
return t
def getTuple( self ):
"Return a tuple of the dice values."
return tuple( [d.getValue() for d in self.myDice] )
def hardways( self ):
"Return True if this is a hardways roll."
return self.myDice[0].getValue() == self.myDice[1].getValue()
The getTotal() and getTuple() methods return basic attribute information about the state of the object. These kinds of methods are often called getters because their names start with “get”.
Collaborating Objects. The following function exercises an instance this class to roll a Dice object a dozen times and print the results.
def test2():
x= Dice()
for i in range(12):
x.roll()
print x.getTotal(), x.getTuple()
This function creates an instance of Dice, called x. It then enters a loop to perform a suite of statements 12 times. The suite of statements first manipulates the Dice object using its roll() method. Then it accesses the Dice object using getTotal() and getTuple() method.
Here’s another function which uses a Dice object. This function rolls the dice 1000 times, and counts the number of hardways rolls as compared with the number of other rolls. The fraction of rolls which are hardways is ideally 1/6, 16.6%.
def test3():
x= Dice()
hard= 0
soft= 0
for i in range(1000):
x.roll()
if x.hardways(): hard += 1
else: soft += 1
print hard/1000., soft/1000.
Independence. One point of object collaboration is to allow us to modify one class definition without breaking the entire program. As long as we make changes to Die that don’t change the interface that Die uses, we can alter the implementation of Die all we want. Similarly, we can change the implementation of Dice, as long as the basic set of methods are still present, we are free to provide any alternative implementation we choose.
We can, for example, rework the definition of Die confident that we won’t disturb Dice or the functions that use Dice ( test2() and test3() ). Let’s change the way it represents the value rolled on the die.
Here’s an alternate
implemetation of Die. In this case, the private instance variable, value,
will have a value in the range
. When getValue()
adds 1, the value is in the usual range for a single die,
.
class Die(object):
"""Simulate a 6-sided die."""
def __init__( self ):
self.roll()
def roll( self ):
self.value= random.randint(0,5)
retuen self.value
def getValue( self ):
return 1+self.value
Since this version of Die has the same interface as other versions of Die in this chapter, it is polymorphic with them. There could be performance differences, depending on the performance of random.randint() and random.randrange() functions. Since random.randint() has a slightly simpler definition, it may process more quickly.
Similarly, we can replace Die with the following alternative. Depending on the performance of choice(), this may be faster or slower than other versions of Die.
class Die(object):
"""Simulate a 6-sided die."""
def __init__( self ):
self.domain= range(1,7)
def roll( self ):
self.value= random.choice(self.domain)
return self.value
def getValue( self ):
return self.value
These exercises are considerably more sophisticated then the exercises in previous parts. Each of these sections describes a small project that requires you to create a number of distinct classes which must collaborate to produce a useful result.
When we document a method function, we don’t mention the self variable. This is required when you actually write the class definition. However, we don’t show it in the documentation.
A Block of stock has a number of attributes, including a purchase price, purchase date, and number of shares. Commonly, methods are needed to compute the total spent to buy the stock, and the current value of the stock. A Position is the current ownership of a company reflected by all of the blocks of stock. A Portfolio is a collection of Positions ; it has methods to compute the total value of all Blocks of stock.
When we purchase stocks a little at a time, each Block has a different price. We want to compute the total value of the entire set of Block s, plus an average purchase price for the set of Block s.
The StockBlock class. First, define a StockBlock class which has the purchase date, price per share and number of shares. Here are the method functions this class should have.
Populate the individual fields of date, price and number of shares. This is information which is part of the Position, made up of individual blocks.
Don’t include the company name or ticker symbol.
Use salePrice to compute the return on investment as (sale value - purchase value) ÷ purchase value.
Note that this is not the annualized ROI. We’ll address this issue below.
We can load a simple database with a piece of code the looks like the following. The first statement will create a sequence with four blocks of stock. We chose variable name that would remind us that the ticker symbols for all four is ‘GM’. The second statement will create another sequence with four blocks.
blocksGM = [
StockBlock( purchDate='25-Jan-2001', purchPrice=44.89, shares=17 ),
StockBlock( purchDate='25-Apr-2001', purchPrice=46.12, shares=17 ),
StockBlock( purchDate='25-Jul-2001', purchPrice=52.79, shares=15 ),
StockBlock( purchDate='25-Oct-2001', purchPrice=37.73, shares=21 ),
]
blocksEK = [
StockBlock( purchDate='25-Jan-2001', purchPrice=35.86, shares=22 ),
StockBlock( purchDate='25-Apr-2001', purchPrice=37.66, shares=21 ),
StockBlock( purchDate='25-Jul-2001', purchPrice=38.57, shares=20 ),
StockBlock( purchDate='25-Oct-2001', purchPrice=27.61, shares=28 ),
]
The Position class. A separate class, Position, will have an the name, symbol and a sequence of StockBlocks for a given company. Here are some of the method functions this class should have.
The getROI() method requires a salePrice; it computes the return on investment as (sale value - purchase value) ÷ purchase value. This is an ROI based on an overall yield.
Note that this is not the annualized ROI. We’ll address this issue below.
We can create our Position objects with the following kind of initializer. This creates a sequence of three individual Position objects; one has a sequence of GM blocks, one has a sequence of EK blocks and the third has a single CAT block.
portfolio= [
Position( "General Motors", "GM", blocksGM ),
Position( "Eastman Kodak", "EK", blocksEK )
Position( "Caterpillar", "CAT",
[ StockBlock( purchDate='25-Oct-2001',
purchPrice=42.84, shares=18 ) ] )
]
An Analysis Program. You can now write a main program that writes some simple reports on each Position object in the portfolio. One report should display the individual blocks purchased, and the purchase value of the block. This requires iterating through the Positions in the portfolio, and then delegating the detailed reporting to the individual StockBlocks within each Position.
Another report should summarize each position with the symbol, the total number of shares and the total value of the stock purchased. The overall average price paid is the total value divided by the total number of shares.
In addition to the collection of StockBlock objects that make up a Position, one additional piece of information that is useful is the current trading price for the Position. First, add a currentPrice attribute, and a method to set that attribute. Then, add a getCurrentValue() method which computes a sum of the getSaleValue() method of each StockBlock, using the trading price of the Position.
Annualized Return on Investment. In order to compare portfolios, we might want to compute an annualized ROI. This is ROI as if the stock were held for eactly one year. In this case, since each block has different ownership period, the annualized ROI of each block has to be computed. Then we return an average of each annual ROI weighted by the sale value.
The annualization requires computing the duration of stock ownership. This requires use of the time module. We’ll cover that in depth in Dates and Times: the time and datetime Modules. The essential feature, however, is to parse the date string to create a time object and then get the number of days between two time objects. Here’s a code snippet that does most of what we want.
>>> import datetime
>>> dt1="25-JAN-2001"
>>> tm1= datetime.datetime.strptime( dt1, "%d-%b-%Y" ).date()
>>> tm1
datetime.date(2001, 1, 25)
>>> dt2= "25-JUN-2001"
>>> tm2= datetime.datetime.strptime( dt2, "%d-%b-%Y" ).date()
>>> tm2
datetime.date(2001, 6, 25)
>>> tm2-tm1
datetime.timedelta(151)
>>> (tm2-tm1).days/365.25
0.4134154688569473
In this example, tm1 and tm2 are datetime.date objects with details parsed from the date string by datetime.datetime.strptime().
We can subtract two datetime.date objects and get a datetime.timedelta that has the number of days between the two dates. A timedelta can be used on datetime.datetime objects to get days and seconds between two date-time stamps.
In this case, there are 151 days between the two dates. When we divide by the number of days in a year (including leap days) we get the fraction of a year between the two dates.
Once we’ve added the necessary support to StockBlock, we can then add to Position.
The Surface Air Consumption Rate is used by SCUBA divers to predict air used at a particular depth. If we have a sequence of Dive objects with the details of each dive, we can do some simple calculations to get averages and ranges for our air consumption rate.
For each dive, we convert our air consumption at that dive’s depth to a normalized air consumption at the surface. Given depth (in feet), d, tarting tank pressure (psi), s, final tank pressure (psi), f, and time (in minutes) of t, the SACR, c, is given by the following formula.

Typically, you will average the SACR over a number of similar dives.
The Dive Class. You will want to create a Dive class that contains attributes which include start pressure, finish pressure, time and depth. Typical values are a starting pressure of 3000, ending pressure of 700, depth of 30 to 80 feet and times of 30 minutes (at 80 feet) to 60 minutes (at 30 feet). SACR’s are typically between 10 and 20. Your Dive class should have a method named Dive.getSACR() which returns the SACR for that dive.
To make life a little simpler putting the data in, we’ll treat time as string of HH:MM, and use string functions to pick this apart into hours and minutes. We can save this as tuple of two intgers: hours and minutes. To compute the duration of a dive, we need to normalize our times to minutes past midnight, by doing hh*60+mm. Once we have our times in minutes past midnight, we can easily subtract to get the number of minutes of duration for the dive. You’ll want to create a method function Dive.getDuration() to do just this computation for each dive.
The DiveLog Class. We’ll want to initialize our dive log as follows:
log = [
Dive( start=3100, finish=1300, in="11:52", out="12:45", depth=35 ),
Dive( start=2700, finish=1000, in="11:16", out="12:06", depth=40 ),
Dive( start=2800, finish=1200, in="11:26", out="12:06", depth=60 ),
Dive( start=2800, finish=1150, in="11:54", out="12:16", depth=95 ),
]
Rather than use a simple sequence of Dive objects, you can create a DiveLog class which has a sequence of Dive objects plus a DiveLog.getAvgSACR() method. Your DiveLog method can be initiatlized with a sequence of dives, and can have an append method to put another dive into the sequence.
Exercising the Dive and DiveLog Classes. Here’s how the final application could look. Note that we’re using an arbitrary number of argument values to the Dive.__init__() function, therefore, it has to be declared as def __init__( self, *listOfDives )
log= DiveLog(
Dive( start=3100, finish=1300, in="11:52", out="12:45", depth=35 ),
Dive( start=2700, finish=1000, in="11:16", out="12:06", depth=40 ),
Dive( start=2800, finish=1200, in="11:26", out="12:06", depth=60 ),
Dive( start=2800, finish=1150, in="11:54", out="12:16", depth=95 ),
)
print log.getAvgSACR()
for d in log.dives:
print d
If we want to simulate multi-dice games like Yacht, Kismet, Yatzee, Zilch, Zork, Greed or Ten Thousand, we’ll need a collection that holds more than two dice. The most common configuration is a five-dice collection. In order to be flexible, we’ll need to define a Dice object which will use a tuple, list or Set of individual Die instances. Since the number of dice in a game rarely varies, we can also use a FrozenSet.
Once you have a Dice class which can hold a collection of dice, you can gather some statistics on various multi-dice games. These games fall into two types. In both cases, the player’s turn starts with rolling all the dice, the player can then elect to re-roll or preserve selected dice.
Scorecard Games. In Yacht, Kismet and Yatzee, five dice are used.
The first step in a player’s turn is a roll of all five dice. This can be followed by up to two additional steps in which the player decides which dice to preserve and which dice to roll.
The player is trying to make a scoring hand. A typical scorecard for these games lists a dozen or more “hands” with associated point values. Each turn must fill in one line of the scorecard; if the dice match a hand which has not been scored, the player enters a score. If a turn does not result in a hand that matches an unscored hand, then a score of zero is entered.
The game ends when the scorecard is filled.
A typical score card has spaces for 3-of-a-kinds (1 through 6) worth the sum of the scoring dice; a four-of-a-kind and full house (3 of a kind and a pair) worth the sum of the dice; a small straight (4 in a row) worth 25 points; a long straight (all 5 in a row) worth 30 points; a “chance” (sum of the dice), plus 5-of-a-kind worth 50 points.
Some games award a 35 point bonus for getting all six 3-of-a-kind scores.
Point Games. In Zilch, Zork, Green or Ten Thousand, five dice are typical, but there are some variations.
The player in this game has no limit on the number of steps in their turn. The first step is to roll all the dice and determine a score. Their turn ends when they perceive the risk of another step to be too high, or they’ve made a roll which gives them a score of zero (or zilch) for the turn.
Typically, if the newly rolled dice are non-scoring, their turn is over with a score of zero. At each step, the player is looking at newly rolled dice which improve their score.
The game ends when someone has a score of 10,000.
A tyipcal set of rules awards a straight 1000. Three-of-a-kind scores 100 × the die’s value (except three ones is 1000 points). After removing any three-of-a-kinds, each die showing 1 scores 100, each die showing 5 scores 50. Additionally, some folks will score 1000 × the die’s value for five-of-a-kind.
Our MultiDice class will be based on the example of Dice in this chapter. In addition to a collection of Die instances (a sequence, Set or FrozenSet), the class will have the following methods.
This method will score the hand, returning a list of two-tuples. Each two-tuple will have the name of the hand and the point value for the particular game. In some cases, there will be multiple ways to score a hand, and the list will reflect all possible scorings of the hand, in order from most valuable to least valuable. In other cases, the list will only have a single element.
It isn’t practical to attempt to write a universal MultiDice class that covers all variations of dice games. Rather than write a gigantic does-everything class, the better policy is to create a family of classes that build on each other using inheritance. We’ll look at this in Inheritance.
For this exercise, you’ll have to pick one game and compute the score for that particular game. Later, we’ll see how to create an inheritance hierarchy that can cover all of these multi-dice games.
For the scorecard games (Yacht, Kismet, Yatzee), we want to know if this set of dice matches any of the scorecard hands. In many cases, a set of dice can match a number of potential hands. A hand of all five dice showing the same value (e.g, a 6) is matches the sixes, three of a kind, four of a kind, five of a kind and wild-card rows on most game score-sheets. A sequence of five dice will match both a long straight and a short straight.
Common Scoring Methods. No matter which family of games you elect to pursue, you’ll need some common method functions to help score a hand. The following methods will help to evaluate a set of dice to see which hand it might be.
Given a numeric value, partition the dice into two sets: the dice which have a value that matches the given Die, and the remaining Die which do not match the value.
Return both sets.
This functions will evaluate MultiDice.matchDie() for each Die in the collection. If any given Die has a matchDie() with a match set that contains n matching dice, the hand as a whole matches the template.
This method can be used for 3 of a kind, 4 of a kind and 5 of a kind.
This method returns the matching dice or None if the hand did not have N-of-a-kind. The matching dice set can then be summed (for the hands that count only scoring dice) or the entire set of dice can be summed (for the hands that count all dice.)
This function must establish that four of the five dice form a sequence of values. There are a variety of ways of approaching this; it is actually a challenging algorithm.
Here’s one approach: create a sequence of dice, and sort them into order. Look for an ascending sequence with one “irrelevant” die in it. This irrelevant die must be either (a) a gap at the start of the sequence (1, 3, 4, 5, 6) or (b) a gap at the end of the sequence (1, 2, 3, 4, 6 ) or (c) a single duplicated value (1, 2, 2, 3, 4, 5) within the sequence.
Scoring Yacht, Kismet and Yatzee. For scoring these hands, your overall score() method function will step through the candidate hands in a specific order. Generally, you’ll want to check for fiveOfAKind() first, since fourOfAKind() and threeOfAKind() will also be true for this hand. Similarly, you’ll have to check for largeStraight() before smallStraight().
Your score() method will evaluate each of the scoring methods. If the method matches, your method will append a two-tuple with the name and points to the list of scores.
Scoring Zilch, Zork and 10,000. A scoring hand’s description can be relatively complex in these games. For example, you may have a hand with three 2’s, a 1 and a 5. This is worth 350. The description has two parts: the three-of-a-kind and the extra 1’s and 5’s. Here are the steps for scoring this game.
Evaluate the largeStraight() method. If the hand matches, then return a list with an appropriate 2-tuple.
If you’re building a game variation where five of a kind is a scoring hand, then evaluate fiveOfAKind(). If the hand matches, then return a list with an appropriate 2-tuple.
Three of a kind. Evaluate the threeOfAKind() method. This will create the first part of the hand’s description.
1-5’s. Any non-matching dice from the threeOfAKind() test are then checked using matchValue() to see if there are 1’s or 5’s. If there are any, this is the second part of the hand’s description. If there are none, then there’s no second part of the description.
The final step is to assemble the description. There are four cases: nothing, three-of-a-kind with no 1-5’s, 1-5’s with no three-of-a-kind, and three-of-a-kind plus 1-5’s.
In the nothing case, this is a non-scoring hand. In the other three cases, it is a scoring hand, and you can assign point values to each part of the description.
Exercising The Dice. Your main script should create a MultiDice object, execute an initial roll and score the result. It should then pick three dice to re-roll and score the result. Finally, it should pick one die, re-roll this die and score the result. This doesn’t make sophisticated strategic decisions, but it does exercise your MultiDice object thoroughly.
When playing a scorecard game, the list of potential hands is examined to fill in another line on the scorecard. When playing a points game, each throw must result in a higher score than the previous throw or the turn is over.
A Rational number is a ratio of two integers. Examples include
. We can do arithmetic operations on rational numbers.
We can display them as proper fractions (3 1/7), improper
fractions (22/7) or decimal expansions (3.1428571428571428).
The essence of this class is to save a rational number and perform arithmetic operations on this number or between two rational numbers.
It’s also important to note that all arithmetic operations will create a new Rational number computed from the inputs. This makes our object mostly immutable, which is the expected behavior for numbers.
We’ll start by defining methods to add and multiply two rational values. Later, we’ll delve into the additional methods you’d need to write to create a robust, complete implementation.
You’ll write __add__() and __mul__() methods that will perform their processing with two Rational values: self and other. In both cases, the variable other has to be another Rational number instance.
You can check this with a simple assert statement. assert type(self) == type(other). Generally, however, this extra checking isn’t essential. If you try to use a non-Rational number, you’ll get an AttributeError exception when you try to access the various parts of the Rational number.
Design. A Rational class has two attributes: the numerator and the denominator of the value. These are both integers. Here are the various methods you should create.
Accept the numerator and denominator values. It can have a default value for the denominator of 1. This gives us two constructors: Rational(2,3) and Rational(4). The first creates the fraction 2/3. The second creates the fraction 4/1.
This method should call the Rational._reduce() method to assure that the fraction is properly reduced. For example, Rational(8,4) should automatically reduce to a numerator of 2 and a denominator of 1.
Return a nice string representation for the rational number, typically as an improper fraction. This gives you the most direct view of your Rational number.
You should provide a separate method to provide a proper fraction string with a whole number and a fraction. This other method would do additional processing to extract a whole name and remainder.
Create and return a new Rational number that is the sum of self and other.
Sum of
where
and
are Rational
numbers,
and
.

Example: 3/5 + 7/11 = (33 + 35)/55 = 71/55.
Create and returns a new Rational number that is the product of self and other.
This new fraction that has a numerator of (self.numerator × other.numerator), and a denominator of ( self.denominator × other.denominator ).
Product of
where
and
are Rational
numbers,
and
.

Example: 3/5 × 7/11 = 21/55.
Reduce this Rational number by removing the greatest common divisor from the numerator and the denominator. This is called by Rational.__init__(), Rational.__add__(), Rational.__mul__(), assure that all fractions are reduced.
Reduce is a two-step operation. First, find the greatest common divisor between the numerator and denominator. Second, divide both by this divisor. For example 8/4 has a GCD of 4, and reduces to 2/1.
The Greatest Common Divisor (GCD) algorithm is given in Greatest Common Divisor and Greatest Common Divisor.
Note that we’ve given this method a name that begins with _ to make it private. It’s a “mutator” and updates the object, something that may violate the expectation of immutability.
Standard playing cards have a rank (ace, two through ten, jack, queen and king) and suit (clubs, diamonds, hearts, spades). These form a nifty Card object with two simple attributes. We can add a few generally useful functions.
Here are the methods for your Card class.
Dealing and Decks. Card s are dealt from a Deck; a collection of Card s that includes some methods for shuffling and dealing. Here are the methods that comprise a Deck.
This method needs to do two things. First it must shuffle the cards. The random module has a shuffle() function which does precisely this.
Second, it should deal the shuffled cards. Dealing is best done with a generator method function. The deal() method function should have a simple for-loop that yields each individual Card; this can be used by a client application to generate hands. The presence of the yield statement will make this method function usable by a for statement in a client application script.
Basic Testing. You should do some basic tests of your Card objects to be sure that they respond appropriately to comparison operations. For example,
>>> x1= Card(11,"C")
>>> x1
JC
>>> x2= Card(12,"D")
>>> x1 < x2
True
You can write a simple test script which can the do the following to deal Cards from a Deck. In this example, the variable dealer will be the iterator object that the for statement uses internally.
d= Deck()
dealer= d.deal()
c1= dealer.next()
c2= dealer.next()
Hands. Many card games involve collecting a hand of cards. A Hand is a collection of Card s plus some addition methods to score the hand in way that’s appropriate to the given game. We have a number of collection classes that we can use: list, tuple, dictionary and set.
In Blackjack, the Hand will have two Cards assigned initially; it will then be scored. After this, the player must choose among accepting another card (a hit), using this hand against the dealer (standing), doubling the bet and taking one more card, or splitting the hand into two hands. Ignoring the split option for now, it’s clear that the collection of Cards has to grow and then get scored again. What are the pros and cons of list, tuple, set and dictionary for a hand which grows?
When considering Poker, we have to contend with the innumerable variations on poker; we’ll look at simple five-card draw poker. Games like seven-card stud require you to score potential hands given only two cards, and as many as 21 alternative five-card hands made from seven cards. Texas Hold-Em has from three to five common cards plus two private cards, making the scoring rather complex. For five-card draw, the Hand will have five cards assigned initially, and it will be scored. Then some cards can be removed and replaced, and the hand scored again. Since a valid poker hand is an ascending sequence of cards, called a straight, it is handy to sort the collection of cards. What are the pros and cons of list, tuple, set and dictionary?
For Blackjack, we’ll extend our Card class to score hands. When used in Blackjack, a Card has a point value in addition to a rank and suit. Aces are either 1 or 11; two through ten are worth 2-10; the face cards are all worth 10 points. When an ace is counted as 1 point, the total is called the hard total. When an ace is counted as 11 points, the total is called a soft total.
You can add a point attribute to your card class. This can be set as part of __init__() processing. In that case, the following methods simple return the point value.
As an alternative, you can compute the point value each time it is requested. This has the obvious disadvantage of being slower. However, it is considerably simpler to add methods to a class without revising the existing __init__() method.
Here are the methods you’ll need to add to your Card class in order to handle Blackjack hands.
As a teaser for the next chapter, we’ll note that these methods should be part of a Blackjack-specific subclass of the generic Card class. For now, however, we’ll just update the Card class definition.When we look at inheritance in Inheritance, we’ll see that a class hierarchy can be simpler than the if-statements in the getHardValue() and getSoftValue() methods.
Scoring Blackjack Hands. The objective of Blackjack is to accumulate a Hand with a total point value that is less than or equal to 21. Since an ace can count as 1 or 11, it’s clear that only one of the aces in a hand can have a value of 11, and any other aces must have a value of 1.
Each Card produces a hard and soft point total. The Hand as a whole also has hard and soft point totals. Often, both hard and soft total are equal. When there is an ace, however, the hard and soft totals for the hand will be different.
We have to look at two cases.
The Hand class has a collection of Cards, usually a sequence, but a Set will also work. Here are the methods of the Hand class.
Check for any Card with a different hard and soft point value (this will be an ace). The first such card, if found, is the softSet. The remaining cards form the hardSet.
It’s entirely possible that the softSet will be empty. It’s also entirely possible that there are multiple cards which could be part of the softSet.
The value of this function is the total of the hard values for all of the cards in the hardSet plus the soft value of the card in the softSet.
Exercising Card, Deck and Hand. Once you have the Card, Deck and Hand classes, you can exercise these with a simple function to play one hand of blackjack. This program will create a Deck and a Hand; it will deal two Card s into the Hand. While the Hand ‘s total is soft 16 or less, it will add Cards. Finally, it will print the resulting Hand.
There are two sets of rules for how to fill a Hand. The dealer is tightly constrained, but players are more free to make their own decisions. Note that the player’s hands which go bust are settled immediately, irrespective of what happens to the dealer. On the other hand, the player’s hands which total 21 aren’t resolved until the dealer finishes taking cards.
The dealer must add cards to a hand with a soft 16 or less. If the dealer has a soft total between 17 and 21, they stop. If the dealer has a soft total which is over 21, but a hard total of 16 or less, they will take cards. If the dealer has a hard total of 17 or more, they will stop.
A player may add cards freely until their hard total is 21 or more. Typically, a player will stop at a soft 21; other than that, almost anything is possible.
Additional Plays. We’ve avoided discussing the options to split a hand or double the bet. These are more advanced topics that don’t have much bearing on the basics of defining Card, Deck and Hand. Splitting simply creates additional Hands. Doubling down changes the bet and gets just one additional card.
We’ll extend the Card class we created in Playing Cards and Decks to score hands in Poker, where both the rank and suit are used to determine the hand that is held.
Poker hands are ranked in the following order, from most desirable (and least likely) down to least desirable (and all too common).
Note that a straight flush is both a straight and a flush; four of a kind is also two pair as well as one pair; a full house is also two pair, as well as a one pair. It is important, then, to evaluate poker hands in decreasing order of importance in order to find the best hand possible.
In order to distinguish between two straights or two full-houses, it is important to also record the highest scoring card. A straight with a high card of a Queen, beats a straight with a high card of a 10. Similarly, a full house or two pair is described as “queens over threes”, meaning there are three queens and two threes comprising the hand. We’ll need a numeric ranking that includes the hand’s rank from 9 down to 1, plus the cards in order of “importance” to the scoring of the hand.
The importance of a card depends on the hand. For a straight or straight flush, the most important card is the highest-ranking card. For a full house, the most important cards are the three-of-a kind cards, followed by the pair of cards. For two pair, however, the most important cards are the high-ranking pair, followed by the low-ranking pair. This allows us to compare “two pair 10’s and 4’s” against “two pair 10’s and 9’s”. Both hands have a pair of 10’s, meaning we need to look at the third card in order of importance to determine the winner.
Scoring Poker Hands. The Hand class should look like the following. This definition provides a number of methods to check for straight, flush and the patterns of matching cards. These functions are used by the score() method, shown below.
class PokerHand:
def __init__( self, cards ):
self.cards= cards
self.rankCount= {}
def straight( self ): all in sequence
def straight( self ): all of one suit
def matches( self ): tuple with counts of each rank in the hand
def sortByRank( self ): sort into rank order
def sortByMatch( self ): sort into order by count of each rank, then rank
This function to score a hand checks each of the poker hand rules in descending order.
def score( self ):
if self.straight() and self.flush():
self.sortByRank()
return 9
elif self.matches() == ( 4, 1 ):
self.sortByMatch()
return 8
elif self.matches() == ( 3, 2 ):
self.sortByMatch()
return 7
elif self.flush():
self.sortByRank()
return 6
elif self.straight():
self.sortByRank()
return 5
elif self.matches() == ( 3, 1, 1 ):
self.sortByMatch()
return 4
elif self.matches() == ( 2, 2, 1 ):
self.sortByMatchAndRank()
return 3
elif self.matches() == ( 2, 1, 1, 1 ):
self.sortByMatch()
return 2
else:
self.sortByRank()
return 1
You’ll need to add the following methods to the PokerHand class.
Exercising Card, Deck and Hand. Once you have the Card, Deck and Hand classes, you can exercise these with a simple function to play one hand of poker. This program will create a Deck and a Hand; it will deal five Cards into the Hand. It can score the hand. It can replace from zero to three cards and score the resulting hand.