This article mainly introduces the advanced application of parsing the _ init _ () method in Python, including more complex usage in ING and elif sequences, for more information, see
Use the factory function to use _ init _ ().
We can use factory functions to build a complete set of playing cards. This is much better than enumerating all 52 playing cards. in Python, we have two common factory methods:
- Define a function that creates the object of the required class.
- Define a class that has a method to create an object. This is a complete factory design model, as described in the design patterns book. In languages such as Java, the factory class hierarchy is required because the language does not support independent functions.
- In Python, classes are not required. Only when the relevant factory is very complicated will it show its advantages. The advantage of Python is that when a simple function can do better, we will not force the use of class hierarchies.
- Although this is a book about object-oriented programming, functions are really good. This is common and authentic in Python.
If necessary, we can always rewrite a function as an appropriate Callable object. We can reconstruct a Callable object into our factory class hierarchy. In Chapter 5 "use callable objects and context", we will learn callable objects.
Generally, a class definition has the advantage of implementing code reuse through inheritance. Factory functions are used to encapsulate the hierarchical structure of some target classes and the construction of complex objects. If we have a factory class, we can add subclasses to the factory class when extending the hierarchy of the target class. This provides us with a polymorphism factory class; different factory class definitions have the same method signature and can be used in turn.
This type of horizontal polymorphism is very useful for static compilation languages such as Java or C ++. The compiler can solve the code generation details of classes and methods.
If the selected factory definition cannot reuse any code, the class hierarchy in Python will not be helpful. We can simply use functions with the same signature.
The following are the factory functions of various Card subclass:
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 the Card class through the rank and suit objects of the numerical type. We can now build cards very easily. We have encapsulated the constructor into a single factory function that allows applications to build without knowing how the precise class hierarchy and polymorphism design works.
The following is an example of how to build a deck using this factory function:
deck = [card(rank, suit) for rank in range(1,14) for suit in (Club, Diamond, Heart, Spade)]
It enumerates all the card values and colors to create the complete 52 cards.
1. incorrect factory design and fuzzy else clause
Note the if statement structure in the card () function. We didn't use the "all-encompassing" else clause for any processing; we just throw an exception. Using the "all-encompassing" else clause will lead to a small debate.
On the one hand, the conditions that fall into the else clause cannot be self-evident because they may hide subtle design errors. On the other hand, some else clauses are indeed obvious.
It is important to avoid ambiguous else clauses.
Consider the following variants of the 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 happens when we try to create a full deck:
deck2 = [card2(rank, suit) for rank in range(13) for suit in (Club, Diamond, Heart, Spade)]
Does it work? What if the if condition is more complex?
Some programmers can understand this if statement when scanning. It will be difficult for others to determine if all the situations are correctly executed.
For advanced Python programming, we should not leave it to the reader to deduce whether the condition is applicable to the else clause. The Cainiao condition should be obvious, at least shown.
When to use "all-encompassing" else
Try to use less. Use it only when the conditions are obvious. In case of any doubt, an explicit exception is thrown.
Avoid ambiguous else clauses.
2. use the elif sequence in a simple and consistent manner
Our factory Function card () is a mixture of two common factory design patterns:
For the sake of simplicity, it is best to focus on one or more of these technologies.
We can always use ING to replace the elif condition. (Yes, always. But the opposite is incorrect; changing the elif condition to the ING will be challenging .)
The following is a Card factory without a ING:
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 have rewritten the card () factory function. The ING has been converted into an additional elif clause. This function has the advantage that it is more consistent than the previous version.
3. simple use of ING and class objects
In some examples, we can use ING to replace a series of elif conditions. It is likely that the conditions are too complex. in this case, it may be wise to use a series of elif conditions to express them. For simple examples, the ING can be better and more readable in any case.
Because class is the best object, we can easily map the rank parameter to the constructed class.
Here is the map-only Card factory:
def card4(rank, suit): class_= {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, NumberCard) return class_(rank, suit)
We have mapped the rank object to the class. Then, we pass the rank value and suit value to the class to create the final Card instance.
We 'd better use the defaultdict class. In any case, it is no longer easier for trivial static mappings. It looks like the following code snippet:
Defaultdict (lambda: NumberCard, {1: AceCard, 11: FaceCard, 12: FaceCard, 12: FaceCard })
Note: The defaultdict class must be a zero-parameter function by default. We have used lambda to create necessary functions to encapsulate constants. This function has some defects in any case. In our previous versions, the conversion from 1 to A and from 13 to K is missing. When we try to add these features, problems will occur.
We need to modify the Card ING to provide the Card subclass that can be the same as the rank object of the string version. What else can we do for the ING of the two parts? There are four common solutions:
- You can perform two parallel mappings. We do not recommend this, but we will emphasize what is not desirable.
- You can map binary groups. This also has some disadvantages.
- Can be mapped to the partial () function. The partial () function is a feature of the functools module.
- You can consider modifying our class definition, which is easier to map. You can add _ init _ () to the subclass definition in the next section.
Let's take a look at each specific example.
3.1. two parallel mappings
The following are the key code for the two parallel ing solutions:
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 repeated ing key 1, 11, 12, and 13 sequences. Repetition is bad, because the parallel structure remains this way after software updates.
Do not use parallel structures
The parallel structure must be replaced by tuples or some other suitable sets.
3.2. values mapped to tuples
The following is the key code for binary group ING:
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 quite good. There is no need for too much code to classify special situations in cards. When we need to change the Card class hierarchy to add additional Card subclass, we will see how it is modified or expanded.
It is really strange to map the rank value to a class object, and only one of the two parameters required for class initialization. It seems more reasonable to map card values to a simple class or function objects that do not provide some confusing parameters (but not all.
3.3. partial function solution
Compared to one of the functions and parameters mapped to a binary group, we can create a partial () function. This is a function that provides some (but not all) parameters. We will use the partial () function in the functools library to create a partial class with the rank parameter.
The following is a rank ing rank to partial () function, which can be used for object creation:
from functools import partialpart_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)
Ing associates the rank object with the partial () function and assigns it to part_class. This partial () function can be applied to the suit object to create the final object. Partial () functions are common functional programming techniques. It is used when we have a function to replace the object method.
But in general, the partial () function does not help most object-oriented programming. Compared with the partial () function, we can simply update the class method to accept parameters of different combinations. The partial () function is similar to creating a coherent interface for object construction.
3.4. consistent factory interface
In some cases, the classes we designed define the sequence for the use of methods, and the Order of the measures is similar to that of the partial () function.
In an object representation, we may have x. a (). B (). We can regard it as x (a, B ). X. a () function is a type of partial () function waiting for B. We can think of it as x (a) (B.
The idea here is that Python provides two options for us to manage the status. We can update objects and create stateful (to some extent) partial () functions. Due to this equivalence, we can rewrite the partial () function to a coherent factory object. We set the rank object to a coherent method to return self. Set the suit object to create a Card instance.
The following is a coherent Card Factory class. There are 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 status of the constructor. 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, and then we use that instance to create a Card instance. This does not substantially change how _ init _ () operates in the Card class hierarchy. However, it does change the way our client applications create objects.