Outcome Class

In Outcome Analysis we’ll look at the responsibilities and collaborators of Outcome objects.

In Design Decision – Object Identity we’ll look at how we can implement the notion of object identity and object equality. This is important because we will be matching Outcome objects based on bets and spinning the Roulette wheel.

We’ll look forward to some other use cases in Looking Forward. Specifically, we know that players, games, and tables will all share references to single outcome objects. How do we do this properly?

In Outcome Design we’ll detail the design for this class. In Outcome Deliverables we’ll provide a list of modules that must be built.

We’ll look at a Python programming topic in Message Formatting. This is a kind of appendix for beginning programmers.

Outcome Analysis

There will be several hundred instances of Outcome objects on a given Roulette table. The bins on the wheel, similarly, collect various Outcomes together. The minimum set of Outcome instances we will need are the 38 numbers, Red, and Black. The other instances will add details to our simulation.

In Roulette, the amount won is a simple multiplication of the amount bet and the odds. In other games, however, there may be a more complex calculation because the house keeps 5% of the winnings, called the “rake”. While it is not part of Roulette, it is good to have our Outcome class designed to cope with these more complex payout rules.

Also, we know that other casino games, like Craps, are stateful. An Outcome may change the game state. We can foresee reworking this class to add in the necessary features to change the state of the game.

While we are also aware that some odds are not stated as x:1, we won’t include these other kinds of odds in this initial design. Since all Roulette odds are x:1, we’ll simply assume that the denominator is always 1. We can forsee reworking this class to handle more complex odds, but we don’t need to handle the other cases yet.

The issue we have, however, is comparing outcomes. They’re used in various places. The idea is to compare the outcomes from a spin of the wheel against the outcomes associated with bets.

How does all this comparison work in Python?

Hint: The default rules aren’t helpful.

Design Decision – Object Identity

Our design will depend on matching Outcome objects. We’ll be testing objects for equality.

The player will be placing bets that contain Outcomes; the table will be holding bets. The wheel will select the winning Outcomes. We need a simple test to see if two objects of the Outcome class are the same.

Was the Outcome for a bet equal to the Outcome contained in a spin of the wheel?

It turns out that this comparison between objects has some subtlety to it.

Here’s the naïve approach to class definition that doesn’t include any provision for equality tests.

Naïve Class Definition

>>> class Outcome:
...    def __init__(self, name, odds):
...        self.name= name
...        self.odds= odds

This seems elegant enough. Sadly, it doesn’t work out when we need to make equality tests.

In Python, if we do nothing special, the __eq__() test will simply compare the internal object id values. These object id values are unique to each distinct object, irrespective of the attribute values.

This default behavior of objects is shown by the following example:

Equality Test Failure

>>> oc1= Outcome( "Any Craps", 8 )
>>> oc2= Outcome( "Any Craps", 8 )
>>> oc1 == oc2
False
>>> id(oc1)
4334572936
>>> id(oc2)
4334573272

Note

Exact ID values will vary.

This example shows that we can have two objects that appear equal, but don’t compare as equal. They are distinct objects with the same attribute values. This makes them not equal according to the default methods inherited from object. However, we would like to have two of these objects test as equal.

Actually, we want more than that.

More than equal

We’ll be creating collections of Outcome objects, and we may need to create sets or maps where hash codes are used in addition to the simple equality tests.

Hash Codes?

Every object has a hash code. The hash code is simply an integer. It can be a summary of the bits that make up the object. It may simply be based on the internal id value for the object. Python computes hash codes and uses these as a quick test for set membership and dictionary keys.

If two hash codes don’t match, the objects can’t possibly be equal. Further comparisons aren’t necessary. If two hash codes do match, then it’s worth the investment of time to do use the more detailed equality comparison.

As we look forward, the Python set and dict depend on a __hash__() method and an __eq__() method of each object in the collection.

Hash Code Failure

>>> hash(oc1)
270386794
>>> hash(oc2)
270392959

Note

Exact ID values will vary.

This shows that two objects that look the same to us can have distinct hash codes. Clearly, this is unacceptable, since we want to be able to create a set of Outcome objects without having things that look like repeats.

Layers of Meaning

The issue is that we have three distinct layers of meaning for comparing objects to see if they are “equal”.

  • Have the same hash code. We can call this “hash equality”.

    This means the __hash__(self)() method for several objects that represent the same Outcome must also have the same hash code. When we put an object into a set or a dictionary, Python uses the hash() function which is implemented by the __hash__() method.

    Sometimes the hash codes are equal, but the object attributes aren’t actually equal. This is called a hash collision, and it’s rare but not unexpected.

    If we don’t implement this, the default version isn’t too useful for creating sets of our Outcome obejcts.

  • Compare as Equal. We can call this “attribute equality”.

    This means that the __eq__() method returns True. When we use the == operator, this is evaluated by using the __eq__() method. This must be overridden by a class to implement attribute equality.

    If we don’t implement this, the default version isn’t too useful for our Outcome obejcts.

  • Are references to the same object. We can call this “identity”.

    We can test that two objects are the same by using the is comparison between two objects. This uses the internal Python identifier for each object. The identifier is revealed by the id() function.

    When we use the is comparison, we’re asserting that the two variables are references to the same underlying object. This is the identity comparison.

Basics of Equality

We note that each instance of Outcome has a distinct Outcome.name value, it seems simple enough to compare names. This is one sense of “equal” that seems to be appropriate.

We can define the __eq__() and __ne__() methods work two ways:

  • When comparing Outcome and string, it will compares the Outcome.name attribute. This is easy, lazy and seems to work perfectly.
  • When comparing Outcome and Outcome, it can compare both name and odds. This seems like over-engineering. The odds depend on the name. The name is the key, the odds are just an attribute.

Similarly, we can compute the value of __hash__() using only the string name, and not the odds. This seems elegantly simple to return the hash of the string name rather than compute a hash.

The definition for __hash__() in section 3.3.1 of the Language Reference Manual tells us to do the calculation using a modulus based on sys.hash_info.width. This is the number of bits, the actual value we want to use is We’d use sys.hash_info.modulus.

Looking Forward

We’ll be looking at Outcome objects in several contexts.

  • We’ll have them in bins of a wheel as winning outcomes from each spin of the wheel.
  • We’ll have them in bets that have been placed on the table.
  • They player will have some outcomes that they prefer to bet on.

We’ll be comparing table bet outcomes and bin outcomes for equality. We have a solution to that, above.

We’ll be creating outcome objects, too. This bumps into an interesting problem.

How do we maintain Don’t Repeat Yourself (DRY) when creating Outcome objects?

We don’t want to include the odds every time we create an Outcome. Repeating the odds would violate the DRY principle.

What are some alternatives?

  • Global Outcome Objects. We can declare global variables for the various outcomes and use those global objects as needed.

    Generally, globals variables are often undesirable because changes to those variables can have unexpected consequences in a large application.

    Global constants are no problem at all. The pool of Outcome instances are proper constant values used to create bins and bets. There would be a lot of them, and they would all be assigned to distinct variables. This sounds complicated.

  • Outcome Factory. We can create a function which is a Factory for individual Outcome objects.

    When some part of the application needs an Outcome object, the factory will do one of two things. If the object doesn’t yet exist, the Factory would create it, save it, and return a reference to it. When some part of the application asked for an Outcome which already exists, the Factory would return a reference to the existing object.

    This centralizes the pool of global objects into a single object, the Factory.

    Further, we can identify Outcome instances by their names, and avoid repeating the payout odds. The function would map a name of an Outcome the object with all of it’s details.

    As a practical matter, the Factory could be seeded with all outcomes. The factory function is – in effect – a global pool of constant objects.

  • Singleton Outcome Class. A Singleton class creates and maintains a single instance of itself. This requires that the class have a static instance() method that is a reference to the one-and-only instance of the class.

    This saves us from creating global variables. Instead, each class definition contains it’s own private reference to the one-and-only object of that class.

    However, this has the profound disadvantage that each distinct outcome would need to be a distinct subclass of Outcome. This is an unappealing level of complexity. Further, it doens’t solve the DRY problem of repeating the details of each Outcome.

A Factory seems like a good way to proceed. It can maintain a collection, and provide values from that collection. We can use class strings to identify Outcome objects. We don’t have to repeat the odds.

We’ll look forward to this in subsequent exercises. For now, we’ll start with the basic class.

Outcome Design

class Outcome

Outcome contains a single outcome on which a bet can be placed.

In Roulette, each spin of the wheel has a number of Outcomes with bets that will be paid off.

For example, the “1” bin has the following winning Outcomes: “1”, “Red”, “Odd”, “Low”, “Column 1”, “Dozen 1-12”, “Split 1-2”, “Split 1-4”, “Street 1-2-3”, “Corner 1-2-4-5”, “Five Bet”, “Line 1-2-3-4-5-6”, “00-0-1-2-3”, “Dozen 1”, “Low” and “Column 1”. All of these bets will payoff if the wheel spins a “1”.

Fields

Outcome.name

Holds the name of the Outcome. Examples include "1", "Red".

Outcome.odds

Holds the payout odds for this Outcome. Most odds are stated as 1:1 or 17:1, we only keep the numerator (17) and assume the denominator is 1.

We can use name to provide hash codes and do equality tests.

Constructors

Outcome.__init__(self, name, odds)
Parameters:
  • name (str) – The name of this outcome
  • odds (int) – The payout odds of this outcome.

Sets the instance name and odds from the parameter name and odds.

Methods

For now, we’ll assume that we’re going to have global instances of each Outcome. Later we’ll introduce some kind of Factory.

Outcome.winAmount(self, amount) → amount

Multiply this Outcome‘s odds by the given amount. The product is returned.

Parameters:amount (number) – amount being bet
Outcome.__eq__(self, other) → boolean

Compare the name attributes of self and other.

Parameters:other (Outcome) – Another Outcome to compare against.
Returns:True if this name matches the other name.
Return type:bool
Outcome.__ne__(self, other) → boolean

Compare the name attributes of self and other.

Parameters:other (Outcome) – Another Outcome to compare against.
Returns:True if this name does not match the other name.
Return type:bool
Outcome.__hash__(self) → int

Hash value for this outcome.

Returns:The hash value of the name, hash(self.name).
Return type:int

A hash calculation must include all of the attributes of an object that are essential to it’s distinct identity.

In this case, we can return hash(self.name) because the odds aren’t really part of what makes an outcome distinct. Each outcome is an abstraction and a string name is all that identifies them.

The definition for __hash__() in section 3.3.1 of the Language Reference Manual tells us to do the calculation using a modulus based on sys.hash_info.width. That value is the number of bits, the actual value we want to use is sys.hash_info.modulus, which is based on the width.

Outcome.__str__(self) → string

Easy-to-read representation of this outcome. See Message Formatting.

This easy-to-read String output method is essential. This should return a String representation of the name and the odds. A form that looks like 1-2 Split (17:1) works nicely.

Returns:String of the form name (odds:1).
Return type:str
Outcome.__repr__(self) → string

Detailed representation of this outcome. See Message Formatting.

Returns:String of the form Outcome(name, odds).
Return type:str

Outcome Deliverables

There are two deliverables for this exercise. Both will have Python docstrings.

  • The Outcome class.

  • Unit tests of the Outcome class. This can be doctest strings inside the class itself, or it can be a separate unittest.TestCase class.

    The unit test should create a three instances of Outcome, two of which have the same name. It should use a number of individual tests to establish that two Outcome with the same name will test true for equality, have the same hash code, and establish that the winAmount() method works correctly.

Message Formatting

For the very-new-to-Python, there are few variations on creating a formatted string.

Generally, we simply use something like this.

def __str__( self ):
    return "{name:s} ({odds:d}:1)".format_map( vars(self) )

This uses the built-in vars() function to expose the attributes of an object as a simple dictionary that maps attribute names to values.

This is similar to using the self.__dict__ internal dictionary.

The format string uses :s and :d as detailed specifications for the values to interpolate into the string. There’s a lot of flexbility in how numbers are formatted.

There’s another variation that can be handy.

def __repr__( self ):
    return "{class_:s}({name!r}, {odds!r})".format(
        class_=type(self).__name__, **vars(self) )

This exposes the class name as well as the attribute values.

We’ve used the !r to request the internal representation for each attribute. For a string, it means it will be explicitly quoted.