Book Image

Mastering Object-oriented Python

By : Steven F. Lott, Steven F. Lott
Book Image

Mastering Object-oriented Python

By: Steven F. Lott, Steven F. Lott

Overview of this book

Table of Contents (26 chapters)
Mastering Object-oriented Python
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Some Preliminaries
Index

Implementing __init__() in each subclass


As we look at the factory functions for creating Card objects, we see some alternative designs for the Card class. We might want to refactor the conversion of the rank number so that it is the responsibility of the Card class itself. This pushes the initialization down into each subclass.

This often requires some common initialization of a superclass as well as subclass-specific initialization. We need to follow the Don't Repeat Yourself (DRY) principle to keep the code from getting cloned into each of the subclasses.

The following is an example where the initialization is the responsibility of each subclass:

class Card:
    pass
class NumberCard( Card ):
    def  __init__( self, rank, suit ):
        self.suit= suit
        self.rank= str(rank)
        self.hard = self.soft = rank
class AceCard( Card ):
    def  __init__( self, rank, suit ):
        self.suit= suit
        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 simplifies 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" )

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.

Tip

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.