Mastering Object-Oriented Python: Chapter No. 1 "The - Init - Method"
Mastering Object-Oriented Python: Chapter No. 1 "The - Init - Method"
Steven F. Lott
Part 1, Pythonic Classes via Special Methods: This part looks more deeply at objectoriented programming techniques and how we can more tightly integrate the class definitions of our applications with Python's built-in features. It consists of nine chapters, which are as follows: Chapter 1, The _init_() Method, provides us with a detailed description and implementation of the _init_() method. We will look at different forms of initialization for simple objects. From this, we can look into more complex objects that involve collections and containers. Chapter 2, Integrating Seamlessly with Python Basic Special Methods, will explain in detail as to how we can expand a simple class definition to add special methods. We'll need to take a look at the default behavior inherited from the object so that we can understand what overrides are needed and when they're actually needed. Chapter 3, Attribute Access, Properties, and Descriptors, shows us how the default processing works in some detail. We need to decide where and when to override the default behavior. We will also explore descriptors and gain a much deeper understanding on how Python's internals work. Chapter 4, The ABCs of Consistent Design, looks at the abstract base classes in the collections.abc module in general. We'll look at the general concepts behind the various containers and collections that we might want to revise or extend. Similarly, we'll look at the concepts behind the numbers that we might want to implement. Chapter 5, Using Callables and Contexts, looks at several ways to create context managers using the tools in contextlib. We'll show you a number of variant designs for callable objects. This will show you why a stateful callable object is sometimes more useful than a simple function. We'll also take a look at how to use some of the existing Python context managers before we dive in and write our own context manager.
Chapter 6, Creating Containers and Collections, focuses on the basics of container classes. We'll review the variety of special methods that are involved in being a container and offering the various features that containers offer. We'll address extending built-in containers to add features. We'll also look at wrapping built-in containers and delegating methods through the wrapper to the underlying container. Chapter 7, Creating Numbers, covers these essential arithmetic operators: +, -, *, /, //, %, and **. We'll also take a look at these comparison operators: <, >, <=, >=, ==, and !=. We'll finish by summarizing some of the design considerations that go into extending or creating new numbers. Chapter 8, Decorators and Mixins Cross-cutting Aspects, covers simple function decorators, function decorators with arguments, class decorators, and method decorators.
Part 2, Persistence and Serialization: A persistent object has been serialized to a storage medium. Perhaps it's transformed to JSON and written to the filesystem. An ORM layer can store the object in a database. This part will take a look at the alternatives to handle persistence. This section contains five chapters, which are as follows: Chapter 9, Serializing and Saving JSON, YAML, Pickle, CSV, and XML, covers simple persistence using libraries focused on various data representations such as JSON, YAML, pickle, XML, and CSV. Chapter 10, Storing and Retrieving Objects via Shelve, explains basic database operations with Python modules, such as shelve (and dbm). Chapter 11, Storing and Retrieving Objects via SQLite, moves to the more complex world of SQL and the relational database. Because SQL features don't match object-oriented programming features well, we have an impedance mismatch problem. A common solution is to use ORM to allow us to persist a large domain of objects. Chapter 12, Transmitting and Sharing Objects, takes a look at the HTTP protocol, JSON, YAML, and XML representation to transmit an object. Chapter 13, Configuration Files and Persistence, covers various ways in which a Python application can work with a configuration file.
Part 3, Testing, Debugging, Deploying, and Maintaining: We'll show you how to gather data to support and debug high-performance programs. This will include information on creating the best possible documentation in order to reduce the confusion and complexity of the support. This section contains the final five chapters, which are as follows:
Chapter 14, The Logging and Warning Modules, takes a look at using the logging and warning modules to create audit information, as well as debug. We'll take a significant step beyond using the print() function. Chapter 15, Designing for Testability, covers designing for testability and how we use unittest and doctest. Chapter 16, Coping with the Command Line, takes a look at using the argparse module to parse options and arguments. We'll take this a step further and use the command design pattern to create program components that can be combined and expanded without resorting to writing shell scripts. Chapter 17, The Module and Package Design, covers module and package design. This is a higher-level set of considerations. We will take a look at related classes in a module and related modules in a package. Chapter 18, Quality and Documentation, covers how we can document our design to create trust that our software is correct and has been properly implemented.
We can see that a class is an object of the class named type and that the base class for our new class is the class named object. As we look at each method, we also take a look at the default behavior inherited from object. In some cases, the superclass special method behavior will be exactly what we want. In other cases, we'll need to override the special method.
The Rectangle class has a method that uses two attributes to return a value. The attributes have not been initialized anywhere. This is legal Python. It's a little strange to avoid specically setting attributes, but it's valid. The following is an interaction with the Rectangle class:
>>> r= Rectangle() >>> r.length, r.width = 13, 8 >>> r.area() 104
While this is legal, it's a potential source of deep confusion, which is a good reason to avoid it.
[ 26 ]
Chapter 1
However, this kind of design grants exibility, so there could be times when we needn't set all of the attributes in the __init__() method. We walk a ne line here. An optional attribute is a kind of subclass that's not formally declared as a proper subclass. We're creating polymorphism in a way that could lead to confusing and inappropriate use of convoluted if statements. While uninitialized attributes may be useful, they could be the symptom of a bad design. The Zen of Python poem (import this) offers the following advice: "Explicit is better than implicit." An __init__() method should make the instance variables explicit.
Pretty Poor Polymorphism There's a ne line between exibility and foolishness. We may have stepped over the edge off exible into foolish as soon as we feel the need to write:
if 'x' in self.__dict__:
Or:
try: self.x except AttributeError:
It's time to reconsider the API and add a common method or attribute. Refactoring is better than adding if statements.
The __init__() Method def _points( self ): return int(self.rank), int(self.rank) class AceCard( Card ): def _points( self ): return 1, 11 class FaceCard( Card ): def _points( self ): return 10, 10
In this example, we factored the __init__() method into the superclass so that a common initialization in the superclass, Card, applies to all the three subclasses NumberCard, AceCard, and FaceCard. This shows a common polymorphic design. Each subclass provides a unique implementation of the _points() method. All the subclasses have identical signatures: they have the same methods and attributes. Objects of these three subclasses can be used interchangeably in an application. If we simply use characters for suits, we will be able to create Card instances as shown in the following code snippet:
cards = [ AceCard('A', ''), NumberCard('2',''), NumberCard('3',''), ]
We enumerated the class, rank, and suit for several cards in a list. In the long run, we need a much smarter factory function to build Card instances; enumerating all 52 cards this way is tedious and error prone. Before we get to the factory functions, we take a look at a number of other issues.
Chapter 1
Python has no simple formal mechanism for dening an object as immutable. We'll look at techniques to assure immutability in Chapter 3, Attribute Access, Properties, and Descriptors. In this example, it might make sense for the attributes of a suit to be immutable. The following is a class that we'll use to build four manifest constants:
class Suit: def __init__( self, name, symbol ): self.name= name self.symbol= symbol
For an example this small, this method isn't a huge improvement over single character suit codes. In more complex cases, there may be a short list of Strategy or State objects that can be created like this. This can make the Strategy or State design patterns work efciently by reusing objects from a small, static pool of constants. We do have to acknowledge that in Python these objects aren't technically constant; they are mutable. There may be some benet in doing the extra coding to make these objects truly immutable.
The irrelevance of immutability Immutability can become an attractive nuisance. It's sometimes justied by the mythical "malicious programmer" who modies the constant value in their application. As a design consideration, this is silly. This mythical, malicious programmer can't be stopped this way. There's no easy way to "idiot-proof" code in Python. The malicious programmer has access to the source and can tweak it just as easily as they can write code to modify a constant. It's better not to struggle too long to dene the classes of immutable objects. In Chapter 3, Attribute Access, Properties, and Descriptors, we'll show ways to implement immutability that provides suitable diagnostic information for a buggy program.
[ 29 ]
In Python, a class isn't required. It's merely a good idea when there are related factories that are complex. One of the strengths of Python is that we're not forced to use a class hierarchy when a simple function might do just as well.
While this is a book about object-oriented programming, a function really is ne. It's common, idiomatic Python.
We can always rewrite a function to be a proper callable object if the need arises. From a callable object, we can refactor it into a class hierarchy for our factories. We'll look at callable objects in Chapter 5, Using Callables and Contexts. The advantage of class denitions in general is to achieve code reuse via inheritance. The function of a factory class is to wrap some target class hierarchy and the complexities of object construction. If we have a factory class, we can add subclasses to the factory class when extending the target class hierarchy. This gives us polymorphic factory classes; the different factory class denitions have the same method signatures and can be used interchangeably. This class-level polymorphism can be very helpful with statically compiled languages such as Java or C++. The compiler can resolve the details of the class and methods when generating code. If the alternative factory denitions don't actually reuse any code, then a class hierarchy won't be helpful in Python. We can simply use functions that have the same signatures. The following is a factory function for our various Card subclasses:
def card( rank, suit ): if rank == 1: return AceCard( 'A', suit ) elif 2 <= rank < 11: return NumberCard( str(rank), suit ) [ 30 ]
Chapter 1 elif 11 <= rank < 14: name = { 11: 'J', 12: 'Q', 13: 'K' }[rank] return FaceCard( name, suit ) else: raise Exception( "Rank out of range" )
This function builds a Card class from a numeric rank number and a suit object. We can now build cards more simply. We've encapsulated the construction issues into a single factory function, allowing an application to be built without knowing precisely how the class hierarchy and polymorphic design works. The following is an example of how we can build a deck with this factory function:
deck = [card(rank, suit) for rank in range(1,14) for suit in (Club, Diamond, Heart, Spade)]
This enumerates all the ranks and suits to create a complete deck of 52 cards.
Some programmers can understand this if statement at a glance. Others will struggle to determine if all of the cases are properly exclusive. For advanced Python programming, we should not leave it to the reader to deduce the conditions that apply to an else clause. Either the condition should be obvious to the newest of n00bz, or it should be explicit.
When to use catch-all else Rarely. Use it only when the condition is obvious. When in doubt, be explicit and use else to raise an exception. Avoid the vague else clause.
For the sake of simplicity, it's better to focus on just one of these techniques rather than on both. We can always replace a mapping with elif conditions. (Yes, always. The reverse is not true though; transforming elif conditions to a mapping can be challenging.) The following is a Card factory without the mapping:
def card3( rank, suit ): if rank == 1: return elif 2 <= rank < 11: elif rank == 11: return FaceCard( elif rank == 12: return FaceCard( elif rank == 13: return FaceCard( else: raise Exception( AceCard( 'A', suit ) return NumberCard( str(rank), suit ) 'J', suit ) 'Q', suit ) 'K', suit ) "Rank out of range" )
We rewrote the card() factory function. The mapping was transformed into additional elif clauses. This function has the advantage that it is more consistent than the previous version.
[ 32 ]
Chapter 1
We've mapped the rank object to a class. Then, we applied the class to the rank and suit values to build the nal Card instance. We can use a defaultdict class as well. However, it's no simpler for a trivial static mapping. It looks like the following code snippet:
defaultdict( lambda: NumberCard, {1: AceCard, 11: FaceCard, 12: FaceCard, 12: FaceCard} )
Note that the default of a defaultdict class must be a function of zero arguments. We've used a lambda construct to create the necessary function wrapper around a constant. This function, however, has a serious deciency. It lacks the translation from 1 to A and 13 to K that we had in previous versions. When we try to add that feature, we run into a problem. We need to change the mapping to provide both a Card subclass as well as the string version of the rank object. What can we do for this two-part mapping? There are four common solutions: We can do two parallel mappings. We don't suggest this, but we'll show it to emphasize what's undesirable about it. We can map to a two-tuple. This also has some disadvantages. We can map to a partial() function. The partial() function is a feature of the functools module. We can also consider modifying our class denition to t more readily with this kind of mapping. We'll look at this alternative in the next section on pushing __init__() into the subclass denitions.
This is not desirable. It involves a repetition of the sequence of the mapping keys 1, 11, 12, and 13. Repetition is bad because parallel structures never seem to stay that way after the software has been updated.
Don't use parallel structures Two parallel structures should be replaced with tuples or some kind of proper collection.
This is reasonably pleasant. It's not much code to sort out the special cases of playing cards. We will see how it could be modied or expanded if we need to alter the Card class hierarchy to add additional subclasses of Card. It does feel odd to map a rank value to a class object and just one of the two arguments to that class initializer. It seems more sensible to map the rank to a simple class or function object without the clutter of providing some (but not all) of the arguments.
Chapter 1
The following is a mapping from rank to a partial() function that can be used for object construction:
from functools import partial part_class= { 1: partial(AceCard,'A'), 11: partial(FaceCard,'J'), 12: partial(FaceCard,'Q'), 13: partial(FaceCard,'K'), }.get(rank, partial(NumberCard, str(rank))) return part_class( suit )
The mapping associates a rank object with a partial() function that is assigned to part_class. This partial() function can then be applied to the suit object to create the nal object. The use of partial() functions is a common technique for functional programming. It works in this specic situation where we have a function instead of an object method. In general, however, partial() functions aren't helpful for most object-oriented programming. Rather than create partial() functions, we can simply update the methods of a class to accept the arguments in different combinations. A partial() function is similar to creating a uent interface for object construction.
The __init__() Method 11:(FaceCard,'J'), 12:(FaceCard,'Q'), 13:(FaceCard,'K'), }.get(rank, (NumberCard, str(rank))) return self def suit( self, suit ): return self.class_( self.rank_str, suit )
The rank() method updates the state of the constructor, and the suit() method actually creates the nal Card object. This factory class can be used as follows:
card8 = CardFactory() deck8 = [card8.rank(r+1).suit(s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]
First, we create a factory instance, then we use that instance to create Card instances. This doesn't materially change how __init__() itself works in the Card class hierarchy. It does, however, change the way that our client application creates objects.
Chapter 1 self.rank= "A" self.hard, self.soft = 1, 11 class FaceCard( Card ): def __init__( self, rank, suit ): self.suit= suit self.rank= {11: 'J', 12: 'Q', 13: 'K' }[rank] self.hard = self.soft = 10
This is still clearly polymorphic. The lack of a truly common initialization, however, leads to some unpleasant redundancy. What's unpleasant here is the repeated initialization of suit. This must be pulled up into the superclass. We can have each __init__() subclass make an explicit reference to the superclass. This version of the Card class has an initializer at the superclass level that is used by each subclass, as shown in the following code snippet:
class Card: def __init__( self, rank, suit, hard, soft ): self.rank= rank self.suit= suit self.hard= hard self.soft= soft class NumberCard( Card ): def __init__( self, rank, suit ): super().__init__( str(rank), suit, rank, rank ) class AceCard( Card ): def __init__( self, rank, suit ): super().__init__( "A", suit, 1, 11 ) class FaceCard( Card ): def __init__( self, rank, suit ): super().__init__( {11: 'J', 12: 'Q', 13: 'K' }[rank], suit, 10, 10 )
We've provided __init__() at both the subclass and superclass level. This has the small advantage that it simplies our factory function, as shown in the following code snippet:
def card10( rank, suit ): if rank == 1: return AceCard( rank, suit ) elif 2 <= rank < 11: return NumberCard( rank, suit ) elif 11 <= rank < 14: return FaceCard( rank, suit ) else: raise Exception( "Rank out of range" )
[ 37 ]
Simplifying a factory function should not be our focus. We can see from this variation that we've created rather complex __init__() methods for a relatively minor improvement in a factory function. This is a common trade-off.
Factory functions encapsulate complexity There's a trade-off that occurs between sophisticated __init__() methods and factory functions. It's often better to stick with more direct but less programmer-friendly __init__() methods and push the complexity into factory functions. A factory function works well if you wish to wrap and encapsulate the construction complexities.
Before designing a new class, we need to ask this question: is using a simple
We can use random.shuffle() to shufe the deck and deck.pop() to deal cards into a player's Hand. Some programmers rush to dene 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 simplied, 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 denition 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.
[ 38 ]
Chapter 1
To design a collection of objects, we have the following three general design strategies: Wrap: This design pattern is an existing collection denition. This might be an example of the Facade design pattern. Extend: This design pattern is an existing collection class. This is ordinary subclass denition. 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.
We've dened 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.
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 shufe the cards. The pop() method is simply inherited from list and works perfectly. Other methods inherited from the list also work.
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.
[ 40 ]
Chapter 1
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.
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 difcult 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.
[ 41 ]
We could try to create a uent 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 uent 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.
This initialization sets all of the instance variables in a single step. The other methods are simply copies of the previous class denition. We can build a Hand2 object in two ways. This rst 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() ) [ 42 ]
Chapter 1
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 benet from a way of building a composite object in a single, simple evaluation.
Each method requires the current Hand as an argument value. The decisions are based on the available information; that is, on the dealer's cards and the player's cards. We can build a single instance of this strategy for use by various Player instances as shown in the following code snippet:
dumb = GameStrategy()
We can imagine creating a family of related strategy classes, each one using different rules for the various decisions a player is offered in blackjack.
The Table class requires the following sequence of events by the Player instances: The player must place an initial bet based on the betting strategy. The player will then receive a hand. If the hand is splittable, the player must decide to split or not based on the play strategy. This can create additional Hand instances. In some casinos, the additional hands are also splittable. For each Hand instance, the player must decide to hit, double, or stand based on the play strategy. The player will then receive payouts, and they must update their betting strategy based on their wins and losses.
From this, we can see that the Table class has a number of API methods to receive a bet, create a Hand object, offer a split, resolve each hand, and pay off the bets. This is a large object that tracks the state of play with a collection of Players. The following is the beginning of a Table class that handles the bets and cards:
class Table: def __init__( self ): self.deck = Deck() def place_bet( self, amount ): print( "Bet", amount ) def get_hand( self ): try: self.hand= Hand2( d.pop(), d.pop(), d.pop() ) self.hole_card= d.pop() except IndexError: # Out of cards: need to shuffle. self.deck= Deck() return self.get_hand() print( "Deal", self.hand ) return self.hand def can_insure( self, hand ): return hand.dealer_card.insure
The Table class is used by the Player class to accept a bet, create a Hand object, and determine if theinsurance bet is in play for this hand. Additional methods can be used by the Player class to get cards and determine the payout. The exception handling shown in get_hand() is not a precise model of casino play. This may lead to minor statistical inaccuracies. A more accurate simulation requires developing a deck that reshufes itself when empty instead of raising an exception.
[ 44 ]
Chapter 1
In order to interact properly and simulate realistic play, the Player class needs a betting strategy. The betting strategy is a stateful object that determines the level of the initial bet. The various betting strategies generally change the bet based on whether the game was a win or a loss. Ideally, we'd like to have a family of betting strategy objects. Python has a module with decorators that allows us to create an abstract superclass. An informal approach to creating Strategy objects is to raise an exception for methods that must be implemented by a subclass. We've dened an abstract superclass as well as a specic subclass as follows to dene a at betting strategy:
class BettingStrategy: def bet( self ): raise NotImplementedError( "No bet method" ) def record_win( self ): pass def record_loss( self ): pass class Flat(BettingStrategy): def bet( self ): return 1
The superclass denes the methods with handy default values. The basic bet() method in the abstract superclass raises an exception. The subclass must override the bet() method. The other methods can be left to provide the default values. Given the game strategy in the previous section plus the betting strategy here, we can look at more complex __init__() techniques surrounding the Player class. We can make use of the abc module to formalize an abstract superclass denition. It would look like the following code snippet:
import abc class BettingStrategy2(metaclass=abc.ABCMeta): @abstractmethod def bet( self ): return 1 def record_win( self ): pass def record_loss( self ): pass
[ 45 ]
This has the advantage that it makes the creation of an instance of BettingStrategy2, or any subclass that failed to implement bet(), impossible. If we try to create an instance of this class with an unimplemented abstract method, it will raise an exception instead of creating an object.
super().bet().
And yes, the abstract method has an implementation. It can be accessed via
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. There are several overall design patterns that have multiple ways to build an object. One design pattern is complex __init__() that is called multi-strategy initialization. Also, there are multiple class-level (static) constructor methods. These are incompatible approaches. They have radically different interfaces.
Avoid clone methods A clone method that unnecessarily duplicates an object is rarely needed in Python. Using cloning may be an indication of failure to understand the object-oriented design principles available in Python. A clone method encapsulates the knowledge of object creation in the wrong place. The source object that's being cloned cannot know about the structure of the target object that was built from the clone. However, the reverse (targets having knowledge about a source) is acceptable if the source provides a reasonably well-encapsulated interface.
The examples we have shown here are effectively cloning because they're so simple. We'll expand on them in the next chapter. However, to show ways in which these fundamental techniques are used to do more than trivial cloning, we'll look at turning a mutable Hand object into a frozen, immutable Hand object. The following is an example of a Hand object that can be built in either of the two ways:
class Hand3: def __init__( self, *args, **kw ): if len(args) == 1 and isinstance(args[0],Hand3): # Clone an existing hand; often a bad idea [ 46 ]
Chapter 1 other= args[0] self.dealer_card= other.dealer_card self.cards= other.cards else: # Build a fresh, new hand. dealer_card, *cards = args self.dealer_card= dealer_card self.cards= list(cards)
In the rst 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. This parallels the way a frozenset object can be built from individual items or an existing set object. We look more at creating immutable objects in the next chapter. Creating a new Hand from an existing Hand allows us to create a memento of a Hand object using a construct like the following code snippet:
h = Hand( deck.pop(), deck.pop(), deck.pop() ) memento= Hand( h )
We saved the Hand object in the memento variable. This can be used to compare the nal with the original hand that was dealt, or we can freeze it for use in a set or mapping too.
The __init__() Method self.dealer_card= other.dealer_card self.cards= [other.cards[kw['split']], card] elif len(args) == 3: # Build a fresh, new hand. dealer_card, *cards = args self.dealer_card= dealer_card self.cards= list(cards) else: raise TypeError( "Invalid constructor args={0!r} kw={1!r}".format(args, kw) ) def __str__( self ): return ", ".join( map(str, self.cards) )
This design involves getting extra cards to build proper, split hands. When we create one Hand4 object from another Hand4 object, we provide a split keyword argument that uses the index of the Card class from the original Hand4 object. The following code snippet shows how we'd use this to 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 and 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.
[ 48 ]
Chapter 1
The following is a version of Hand with static methods that can be used to build new instances of Hand from an existing Hand instance:
class Hand5: def __init__( self, dealer_card, *cards ): self.dealer_card= dealer_card self.cards = list(cards) @staticmethod def freeze( other ): hand= Hand5( other.dealer_card, *other.cards ) return hand @staticmethod def split( other, card0, card1 ): hand0= Hand5( other.dealer_card, other.cards[0], card0 ) hand1= Hand5( other.dealer_card, other.cards[1], card1 ) return hand0, hand1 def __str__( self ): return ", ".join( map(str, self.cards) )
One method freezes or creates a memento version. The other method splits a Hand5 instance to create two new child instances of Hand5. This is considerably more readable and preserves the use of the parameter names to explain the interface. The following code snippet shows how we can split a Hand5 instance with this version of the class:
d = Deck() h = Hand5( d.pop(), d.pop(), d.pop() ) s1, s2 = Hand5.split( h, d.pop(), d.pop() )
We created an initial h instance of Hand5, split it into two other hands, s1 and s2, and dealt an additional Card class into each. The split() static method is much simpler than the equivalent functionality implemented via __init__(). However, it doesn't follow the pattern of creating a fronzenset object from an existing set object.
The __init__() Method def __init__( self, table, bet_strategy, game_strategy ): self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table= table def game( self ): self.table.place_bet( self.bet_strategy.bet() ) self.hand= self.table.get_hand() if self.table.can_insure( self.hand ): if self.game_strategy.insurance( self.hand ): self.table.insure( self.bet_strategy.bet() ) # Yet more... Elided for now
The __init__() method for Player seems to do little more than bookkeeping. We're simply transferring named parameters to same-named instance variables. If we have numerous parameters, simply transferring the parameters into the internal variables will amount to a lot of redundant-looking code. We can use this Player class (and related objects) as follows:
table = Table() flat_bet = Flat() dumb = GameStrategy() p = Player( table, flat_bet, dumb ) p.game()
We can provide a very short and very exible initialization by simply transferring keyword argument values directly into the internal instance variables. The following is a way to build a Player class using keyword argument values:
class Player2: def __init__( self, **kw ): """Must provide table, bet_strategy, game_strategy.""" self.__dict__.update( kw ) def game( self ): self.table.place_bet( self.bet_strategy.bet() ) self.hand= self.table.get_hand() if self.table.can_insure( self.hand ): if self.game_strategy.insurance( self.hand ): self.table.insure( self.bet_strategy.bet() ) # etc.
This sacrices a great deal of readability for succinctness. It crosses over into a realm of potential obscurity.
[ 50 ]
Chapter 1
Since the __init__() method is reduced to one line, it removes a certain level of "wordiness" from the method. This wordiness, however, is transferred to each individual object constructor expression. We have to add the keywords to the object initialization expression since we're no longer using positional parameters, as shown in the following code snippet:
p2 = Player2( table=table, bet_strategy=flat_bet, game_strategy=dumb )
Why do this? It does have a potential advantage. A class dened like this is quite open to extension. We can, with only a few specic worries, supply additional keyword parameters to a constructor. The following is the expected use case:
>>> p1= Player2( table=table, bet_strategy=flat_bet, game_ strategy=dumb) >>> p1.game()
We've added a log_name attribute without touching the class denition. This can be used, perhaps, as part of a larger statistical analysis. The Player2.log_name attribute can be used to annotate logs or other collected data. We are limited in what we can add; we can only add parameters that fail to conict with the names already in use within the class. Some knowledge of the class implementation is required to create a subclass that doesn't abuse the set of keywords already in use. Since the **kw parameter provides little information, we need to read carefully. In most cases, we'd rather trust the class to work than review the implementation details. This kind of keyword-based initialization can be done in a superclass denition to make it slightly simpler for the superclass to implement subclasses. We can avoiding writing an additional __init__() method in each subclass when the unique feature of the subclass involves simple new instance variables. The disadvantage of this is that we have obscure instance variables that aren't formally documented via a subclass denition. If it's only one small variable, an entire subclass might be too much programming overhead to add a single variable to a class. However, one small variable often leads to a second and a third. Before long, we'll realize that a subclass would have been smarter than an extremely exible superclass.
[ 51 ]
We can (and should) hybridize this with a mixed positional and keyword implementation as shown in the following code snippet:
class Player3( Player ): def __init__( self, table, bet_strategy, game_strategy, **extras ): self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table= table self.__dict__.update( extras )
This is more sensible than a completely open denition. We've made the required parameters positional parameters. We've left any nonrequired parameters as keywords. This claries the use of any extra keyword arguments given to the __ init__() method. This kind of exible, keyword-based initialization depends on whether we have relatively transparent class denitions. This openness to change requires some care to avoid debugging name clashes because the keyword parameter names are open-ended.
Chapter 1
We write a casino game simulation in order to experiment with endless variations on GameStrategy. These are so simple (merely four methods) that there's little real benet from inheritance from the superclass. We could dene the classes independently, lacking an overall superclass. The initialization error-checking shown in this example would force us to create subclasses merely to pass the error check. No usable code is inherited from the abstract superclass. One of the biggest duck typing issues surrounds numeric types. Different numeric types will work in different contexts. Attempts to validate the types of arguments may prevent a perfectly sensible numeric type from working properly. When attempting validation, we have the following two choices in Python: We write validation so that a relatively narrow collection of types is permitted, and someday the code will break because a new type that would have worked sensibly is prohibited We eschew validation so that a broad collection of types is permitted, and someday the code will break because a type that would not work sensibly was used
Note that both are essentially the same. The code could perhaps break someday. It either breaks because a type was prevented from being used even though it's sensible or a type that's not really sensible was used.
Just allow it Generally, it's considered better Python style to simply permit any type of data to be used. We'll return to this in Chapter 4, The ABCs of Consistent Design.
The question is this: why restrict potential future use cases? And the usual answer is that there's no good reason to restrict potential future use cases. Rather than prevent a sensible, but possibly unforeseen, use case, we can provide documentation, testing, and debug logging to help other programmers understand any restrictions on the types that can be processed. We have to provide the documentation, logging, and test cases anyway, so there's minimal additional work involved. The following is an example docstring that provides the expectations of the class:
class Player: def __init__( self, table, bet_strategy, game_strategy ): [ 53 ]
The __init__() Method """Creates a new player associated with a table, and configured with proper betting and play strategies :param table: an instance of :class:`Table` :param bet_strategy: an instance of :class:`BettingStrategy` :param game_strategy: an instance of :class:`GameStrategy` """ self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table= table
The programmer using this class has been warned about what the type restrictions are. The use of other types is permitted. If the type isn't compatible with the expected type, then things will break. Ideally, we'll use too like unittest or doctest to uncover the breakage.
[ 54 ]
Chapter 1
Conventionally, we'll treat some names in a way that's less public. They're generally implementation details that are subject to change without notice, but there's no formal notion of private.
Names that begin with _ are honored as less public by some parts of Python. The help() function generally ignores these methods. Tools such as Sphinx can conceal these names from documentation. Python's internal names begin (and end) with __. This is how Python internals are kept from colliding with application features above the internals. The collection of these internal names is fully dened by the language reference. Further, there's no benet to trying to use __ to attempt to create a "super private" attribute or method in our code. All that happens is that we create a potential future problem if a release of Python ever starts using a name we chose for internal purposes. Also, we're likely to run afoul of the internal name mangling that is applied to these names. The rules for the visibility of Python names are as follows: Most names are public. Names that start with _ are somewhat less public. Use them for implementation details that are truly subject to change. Names that begin and end with __ are internal to Python. We never make these up; we use the names dened by the language reference.
Generally, the Python approach is to register the intent of a method (or attribute) using documentation and a well-chosen name. Often, the interface methods will have elaborate documentation, possibly including doctest examples, while the implementation methods will have more abbreviated documentation and may not have doctest examples. For programmers new to Python, it's sometimes surprising that privacy is not more widely used. For programmers experienced in Python, it's surprising how many brain calories get burned sorting out private and public declarations that aren't really very helpful because the intent is obvious from the method names and the documentation.
Summary
In this chapter, we have reviewed the various design alternatives of the __init__() method. In the next chapter, we will take a look at the special methods, along with a few advanced ones as well.
[ 55 ]
Alternatively, you can buy the book from Amazon, BN.com, Computer Manuals and most internet book retailers.
www.PacktPub.com