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 – Complex we’ll detail the design for this class. In Outcome Design – Simple we’ll detail a simpler design using @dataclass definitions from the standard library.

In Outcome Deliverables we’ll provide a list of classes and tests to 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 Outcome instances 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 foresee reworking this class to handle more complex odds, but we don’t need to handle the other cases yet.

The fundamental consideration here is comparing two instances of a Outcome. One will be part of a Bet, the other instance will be part of the Wheel. We need to know if the bet’s Outcome matches the wheel’s Outcome.

How does all object comparison work in Python?

Hint: The default rules aren’t helpful.

The rules reveal the foundations of object state and identity. While this class is seemingly insignificantly trivial, it exposes central Python concepts.

Design Decision – Object Identity

Our design must match Outcome objects. We’ll be testing objects for equality.

The player will be placing bets that contain Outcome instances; the table will be holding bets. The wheel will select the winning Outcome instances. 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: str, odds: int) -> None:
...        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 default special method, __eq__(), 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) == id(oc2)
False
>>> hash(oc1) == hash(oc2)
False
>>> oc1 is oc2
False

This example shows that we can have two objects that appear equal, but don’t compare as equal. While we can see they have the same attribute values, The is test shows they are distinct objects. 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 simple equality. We need to have them behave identically when used in sets and dictionaries.

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 is a summary of the bits that make up 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, there’s a possibility the two objects are equal.

The rare case of equal hash values for unequal objects is called a “hash collision.” It’s a consequence of digesting large and complex objects into small numeric values.

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 lesson here is there are three distinct layers of meaning for comparing objects to see if they are “equal”. Here are the layers:

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

    We need the __hash__() method for several objects that represent the same Outcome to have the same hash code. When we put an object into a set or a dictionary, Python uses the hash() function, 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 __hash__(), each instance is considered distinct making equality tests and set membership awkwardly wrong.

  • Objects 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 implementation of this method isn’t too useful for our Outcome objects.

  • Variables are two references to the same underlying object. We can call this “identity”.

    We can test that two objects are the same by using the is comparison operator. 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 most often used when comparing an object against the None object. In that exceptional case, we often use x is None.

How can we get the __hash__() and __eq__() implemented properly? We have several choices. We’ll start by looking at the typing.NamedTuple class.

Named Tuples

The naïve class definition shown above didn’t work out well. A significant improvement is to use the built-in tuple class. This can bind a name and a payoff amount together. Look at the following example; this shows behavior we would like to see:

>>> oc1 = Outcome("Any Craps", 8)
>>> oc2 = Outcome("Any Craps", 8)
>>> oc1 == oc2
True
>>> id(oc1) == id(oc2)
False
>>> hash(oc1) == hash(oc2)
True
>>> oc1 is oc2
False

This is the behavior we want. Two distinct instances of an outcome compare as equal when all of their individual attribute values are equal.

We can leverage the typing.NamedTuple to provide this equality test for our unique classes.

from typing import NamedTuple

class Outcome(NamedTuple):
    name: str
    odds: int

This definition provides us with a minimal definition of an outcome with all of the right behaviors.

The type hints in this class definition allow us to use mypy to confirm our design is likely to work. This is an important part of static analysis to be sure that the code is likely to work reliably and consistently.

This isn’t the only available solution to building __hash__() and __eq__() methods. We can also use a dataclasses.dataclass.

Frozen Dataclasses

We can leverage the dataclasses.dataclass to provide an equality test that’s similar to the features of a typing.NamedTuple. This gives us a useful equality test for our unique classes.

from dataclasses import dataclass

@dataclass(frozen=True)
class Outcome_d:
    name: str
    odds: int

This definition provides us with a minimal definition of an outcome with all of the right behaviors.

>>> oc1 = Outcome("Any Craps", 8)
>>> oc2 = Outcome("Any Craps", 8)
>>> oc1 == oc2
True
>>> id(oc1) == id(oc2)
False
>>> hash(oc1) == hash(oc2)
True
>>> oc1
Outcome(name='Any Craps', odds=8)

For the purposes we have in mind, both a typing.NamedTuple and a frozen dataclasses.dataclass have similar behaviors. There are differences, of course, but they don’t impact anything we’re going to do.

We’ll use the dataclasses in a number of later exercises. For that reason, we’ll start with a frozen dataclass as a way to define the Outcome class with useful equality comparisons.

Special Method Implementation

Instead of using the available standard library class definitions, we can implement a few special methods that will provide useful equality tests. This is more complex than using one of the built-in library classes, but can be helpful for understanding how Python works.

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__() special methods to provide useful equality tests:

  • When comparing with a string value, compare the self.name attribute.

  • When comparing with an Outcome instance, compare self.name and other.name.

Similarly, we can compute the value of the __hash__() method to use 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.

Additional Outcome Design Thoughts

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.

  • The player will have a set of outcomes they can bet on.

All these uses of Outcome objects forces us to consider an interesting question:

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

If a player always bets on black, we don’t want to include the odds when the player names the “black” Outcome instance. Repeating the odds violates 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. A 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. Because of the way Python module imports work, we can define all of these in a single module and share the pool throughout the application.

  • Outcome Factory. We can create an object which is a Factory for individual Outcome objects. When some part of the application – for example, the player – needs an Outcome object, the factory provides the object. The Factory can create use the unique name to locate the complete Outcome instance. This allows a player to request the “Black” Outcome instance. If we identify Outcome instances by their names, we can avoid repeating the payout odds. When we look at the Roulette Wheel, this seems to be the perfect factory for Outcome instances. A player can request the domain of all Outcome instances from the wheel.

  • Singleton Outcome Class. A Singleton class creates and maintains a single instance of itself. This involves some unpleasant meta-programming to change the way object creation works. It’s not particularly difficult, but it seems needless when the other alternatives appear to be simpler. 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 doesn’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 later chapters. For now, we’ll start with the basic class.

In the next section we’ll look at a complete and detailed definition of the Outcome class.

Outcome Design – Complex

This is the design for an Outcome class separate from any dataclass definition. This is rather complex. The simpler approach is described below under Outcome Design – Simple.

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 Outcome objects with bets that will be paid off. For example, the “1” bin has the following winning Outcome instances: “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 thee above-named bets will pay off if the wheel spins a “1”. This makes a Wheel and a Bin fairly complex containers of Outcome objects.

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 as the basis for computing hash codes and doing equality tests.

Constructors

Outcome.__init__(self, name: str, odds: int) → None
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: float) → float

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

Parameters

amount (float) – amount being bet

Outcome.__eq__(self, other) → bool

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) → bool

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) → str

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) → str

Detailed representation of this outcome. See Message Formatting.

Returns

String of the form Outcome(name=name, odds=odds).

Return type

str

Outcome Design – Simple

A simpler variation on the Outcome class can be based on @dataclass(frozen=True).

See above, in the fields section, the two fields required.

The default methods created by the @dataclass decorator should work perfectly.

The __str__() method will have to be written based on the description above, under methods.

This should pass all of the unit tests described in the Outcome Deliverables section.

Example Unit Test Case

The Outcome class doesn’t have too many features. The unit tests should reflect the core simplicity.

def test_outcome():
    o1 = Outcome("Red", 1)
    o2 = Outcome("Red", 1)
    o3 = Outcome("Black", 2)
    assert str(o1) == "Red 1:1"
    assert repr(o2) == "Outcome(name='Red', odds=1)"
    assert o1 == o2
    assert o1.odds == 1
    assert o1.name == "Red"
    assert o1 != o3
    assert o2 != o3

This seems to cover almost everything imaginable with respect to the Outcome class.

Outcome Deliverables

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

  • The Outcome class. This can be put into a roulette.py file. A single file can include all of the class definitions.

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

    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.

    We’ve provided an example as part of this section, to clarify what the test case should include.

Message Formatting

For the very-new-to-Python, there are few variations on creating a formatted message string. The str() function makes use of a class __str__() method to provide the class-unique string. The repr() function makes use of the class __repr__() method to provide the class unit representation.

The str() function is intended to produce easy-to-read strings. The repr() function is intended to provide detailed strings, often in Python syntax.

Generally, we simply use something like this.

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

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

There’s another variation that can be handy.

def __repr__( self ):
    return f"{self.__class__.__name__:s}(name={self.name!r}, odds={self.odds!r})"

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

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

Looking Forward

In the next chapter, we’ll combine Outcome instances into Bin collections. These Bin objects will become part of the Roulette Wheel collection.