Classes

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

Semantics

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

  • Identity. Each object is unique and is distinguishable from all other objects. In the real world, two otherwise identical coffee cups can be distiguished as distinct objects. For example, they occupy different locations on our desk. In the world of a computer’s memory, objects could be identified by their address, which would make them unique.
  • Classification. This is sometimes called Encapsulation. Objects with the same attributes and behavior belong to a common class. Each individual object has unique attribute values. We saw this when we looked at the various collection classes. Two different list objects have the same general structure, and the same behavior. Both lists respond to append(), pop(), and all of the other methods of a list. However, each list object has a unique sequence of values.
  • Inheritance. A class can inherit methods from a parent class, reusing common features. A superclass is more general, a subclass overrides superclass features, and is more specific. With the built-in Python classes, we’ve looked at the ways in which all immutable sequences are alike.
  • Polymorphism. A general operation can have variant implementation methods, depending on the class of the object. We saw this when we noted that almost every class on Python has a + operation. Between two floating-point numbers the + operation adds the numbers, between two lists, however, the + operation concatenates the lists.

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.

  • We can use a leading _ on an instance variable or method function name to make it more-or-less private to the class.
  • We can use properties or descriptors to create more sophisticated protocols for accessing instance variables. We’ll wait until Attributes, Properties and Descriptors to cover these more advanced techniques.

An Object’s Lifecycle. Each instance of every class has a lifecycle. The following is typical of most objects.

  1. Definition. The class definition is read by the Python interpreter (or it is a builtin class). Class definitions are created by the class statement. Examples of built-in classes include file, str, int, etc.
  2. Construction. An instance of the class is constructed: Python allocates the namespace for the object’s instance variables and associating the object with the class definition. The __init__() method is executed to initialize any attributes of the newly created instance.
  3. Access and Manipulation. The instance’s methods are called (similar to function calls we covered in Functions), by client objects or the main script. There is a considerable amount of collaboration among objects in most programs. Methods that report on the state of the object are sometimes called accessors; methods that change the state of the object are sometimes called mutators or manipulators.
  4. Garbage Collection. Eventually, there are no more references to this instance. Typically, the variable with an object reference was part of the body of a function that finished, the namespace is dropped, and the variables no longer exist. Python detects this, and removes the referenced object from memory, freeing up the storage for subsequent reuse. This freeing of memory is termed garbage collection, and happens automatically. See Garbage Collection for more information.

Class Definition: the class Statement

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
  1. We imported the random module to provide the random number generator.
  2. We defined the simple class named Die, and claimed it as a subclass of object. The indented suite contains three elements.
    • The docstring, which provides a simple definition of the real-world thing that this class represents. As with functions, the docstring is retrieved with the help() function.
    • We defined a method function named roll(). This method function has the mandatory positional parameter, self, which is used to qualifty the instance variables. The self variable is a namespace for all of the attributes and methods of this object.
    • We defined a method function named getValue(). This function will return the last value rolled.

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.

Creating and Using Objects

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.

  • A new object is created. This object has a reference to its class definition.
  • The object’s initializer method, __init__(), is called. We’ll look at how you define this method function in th next section.

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)
  1. We use the Die class object to create two variables, d1, and d2; both are new objects, instances of Die.
  2. We evaluate the roll() method of d1; we also evaluate the roll() method of d2. Each of these calls sets the object’s value variable to a unique, random number. There’s a pretty good chance (1 in 6) that both values might happen to be the same. If they are, simply call d1.roll() and d2.roll() again to get new values.
  3. We evaluate the getValue() method of each object. The results aren’t too surprising, since the value attribute was set by the roll() method. This attribute will be changed the next time we call the roll() method.
  4. We also ask for a representation of each object. If we provide a method named __str__() in our class, that method is used; otherwise Python shows the memory address associated with the object. All we can see is that the numbers are different, indicating that these instances are distinct objects.

Special Method Names

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.

__init__(self, ...)

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.

__str__(self) → str
The __str__() method of a class is called whenever Python needs a string representation of an object. This is the method used by the str() built-in function. When printing an object, the str() is called implicitly to get the value that is printed.
__repr__(self) → str
The __repr__() method of a class is used when we want to see the details of an object’s values. This method is used by the repr() function.

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.

Some Examples

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() -&gt; number
        Updates the die with a random roll."""
        self.value= 1+random.randrange(self.sides)
        return self.value
    def getValue( self ):
        """getValue() -&gt; 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()
  1. This version of the Die class contains a doc string and three methods: __init__(), roll() and getValue().
  2. The __init__() method, called a constructor, is called automatically when the object is created. We provide a body that sets two instance variables of a Die object. It sets the number of sides, sides to 6 and it then rolls the die a first time to set a value.
  3. The roll() method, called a manipulator, generates a random number, updating the value instance variable.
  4. The getValue() method, called a getter or an accessor, returns the value of the value instance variable, value. Why write this kind of function? Why not simply use the instance variable? We’ll address this in the FAQ’s at the end of this chapter.<
  5. The main() function is outside the Die class, and makes use of the class definition. This function creates two Die, d1 and d2, and then rolls those two Die a dozen times.
  6. This is the top-level script in this file. It executes the main() function, which – in turn – then creates Die objects.

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 )
  1. This class, Point, initializes each point object with the x and y coordinate values of the point. It also provides a few member functions to manipulate the point.
  2. The __init__() method requires two argument values. A client program would use Point( 640, 480 ) to create a Point object and provide arguments to the __init__() method function.
  3. The offset() method requires two argument values. This is a manipulator which changes the state of the point. It moves the point to a new location based on the offset values.
  4. The offset2() method requires one argument value. This method makes use of the offset() method. This kind of reuse assures that both methods are perfectly consistent.
  5. We’ve added a __str__() method, which returns the string representation of the object. When we print any object, the print statement (or print() function) automatically calls the str() built-in function. The str() function uses the __str__() method of an object to get a string representation of the object.
def main():
    obj1_corner = Point( 12, 24 )
    obj2_corner = Point( 8, 16 )
    obj1_corner.offset( -4, -8 )
    print obj1_corner
    print obj2_corner

main()
  1. We construct a Point, named obj1_corner.
  2. We manipulate the obj1_corner Point to move it a few pixels left and up.
  3. We access the obj1_corner object by printing it. This will call the str() function, which will use the __str__() method to get a string representation of the Point.

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 Collaboration

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()
  1. We’re building on the definition of a single Die, from the die.py example. We didn’t repeat it here to save some space in the example.
  2. This class, Dice, defines a pair of Die instances.
  3. The __init__() method creates an instance variable, myDice, which has a tuple of two instances of the Die class.
  4. The roll() method changes the overall state of a given Dice object by changing the two individual Die objects it contains. This manipulator uses a for loop to assign each of the internal Die objects to d. In the loop it calls the roll() method of the Die object, d. This technique is called delegation: a Dice object delegates the work to two individual Die objects. We don’t know, or care, how each Die computes it’s next value.
  5. The getTotal() method computes a sum of all of the Die objects. It uses a for loop to assign each of the internal Die objects to d. It then uses the getValue() method of d. This is the official interface method; by using it, we can remain blissfully unaware of how Die saves it’s state.
  6. The getTuple() method returns the values of each Die object. It uses a list comprehension to create a list of the value instance variables of each Die object. The built-in function tuple() converts the list into an immutable tuple.
  7. The hardways() method examines the value of each Die objec to see if they are the same. If they are, the total was made “the hard way.”

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 0 \leq value < 5. When getValue() adds 1, the value is in the usual range for a single die, 1 \leq value < 6.

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

Class Definition Exercises

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.

Stock Valuation

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.

StockBlock.__init__(self, date, price, number_of_shares)

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.

StockBlock.__str__(self) → string
Return a nicely formatted string that shows the date, price and shares.
StockBlock.getPurchValue(self) → number
Compute the value as purchase price per share × shares.
Stockblock.getSaleValue(self, salePrice) → number
Use salePrice to compute the value as sale price per share × shares.
StockBlock.getROI(self, salePrice) → number

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.

Position.
Position.__init__(self, name, symbol, * blocks)
Accept the company name, ticker symbol and a collection of StockBlock instances.
Position.__str__(self) → string
Return a string that contains the symbol, the total number of shares in all blocks and the total purchse price for all blocks.
Position.getPurchValue(self) → number
Sum the purchase values for all of the StockBlocks in this Position. It delegates the hard part of the work to each StockBlock‘s getPurchValue() method.
Position.getSaleValue(self, salePrice) → number
The getSaleValue() method requires a salePrice; it sums the sale values for all of the StockBlocks in this Position. It delegates the hard part of the work to each StockBlock‘s getSaleValue() method.
Position.getROI(self, salePrice) → number

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.

StockBlock.ownedFor(self, saleDate) → number
This method computes the number of years the stock was owned.
StockBlock.annualROI(self, salePrice, saleDate) → number
This methods divides the gross ROI by the duration in years to return the annualized ROI.

Once we’ve added the necessary support to StockBlock, we can then add to Position.

Position.annualROI(self, salePrice, saleDate) → number
Given the StockBlock.annualROI() method, we can then compute a weighted average of each block’s ROI. This is the annualized ROI for the entire position.

Dive Logging and Surface Air Consumption Rate

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.

c = \frac{33 (s - f)}{t(d + 33)}

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.

Dive.__init__(self, pressure_start, pressure_finish, time_start, time_finish, depth)
The __init__() method will initialize a Dive with the start and finish pressure in PSI, the in and out time as a string, and the depth as an integer. This method should parse both the time_start string and time_finish string and normalize each to be minutes after midnight so that it can compute the duration of the dive. Note that a practical dive log would have additional information like the date, the location, the air and water temperature, sea state, equipment used and other comments on the dive.
Dive.__str__(self) → string
The __str__() method should return a nice string representation of the dive information.
Dive.getSACR(self) → number
The getSACR() method can compute the SACR value from the starting pressure, final pressure, time and depth information.

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

Multi-Dice

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.

MultiDice.__init__(self, dice)
When initializing an instance of MultiDice, you’ll create a collection of individual Die instances. You can use a sequence of some kind, a Set or a FrozenSet.
MultiDice.roll(self)
Roll all dice in the sequence or Set. Note that your choice of collection doesn’t materially alter this method. That’s a cool feature of Python.
MultiDice.getDice(self) → tuple
This method returns the collection of dice as a tuple so that a client class can examine them and potentialy re-roll some or all of the dice.
MultiDice.reroll(self, * dice)
Roll just the given dice. Remember that the MultiDice.getDice() returned the actual dice objects from our set. When the client program gives these objects back to us, we don’t need to search through our sequence or set to locate the underlying objects. We’ve been given the objects.
MultiDice.score(self) → number

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.

MultiDice.matchDie(self, die) → match set, non-match set
Give a Die, use matchValue() to partition the dice based on the value of the given Die‘s value.
MultiDice.matchValue(self, number) → match set, non-match set

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.

MultiDice.NOfAKind(self, n) → match set or :literal:`None`

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

MultiDice.largeStraight(self) → boolean
This function must establish that all five dice form a sequence of values from 1 to 5 or 2 to 6. There must be no gaps and no duplicated values.
MultiDice.smallStraight(self) → boolean

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.

MultiDice.chance(self) → number
The sum of the dice values. It is a number between 5 and 30.

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.

  1. Evaluate the largeStraight() method. If the hand matches, then return a list with an appropriate 2-tuple.

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

  3. Three of a kind. Evaluate the threeOfAKind() method. This will create the first part of the hand’s description.

    • If the matching set has exactly three dice, then the set of unmatched dice must be examined for additional 1’s and 5’s. The first part of the hand’s description string is three-of-a-kind.
    • If the matching has four or five dice, then one or two dice must be popped from the matching set and added to the non-matching set. The set of unmatched dice must be examined for addtional 1’s and 5’s. The first part of the hand’s description string is three-of-a-kind.
    • If there was no set of three matching dice, then all the dice are in the non-matching set, which is checked for 1’s and 5’s. The string which describes the hand has no first part, since there was no three-of-a-kind.
  4. 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.

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

Rational Numbers

A Rational number is a ratio of two integers. Examples include 1/2, 2/3,
22/7, 355/113. 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.

Rational.__init__(self, numerator, denominator=1)

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.

Rational.__str__(self) → string

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.

Rational.__float__(self) → float
Return the floating-point value for the fraction. This method is called when a program does float( rational ).
Rational.__add__(self, other) → Rational

Create and return a new Rational number that is the sum of self and other.

Sum of S + O where S and O are Rational numbers, \frac{S_n}{S_d} and \frac{O_n}{O_d}.

\frac{S_n}{S_d} + \frac{O_n}{O_d} = \frac{S_n \times O_d + O_n \times S_d}{S_d \times O_d}

Example: 3/5 + 7/11 = (33 + 35)/55 = 71/55.

Rational.__mul__(self, other) → Rational

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 S + O where S and O are Rational numbers, \frac{S_n}{S_d} and \frac{O_n}{O_d}.

\frac{S_n}{S_d} \times \frac{O_n}{O_d} = \frac{S_n \times O_n}{S_d \times O_d}

Example: 3/5 × 7/11 = 21/55.

Rational._reduce(self)

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.

Playing Cards and Decks

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.

Card.__init__(self, rank, suit)
Set the rank and suit of the card. The suits can be coded with a single character (“C”, “D”, “H”, “S”), and the ranks can be coded with a number from 1 to 13. The number 1 is an ace. The numbers 11, 12, 13 are Jack, Queen and King, respectively.
Card.__str__(self) → string
Return the rank and suit in the form “2C” or “AS” or “JD”. A rank of 1 would become “A”, a rank of 11, 12 or 13 would become “J”, “Q” or “K”, respectively.
Card.__eq__(self, other) → boolean
Compare this card with other card considering both rank and suit.
Card.__ne__(self, other) → boolean
This can be implemented as not self == other.
Card.__lt__(self, other) → boolean
Compare this card with other, return True if this card’s rank is less than the other. If the ranks are equal compare the suits.
Card.__le__(self, other) → boolean
This can be implemented as self < other or self == other.
Card.__gt__(self, other) → boolean
This can be implemented as not self <= other.
Card.__ge__(self, other) → boolean
This can be implemented as not self < other.

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.

Card.__init__(self)
Create all 52 cards. It can use two loops to iterate through the sequence of suits ("C", "D", "H", "S") and iterate through the ranks range(1,14) . After creating each Card, it can append each Card to a sequence like a list.
Card.deal(self) → Card

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?

Blackjack Hands

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.

Card.getHardValue(self) → number
Returns the points. For most ranks, the points are the rank. For ranks of 11, 12 and 13 return a point value of 10.
Card.getSoftValue(self) → number
Returns the points. For most ranks, the points are the rank. For ranks of 11, 12 and 13 return a point value of 10. A rank of 1 returns a point value of 11.

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.

  • No Aces. The hard and soft total of the hand will be the same; it’s the total of the hard value of each card. If the hard total is less than 21 the hand is in play. If it is equal to 21, it is a potential winner. If it is over 21, the hand has gone bust. Both totals will be computed as the hard value of all cards.
  • One or more Aces. The hard and soft total of the hand are different. The hard total for the hand is the sum of the hard point values of all cards. The soft total for the hand is the soft value of one ace plus the hard total of the rest of the cards. If the hard or soft total is 21, the hand is a potential winner. If the hard total is less than 21 the hand is in play. If the hard total is over 21, the hand has gone bust.

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.

Hand.__init__(self, * cards)
This method should be given two instances of Card to represent the initial deal. It should create a sequence or Set with these two initial cards.
Hand.__str__(self) → string
Return a string with all of the individual cards. A construct like the following works out well: ",".join( map(str, self.cards ) ). This gets the string representation of each card in the self.cards collection, and then uses the string‘s’ join() method to assemble the final display of cards.
Hand.hardTotal(self) → number
Sums the hard value of each Card.
Hand.softTotal(self) → number

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.

Hand.add(self, card)
This method will add another Card() to the Hand().

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.

Poker Hands

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

  1. Straight Flush. Five cards of adjacent ranks, all of the same suit.
  2. Four of a Kind. Four cards of the same rank, plus another card.
  3. Full House. Three cards of the same rank, plus two cards of the same rank.
  4. Flush. Five cards of the same suit.
  5. Straight. Five cards of adjacent ranks. In this case, Ace can be above King or below 2.
  6. Three of a Kind. Three cards of the same rank, plus two cards of other ranks.
  7. Two Pair. Two cards of one rank, plus two cards of another rank, plus one card of a third rank.
  8. Pair. Two cards of one rank, plus three cards of other ranks.
  9. High Card. The highest ranking card in the hand.

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.

Hand.straight(self) → boolean
True if the cards form a straight. This can be tackled easily by sorting the cards into descending order by rank and then checking to see if the ranks all differ by exactly one.
Hand.flush(self) → boolean
True if all cards have the same suit.
Hand.matches(self) → tuple
Returns a tuple of the counts of cards grouped by rank. This can be done iterating through each card, using the card’s rank as a key to the self.rankCount dictionary; the value for that dictionary entry is the count of the number of times that rank has been seen. The values of the dictionary can be sorted, and form six distinct patterns, five of which are shown above. The sixth is simply (1, 1, 1, 1, 1) , which means no two cards had the same rank.
Hand.sortByRank(self)
Sorts the cards by rank.
Hand.sortByMatch(self)
Uses the counts in the self.rankCount dictionary to update each card with its match count, and then sorts the cards by match count.
Hand.sortByMatchAndRank(self)
Uses the counts in the self.rankCount dictionary to update each card with its match count, and then sorts the cards by match count and rank as two separate keys.

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.

Table Of Contents

Previous topic

Advanced Mapping Techniques

Next topic

Advanced Class Definition

This Page