Yet more __init__() techniques
We'll take a look at a few other, more advanced __init__()
techniques. These aren't quite so universally useful as the techniques in the previous sections.
The following is a definition for the Player
class that uses two strategy objects and a table
object. This shows an unpleasant-looking __init__()
method:
class Player: 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 flexible 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 sacrifices a great deal of readability for succinctness. It crosses over into a realm of potential obscurity.
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 defined like this is quite open to extension. We can, with only a few specific 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()
The following is a bonus use case:
>>> p2= Player2( table=table, bet_strategy=flat_bet, game_strategy=dumb, log_name="Flat/Dumb" ) >>> p2.game()
We've added a log_name
attribute without touching the class definition. 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 conflict 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 definition 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 definition. 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 flexible superclass.
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 definition. We've made the required parameters positional parameters. We've left any nonrequired parameters as keywords. This clarifies the use of any extra keyword arguments given to the __init__()
method.
This kind of flexible, keyword-based initialization depends on whether we have relatively transparent class definitions. This openness to change requires some care to avoid debugging name clashes because the keyword parameter names are open-ended.