Complex composite objects
The following is an example of a blackjack Hand
description that might be suitable for emulating play strategies:
class Hand: def __init__( self, dealer_card ): self.dealer_card= dealer_card self.cards= [] def hard_total(self ): return sum(c.hard for c in self.cards) def soft_total(self ): return sum(c.soft for c in self.cards)
In this example, we have an instance variable self.dealer_card
based on a parameter of the __init__()
method. The self.cards
instance variable, however, is not based on any parameter. This kind of initialization creates an empty collection.
To create an instance of Hand
, we can use the following code:
d = Deck() h = Hand( d.pop() ) h.cards.append( d.pop() ) h.cards.append( d.pop() )
This has the disadvantage that a long-winded sequence of statements is used to build an instance of a Hand
object. It can become difficult to serialize the Hand
object and rebuild it with an initialization such as this one. Even if we were to create an explicit append()
method in this class, it would still take multiple steps to initialize the collection.
We could try to create a fluent interface, but that doesn't really simplify things; it's merely a change in the syntax of the way that a Hand
object is built. A fluent interface still leads to multiple method evaluations. When we take a look at the serialization of objects in Part 2, Persistence and Serialization we'd like an interface that's a single class-level function, ideally the class constructor. We'll look at this in depth in Chapter 9, Serializing and Saving - JSON, YAML, Pickle, CSV, and XML.
Note also that the hard total and soft total method functions shown here don't fully follow the rules of blackjack. We return to this issue in Chapter 2, Integrating Seamlessly with Python – Basic Special Methods.
Complete composite object initialization
Ideally, the __init__()
initializer method will create a complete instance of an object. This is a bit more complex when creating a complete instance of a container that contains an internal collection of other objects. It'll be helpful if we can build this composite in a single step.
It's common to have both a method to incrementally accrete items as well as the initializer special method that can load all of the items in one step.
For example, we might have a class such as the following code snippet:
class Hand2: def __init__( self, dealer_card, *cards ): self.dealer_card= dealer_card self.cards = list(cards) def hard_total(self ): return sum(c.hard for c in self.cards) def soft_total(self ): return sum(c.soft for c in self.cards)
This initialization sets all of the instance variables in a single step. The other methods are simply copies of the previous class definition. We can build a Hand2
object in two ways. This first example loads one card at a time into a Hand2
object:
d = Deck() P = Hand2( d.pop() ) p.cards.append( d.pop() ) p.cards.append( d.pop() )
This second example uses the *cards
parameter to load a sequence of Cards
class in a single step:
d = Deck() h = Hand2( d.pop(), d.pop(), d.pop() )
For unit testing, it's often helpful to build a composite object in a single statement in this way. More importantly, some of the serialization techniques from the next part will benefit from a way of building a composite object in a single, simple evaluation.