Book Image

Mastering Object-oriented Python

By : Steven F. Lott, Steven F. Lott
Book Image

Mastering Object-oriented Python

By: Steven F. Lott, Steven F. Lott

Overview of this book

Table of Contents (26 chapters)
Mastering Object-oriented Python
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Some Preliminaries
Index

Simple composite objects


A composite object can also be called a container. We'll look at a simple composite object: a deck of individual cards. This is a basic collection. Indeed, it's so basic that we can, without too much struggle, use a simple list as a deck.

Before designing a new class, we need to ask this question: is using a simple list appropriate?

We can use random.shuffle() to shuffle the deck and deck.pop() to deal cards into a player's Hand.

Some programmers rush to define new classes as if using a built-in class violates some object-oriented design principle. Avoiding a new class leaves us with something as shown in the following code snippet:

d= [card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]
random.shuffle(d)
hand= [ d.pop(), d.pop() ]

If it's that simple, why write a new class?

The answer isn't perfectly clear. One advantage is that a class offer a simplified, implementation-free interface to the object. As we noted previously, when discussing factories, a class isn't a requirement in Python.

In the preceding code, the deck only has two simple use cases and a class definition doesn't seem to simplify things very much. It does have the advantage of concealing the implementation's details. But the details are so trivial that exposing them seems to have little cost. We're focused primarily on the __init__() method in this chapter, so we'll look at some designs to create and initialize a collection.

To design a collection of objects, we have the following three general design strategies:

  • Wrap: This design pattern is an existing collection definition. This might be an example of the Facade design pattern.

  • Extend: This design pattern is an existing collection class. This is ordinary subclass definition.

  • Invent: This is designed from scratch. We'll look at this in Chapter 6, Creating Containers and Collections.

These three concepts are central to object-oriented design. We must always make this choice when designing a class.

Wrapping a collection class

The following is a wrapper design that contains an internal collection:

class Deck:
    def __init__( self ):
        self._cards = [card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]
        random.shuffle( self._cards )
    def pop( self ):
        return self._cards.pop()

We've defined Deck so that the internal collection is a list object. The pop() method of Deck simply delegates to the wrapped list object.

We can then create a Hand instance with the following kind of code:

d= Deck()
hand= [ d.pop(), d.pop() ]

Generally, a Facade design pattern or wrapper class contains methods that are simply delegated to the underlying implementation class. This delegation can become wordy. For a sophisticated collection, we may wind up delegating a large number of methods to the wrapped object.

Extending a collection class

An alternative to wrapping is to extend a built-in class. By doing this, we have the advantage of not having to reimplement the pop() method; we can simply inherit it.

The pop() method has the advantage that it creates a class without writing too much code. In this example, extending the list class has the disadvantage that this provides many more functions than we truly need.

The following is a definition of Deck that extends the built-in list:

class Deck2( list ):
    def __init__( self ):
        super().__init__( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade) )
        random.shuffle( self )

In some cases, our methods will have to explicitly use the superclass methods in order to have proper class behavior. We'll see other examples of this in the following sections.

We leverage the superclass's __init__() method to populate our list object with an initial single deck of cards. Then we shuffle the cards. The pop() method is simply inherited from list and works perfectly. Other methods inherited from the list also work.

More requirements and another design

In a casino, the cards are often dealt from a shoe that has half a dozen decks of cards all mingled together. This consideration makes it necessary for us to build our own version of Deck and not simply use an unadorned list object.

Additionally, a casino shoe is not dealt fully. Instead, a marker card is inserted. Because of the marker, some cards are effectively set aside and not used for play.

The following is Deck definition that contains multiple sets of 52-card decks:

class Deck3(list):
    def __init__(self, decks=1):
        super().__init__()
        for i in range(decks):
            self.extend( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade) )
        random.shuffle( self )
        burn= random.randint(1,52)
        for i in range(burn): self.pop()

Here, we used the __init__() superclass to build an empty collection. Then, we used self.extend() to append multiple 52-card decks to the shoe. We could also use super().extend() since we did not provide an overriding implementation in this class.

We could also carry out the entire task via super().__init__() using a more deeply nested generator expression, as shown in the following code snippet:

( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade) for d in range(decks) )

This class provides us with a collection of Card instances that we can use to emulate casino blackjack as dealt from a shoe.

There's a peculiar ritual in a casino where they reveal the burned card. If we're going to design a card-counting player strategy, we might want to emulate this nuance too.