Multi-strategy __init__()

We may have objects that are created from a variety of sources. For example, we might need to clone an object as part of creating a memento, or freeze an object so that it can be used as the key of a dictionary or placed into a set; this is the idea behind the set and frozenset built-in classes.

We'll look at two design patterns that offer multiple ways to build an object. One design pattern uses a complex __init__() method with multiple strategies for initialization. This leads to designing the __init__() method with a number of optional parameters. The other common design pattern involves creating multiple static or class-level methods, each with a distinct definition.

Defining an overloaded __init__() method can be confusing to mypy, because the parameters may have distinct value types. This is solved by using the @overload decorator to describe the different assignments of types to the __init__() parameters. The approach is to define each of the alternative versions of __init__() and decorate with @overload. A final version – without any decoration – defines the parameters actually used for the implementation.

The following is an example of a Hand3 object that can be built in either of the two ways:

class Hand3:

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

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

def __init__(
self,
arg1: Union[Card, "Hand3"],
arg2: Optional[Card] = None,
arg3: Optional[Card] = None,
) -> None:
self.dealer_card: Card
self.cards: List[Card]

if isinstance(arg1, Hand3) and not arg2 and not arg3:
# Clone an existing hand
self.dealer_card = arg1.dealer_card
self.cards = arg1.cards
elif (isinstance(arg1, Card)
and isinstance(arg2, Card)
and isinstance(arg3, Card)
):
# Build a fresh, new hand.
self.dealer_card = cast(Card, arg1)
self.cards = [arg2, arg3]

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.dealer_card!r}, *{self.cards})"

In the first overloaded case, a Hand3 instance has been built from an existing Hand3 object. In the second case, a Hand3 object has been built from individual Card instances. The @overload decorator provides two alternative versions of the __init__() method. These are used by mypy to ensure this constructor is used properly. The undecorated version is used at runtime. It is a kind of union of the two overloaded definitions.

The @overload definitions are purely for mypy type-checking purposes. The non-overloaded definition of __init__() provides a hint for arg1 as union of either a Card object or a Hand3 object. The code uses the isinstance() function to decide which of the two types of argument values were provided. To be more robust, the if-elif statements should have an else: clause. This should raise a ValueError exception.

This design parallels the way a frozenset object can be built from individual items or an existing set object. We will look at creating immutable objects more in the next chapter. Creating a new Hand3 object from an existing Hand3 object allows us to create a memento of a Hand3 object using a construct such as the following code snippet:

h = Hand3(deck.pop(), deck.pop(), deck.pop()) 
memento = Hand3(h) 

We saved the Hand object in the memento variable. This can be used to compare the final with the original hand that was dealt, or we can freeze it for use in a set or mapping too.

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

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