More complex initialization alternatives

In order to write a multi-strategy initialization, it can seem helpful to give up on specific named parameters. This leads to trying to use the **kw construct to take only named arguments. This design has the advantage of being very flexible, but the disadvantage of bypassing automated type checking. It requires a great deal of documentation explaining the variant use cases.

Instead of collecting all named parameters using the ** construct, it's often helpful to use a standalone * construct. When we write def f(a: int, b: int, *, c: int), we're expecting two positional argument values, and the third value must be provided by name. We'd use this function as f(1, 2, c=3). This provides for explicit names to cover special cases.

We can expand our initialization to also split a Hand object. The result of splitting a Hand object is simply another constructor. The following code snippet shows how the splitting of a Hand object might look:

class Hand4:

@overload
def __init__(self, arg1: "Hand4") -> None:
...

@overload
def __init__(self,
arg1: "Hand4", arg2: Card, *, split: int) -> None:
...

@overload
def __init__(self,
arg1: Card, arg2: Card, arg3: Card) -> None:
...

def __init__(
self,
arg1: Union["Hand4", Card],
arg2: Optional[Card] = None,
arg3: Optional[Card] = None,
split: Optional[int] = None,
) -> None:
self.dealer_card: Card
self.cards: List[Card]
if isinstance(arg1, Hand4):
# Clone an existing hand
self.dealer_card = arg1.dealer_card
self.cards = arg1.cards

elif isinstance(arg1, Hand4) and isinstance(arg2, Card) and "split" is not None:
# Split an existing hand
self.dealer_card = arg1.dealer_card
self.cards = [arg1.cards[split], arg2]

elif (
isinstance(arg1, Card)
and isinstance(arg2, Card)
and isinstance(arg3, Card)
):
# Build a fresh, new hand from three cards
self.dealer_card = arg1
self.cards = [arg2, arg3]
else:
raise TypeError("Invalid constructor {arg1!r} {arg2!r} {arg3!r}")

def __str__(self) -> str:
return ", ".join(map(str, self.cards))

This design reflects three separate use cases:

  • Creating a Hand4 object from an existing Hand4 object. In this case, arg1 will have the Hand4 type and the other arguments will have default values of None.
  • Splitting a Hand4 object. This requires a value for the split keyword argument that uses the position of the Card class from the original Hand4 object. Note how * is inserted into the parameter list to show that the split value must be provided as a keyword argument value.
  • Building a Hand4 object from three Card instances. In this case, all three of the positional parameters will have values of the Card type.

The @overload decorator information is used by mypy. It provides documentation for people using this class. It has no runtime impact. 

The following code snippet shows how we'd use these definitions to create and split a hand:

d = Deck() 
h = Hand4(d.pop(), d.pop(), d.pop()) 
s1 = Hand4(h, d.pop(), split=0) 
s2 = Hand4(h, d.pop(), split=1) 

We created an initial h instance of Hand4, split it into two other Hand4 instances, s1 and s2, and dealt an additional Card class into each. The rules of Blackjack only allow this when the initial hand has two cards of equal rank.

While this __init__() method is rather complex, it has the advantage that it can parallel the way in which fronzenset is created from an existing set. The disadvantage is that it needs a large docstring to explain all these variations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset