Leveraging __init__() via a factory function
We can build a complete deck of cards via a factory function. This beats enumerating all 52 cards. In Python, we have two common approaches to factories as follows:
We define a function that creates objects of the required classes.
We define a class that has methods for creating objects. This is the full factory design pattern, as described in books on design patterns. In languages such as Java, a factory class hierarchy is required because the language doesn't support standalone functions.
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.
Note
While this is a book about object-oriented programming, a function really is fine. 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 definitions 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 definitions 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 definitions 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 ) 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.
Faulty factory design and the vague else clause
Note the structure of the if
statement in the card()
function. We did not use a catch-all else
clause to do any processing; we merely raised an exception. The use of a catch-all else
clause is subject to a tiny scrap of debate.
On the one hand, it can be argued that the condition that belongs on an else
clause should never be left unstated because it may hide subtle design errors. On the other hand, some else
clause conditions are truly obvious.
It's important to avoid the vague else
clause.
Consider the following variant on this factory function definition:
def card2( rank, suit ): if rank == 1: return AceCard( 'A', suit ) elif 2 <= rank < 11: return NumberCard( str(rank), suit ) else: name = { 11: 'J', 12: 'Q', 13: 'K' }[rank] return FaceCard( name, suit )
The following is what will happen when we try to build a deck:
deck2 = [card2(rank, suit) for rank in range(13) for suit in (Club, Diamond, Heart, Spade)]
Does it work? What if the if
conditions were more complex?
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.
Tip
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.
Simplicity and consistency using elif sequences
Our factory function, card()
, is a mixture of two very common factory design patterns:
An
if-elif
sequenceA mapping
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 AceCard( 'A', suit ) elif 2 <= rank < 11: return NumberCard( str(rank), suit ) elif rank == 11: return FaceCard( 'J', suit ) elif rank == 12: return FaceCard( 'Q', suit ) elif rank == 13: return FaceCard( 'K', suit ) else: raise Exception( "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.
Simplicity using mapping and class objects
In some cases, we can use a mapping instead of a chain of elif
conditions. It's possible to find conditions that are so complex that a chain of elif
conditions is the only sensible way to express them. For simple cases, however, a mapping often works better and can be easy to read.
Since class
is a first-class object, we can easily map from the rank
parameter to the class that must be constructed.
The following is a Card
factory that uses only a mapping:
def card4( rank, suit ): class_= {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, NumberCard) return class_( rank, suit )
We've mapped the rank
object to a class. Then, we applied the class to the rank
and suit
values to build the final 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 deficiency. 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. Thepartial()
function is a feature of thefunctools
module.We can also consider modifying our class definition to fit more readily with this kind of mapping. We'll look at this alternative in the next section on pushing
__init__()
into the subclass definitions.
We'll look at each of these with a concrete example.
Two parallel mappings
The following is the essence of the two parallel mappings solution:
class_= {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard }.get(rank, NumberCard) rank_str= {1:'A', 11:'J', 12:'Q', 13:'K'}.get(rank,str(rank)) return class_( rank_str, suit )
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.
Tip
Don't use parallel structures
Two parallel structures should be replaced with tuples or some kind of proper collection.
Mapping to a tuple of values
The following is the essence of how mapping is done to a two-tuple:
class_, rank_str= { 1: (AceCard,'A'), 11: (FaceCard,'J'), 12: (FaceCard,'Q'), 13: (FaceCard,'K'), }.get(rank, (NumberCard, str(rank))) return class_( rank_str, suit )
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 modified 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.
The partial function solution
Rather than map to a two-tuple of function and one of the arguments, we can create a partial()
function. This is a function that already has some (but not all) of its arguments provided. We'll use the partial()
function from the functools
library to create a partial of a class with the rank
argument.
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 final object. The use of partial()
functions is a common technique for functional programming. It works in this specific 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 fluent interface for object construction.
Fluent APIs for factories
In some cases, we design a class where there's a defined order for method usage. Evaluating methods sequentially is very much like creating a partial()
function.
We might have x.a().b()
in an object notation. We can think of it as . The x.a()
function is a kind of partial()
function that's waiting for b()
. We can think of this as if it were .
The idea here is that Python offers us two alternatives for managing a state. We can either update an object or create a partial()
function that is (in a way) stateful. Because of this equivalence, we can rewrite a partial()
function into a fluent factory object. We make the setting of the rank
object a fluent method that returns self
. Setting the suit
object will actually create the Card
instance.
The following is a fluent Card
factory class with two method functions that must be used in a specific order:
class CardFactory: def rank( self, rank ): self.class_, self.rank_str= { 1:(AceCard,'A'), 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 final 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.