Advanced Class Definition

This section builds up some additional class definition techniques. We describe the basics of inheritance in Inheritance. We turn to a specific inheritance technique, polymorphism in Polymorphism. There are some class-related functions, which we describe in Built-in Functions. We’ll look at some specific class initializer technique in Initializer Techniques. We include a digression on design approaches in Design Approaches. In Class Variables we provide information on class-level variables, different from instance variables. We conclude this chapter with some style notes in Style Notes.

Inheritance

In Semantics we identified four important features of objects.

  • Identity.
  • Classification.
  • Inheritance.
  • Polymorphism.

The point of inheritance is to allow us to create a subclass which inherits all of the features of a superclass. The subclass can add or replace method functions of the superclass. This is typically used by defining a general-purpose superclass and creating specialized subclasses that all inherit the general-purpose features but add special-purposes features of their own.

We do this by specifying the parent class when we create a subclass.

class subclass ( superclass ) :
    suite

All of the methods of the superclass are, by definition, also part of the subclass. Often the suite of method functions will add to or override the definition of a parent method.

If we omit providing a superclass, we create a classical class definition, where the Python type is instance; we have to do additional processing to determine the actual type. Generally, we should avoid this kind of class definition. It works, but isn’t ideal.

When we use object as the superclass, the Python type is reported more simply as the appropriate class object. As a general principle, every class definition should be a subclass of object, either directly or indirectly.

Important

Python 3

In Python 3, this distinction will be removed. A class with no explicit superclass will still be a subclass of object.

Extending a Class. There are two trivial subclassing techniques. One defines a subclass which adds new methods to the superclass. The other overrides a superclass method. The overriding technique leads to two classes which are polymorphic because they have the same interface. We’ll return to polymorphism in Polymorphism.

Here’s a revised version of our basic Dice class and a subclass to create CrapsDice.

crapsdice.py

#!/usr/bin/env python
"""Define a Die, Dice and CrapsDice."""

class Die(object):
    """Simulate a 6-sided die."""
    def __init__( self ):
        self.domain= range(1,7)
    def roll( self ):
        self.value= random.choice(self.domain)
        return self.value
    def getValue( self ):
        return self.value

class Dice( object ):
    """Simulate a pair of dice."""
    def __init__( self ):
        "Create the two Die objects."
        self.myDice = ( Die(), Die() )
    def roll( self ):
        "Return a random roll of the dice."
        for d in self.myDice:
            d.roll()
    def getTotal( self ):
        "Return the total of two dice."
        return self.myDice[0].value + self.myDice[1].value
    def getTuple( self ):
        "Return a tuple of the dice."
        return self.myDice

class CrapsDice( Dice ):
    """Extends Dice to add features specific to Craps."""
    def hardways( self ):
        """Returns True if this was a hardways roll?"""
        return self.myDice[0].value == self.myDice[1].value
    def isPoint( self, value ):
        """Returns True if this roll has the given total"""
        return self.getTotal() == value

The CrapsDice class contains all the features of Dice as well as the additional features we added in the class declaration.

We can, for example, evaluate the roll() and hardways() methods of CrapsDice. The roll() method is inherited from Dice, but the hardways() method is a direct part of CrapsDice.

Adding Instance Variables. Adding new instance variables requires that we extend the __init__() method.

In this case our subclass __init__() function must start out doing everything the superclass __init__() function does, and then creates a few more attributes.

Python provides us the super() function to help us do this. We can use super() to distinguish between method functions with the same name defined in the superclass and extended in a subclass.

super(type, variable) → bound object
This will do two things: locate the superclass of the given type, and it then bind the given variable to create an object of the superclass. This is often used to call a superclass method from within a subclass: super( classname ,self).method()

Here’s a template that shows how a subclass __init__() method uses super() to evaluate the superclass __init__() method.

class Subclass( Superclass ):
    def __init__( self ):
        super(Subclass,self)__init__()
        # Subclass-specific stuff follows

This will bind our self variable to the parent class so that we can evaluate the parent class __init__() method. After that, we can add our subclass initialization.

We’ll look at additional techniques for creating very flexible __init__() methods in Initializer Techniques.

Various Kinds of Cards. Let’s look closely at the problem of cards in Blackjack. All cards have several general features: they have a rank and a suit. All cards have a point value. However, some cards use their rank for point value, other cards use 10 for their point value and the aces can be either 1 or 11, depending on the the rest of the cards in the hand. We looked at this in the Playing Cards and Decks exercise in Classes.

We can model this very accurately by creating a Card class that encapsulates the generic features of rank, suit and point value. Our class will have instance variables for these attribites. The class will also have two functions to return the hard value and soft value of this card. In the case of ordinary non-face, non-ace cards, the point value is always the rank. We can use this Card class for the number cards, which are most common.

class Card( object ):
    """A standard playing card for Blackjack."""
    def __init__( self, r, s ):
        self.rank, self.suit = r, s
        self.pval= r
    def __str__( self ):
        return "%2d%s" % ( self.rank, self.suit )
    def getHardValue( self ):
        return self.pval
    def getSoftValue( self ):
        return self.pval

We can create a subclass of Card which is specialized to handle the face cards. This subclass simply overrides the value of self.pval, using 10 instead of the rank value. In this case we want a FaceCard.__init__() method that uses the parent’s Card.__init__() method, and then does additional processing. The existing definitions of getHardValue() and getSoftValue() method functions, however, work fine for this subclass. Since Card is a subclass of object, so is FaceCard.

Additionally, we’d like to report the card ranks using letters (J, Q, K) instead of numbers. We can override the __str__() method function to do this translation from rank to label.

class FaceCard( Card ):
    """A 10-point face card: J, Q, K."""
    def __init__( self, r, s ):
        super(FaceCard,self).__init__( r, s )
        self.pval= 10
    def __str__( self ):
        label= ("J","Q","K")[self.rank-11]
        return "%2s%s" % ( label, self.suit )

We can also create a subclass of Card for Aces. This subclass inherits the parent class __init__() function, since the work done there is suitable for aces. The Ace class, however, provides a more complex algorithms for the getHardValue() and getSoftValue() method functions. The hard value is 1, the soft value is 11.

class Ace( Card ):
    """An Ace: either 1 or 11 points."""
    def __str__( self ):
        return "%2s%s" % ( "A", self.suit )
    def getHardValue( self ):
        return 1
    def getSoftValue( self ):
        return 11

Deck and Shoe as Collections of Cards. In a casino, we can see cards handled in a number of different kinds of collections. Dealers will work with a single deck of 52 cards or a multi-deck container called a shoe. We can also see the dealer putting cards on the table for the various player’s hands, as well as a dealer’s hand.

Each of these collections has some common features, but each also has unique features. Sometimes it’s difficult to reason about the various classes and discern the common features. In these cases, it’s easier to define a few classes and then refactor the common features to create a superclass with elements that have been removed from the subclasses. We’ll do that with Decks and Shoes.

We can define a Deck as a sequence of Cards. The deck.__init__() method function of Deck creates appropriate Cards of each subclass. These are Card objects in the range 2 to 10, FaceCard obejcts with ranks of 11 to 13, and Ace objects with a rank of 1.

class Deck( object ):
    """A deck of cards."""
    def __init__( self ):
        self.cards= []
        for suit in ( "C", "D", "H", "S" ):
            self.cards+= [Card(r,suit) for r in range(2,11)]
            self.cards+= [TenCard(r,suit) for r in range(11,14)]
            self.cards+= [Ace(1,suit)]
    def deal( self ):
        for c in self.cards:
            yield c

In this example, we created a single instance variable self.cards within each Deck instance. For dealing cards, we’ve provided a generator function which yields the Card objects in a random order. We’ve omitted the randomization from the deal() function; we’ll return to it in the exercises.

For each suit, we created the Cards of that suit in three steps.

  1. We created the number cards with a list comprehension to generate all ranks in the range 2 through 10.
  2. We created the face cards with a similar process, except we use the TenCard class constructor, since blackjack face cards all count as having ten points.
  3. Finally, we created a one-item list of an Ace instance for the given suit.

We can use Deck objects to create an multi-deck shoe. (A shoe is what dealers use in casinos to handle several decks of slippery playing cards.) The Shoe class will create six separate decks, and then merge all 312 cards into a single sequence.

class Shoe( object ):
    """Model a multi-deck shoe of cards."""
    def __init__( self, decks=6 ):
        self.cards= []
        for i in range(decks):
            d= Deck()
            self.cards += d.cards
    def deal( self ):
        for c in self.cards:
            yield c

For dealing cards, we’ve provided a generator function which yields the Cards in a random order. We’ve omitted the randomization from the deal() function; we’ll return to it in the exercises.

Factoring Out Common Features. When we compare Deck and Shoe, we see two obviously common features: they both have a collection of Cards, called self.cards. Also, they both have a deal() method which yields a sequence of cards.

We also see things which are different. The most obvious differences are details of initializing self.cards. It turns out that the usual procedure for dealing from a shoe involves shuffling all of the cards, but dealing from only four or five of the six available decks. This is done by inserting a marker one or two decks in from the end of the shoe.

In factoring out the common features, we have a number of strategies.

  • One of our existing classes is already generic-enough to be the superclass. In the Card example, we used the generic Card class as superclass for other cards as well as the class used to implement the number cards. In this case we will make concrete object instances from the superclass.
  • We may need to create a superclass out of our subclasses. Often, the superclass isn’t useful by itself; only the subclasses are really suitable for making concrete object instances. In this case, the superclass is really just an abstraction, it isn’t meant to be used by itself.

Here’s an abstract CardDealer from which we can subclass Deck and Shoe. Note that it does not create any cards. Each subclass must do that. Similarly, it can’t deal properly because it doesn’t have a proper shuffle() method defined.

class CardDealer( object ):
    def __init__( self ):
        self.cards= []
    def deal( self ):
        for c in self.shuffle():
            yield c
    def shuffle( self ):
        ...to be done in the exercises...

Python does not have a formal notation for abstract or concrete superclasses. When creating an abstract superclass it is common to return NotImplemented or raise NotImplementedError to indicate that a method must be overridden by a subclass.

We can now rewrite Deck as subclasses of CardDealer.

class Deck( CardDealer ):
    def __init__( self ):
        super(Deck,self).__init__()
        for s in ("C","D","H","S"):
        for suit in ( "C", "D", "H", "S" ):
            self.cards+= [Card(r,suit) for r in range(2,11)]
            self.cards+= [TenCard(r,suit) for r in range(11,14)]
            self.cards+= [Ace(1,suit)]

We can also rewrite Shoe as subclasses of CardDealer.

class Shoe( CardDealer ):
    def __init__( self, decks=6 ):
        CardDealer.__init__( self )
        for i in range(decks):
            d= Deck()
            self.cards += d.cards

The benefit of this is to assure that Deck and Shoe actually share common features. This is not “cut and paste” sharing. This is “by definition” sharing. A change to CardDealer will change both Deck and Shoe, assuring complete consistency.

Polymorphism

In Semantics we identified four important features of objects.

  • Identity.
  • Classification.
  • Inheritance.
  • Polymorphism.

Polymorphism exists when we define a number of subclasses which have commonly named methods. A function can use objects of any of the polymorphic classes without being aware that the classes are distinct.

In some languages, it is essential that the polymorphic classes have the same interface (or be subinterfaces of a common parent interface), or be subclasses of a common superclass. This is sometimes called “strong, hierarchical typing”, since the type rules are very rigid and follow the subclass/subinterface hierarchy.

Python implements something that is less rigid, often called “duck typing”. The phrase follows from a quote attributed to James Whitcomb Riley: “When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.” In short, two objects are effectively of the class Duck if they have a common collection of methods (walk, swim and quack, for example.)

When we look at the examples for Card, FaceCard, Ace in Inheritance, we see that all three classes have the same method names, but have different implementations for some of these methods. These three classes are polymorphic.

A client class like Hand can contain individual objects of any of the subclasses of Card. A function can evaluate these polymorphic methods without knowing which specific subclass is being invoked.

In our example, both FaceCard and Ace were subclasses of Card. This subclass relationship isn’t necesary for polymorphism to work correctly in Python. However, the subclass relationship is often an essential ingredient in an overall design that relies on polymorphic classes.

What’s the Benefit? If we treat all of the various subclasses of Card in a uniform way, we effectively delegate any special-case processing into the relevant subclass. We concentrate the implementation of a special case in exactly one place.

The alternative is to include if statements all over our program to enforce special-case processing rules. This diffusing of special-case processing means that many components wind up with an implicit relationship. For example, all portions of a program that deal with Cards would need multiple if statements to separate the number card points, face card points and ace points.

By making our design polymorphic, all of our subclasses of Card have ranks and suits, as well as hard and soft point values. We we can design the Deck and Shoe classes to deal cards in a uniform way. We can also design a Hand class to total points without knowing which specific class to which a Card object belongs.

Similarly, we made our design for Deck and Shoe classes polymorphic. This allows us to model one-deck blackjack or multi-deck blackjack with no other changes to our application.

The Hand of Cards. In order to completely model Blackjack, we’ll need a class for keeping the player and dealer’s hands. There are some differences between the two hands: the dealer, for example, only reveals their first card, and the dealer can’t split. There are, however, some important similarities. Every kind of Hand must determine the hard and soft point totals of the cards.

The hard point total for a hand is simply the hard total of all the cards. The soft total, on the other hand, is not simply the soft total of all cards. Only the Ace cards have different soft totals, and only one Ace can meaningfully contribute it’s soft total of 11. Generally, all cards provide the same hard and soft point contributions. Of the cards where the hard and soft values differ, only one such card needs to be considered.

Note that we are using the values of the getHardValue() and getSoftValue() methods. Since this test applies to all classes of cards, we preserve polymorphism by checking this property of every card. We’ll preserving just one of the cards with a soft value that is different from the hard value. At no time do use investigate the class of a Card to determine if the card is of the class Ace. Examining the class of each object needlessly constrains our algorithm. Using the polymorphic methods means that we can make changes to the class structure without breaking the processing of the Hand class.

Important

Pretty Poor Polymorphism

The most common indicator of poor use polymorphism is using the type(), isinstance() and issubclass() functions to determine the class of an object. These should used rarely, if at all. All processing should be focused on what is different about the objects, not the class to which an object belongs.

We have a number of ways to represent the presence of a Card with a distinct hard and soft value.

  • An attribute with the point difference (usually 10).
  • A collection of all Cards except for one Card with a point difference, and a single attribute for the extra card.

We’ll choose the first implementation. We can use use a sequence to hold the cards. When cards are added to the hand, the first card that returns distinct values for the hard value and soft value will be used to set a variable has keeps the hard vs. soft point difference.

hand.py

class Hand( object ):
    """Model a player's hand."""
    def __init__( self ):
        self.cards = [ ]
        self.softDiff= 0
    def addCard( self, aCard ):
        self.cards.append( aCard )
        if aCard.getHardValue() != aCard.getSoftValue():
            if self.softDiff == 0:
                self.softDiff= aCard.getSoftValue()-aCard.getHardValue()
    def points( self ):
        """Compute the total points of cards held."""
        p= 0
        for c in self.cards:
            p += c.getHardValue()
        if p + self.softDiff <= 21:
            return p + self.softDiff
        else:
            return p
  1. The __init__() special function creates the instance variable, self.cards, which we will use to accumulate the Card objects that comprise the hand. This also sets self.softDiff which is the difference in points between hard and soft hands. Until we have an Ace, the difference is zero. When we get an Ace, the difference will be 10.

  2. We provide an addCard() method that places an additional card into the hand. At this time, we examine the Card to see if the soft value is different from the hard value. If so, and we have not set the self.softDiff yet, we save this difference.

  3. The points() method evaluates the hand. It initializes the point count, p, to zero. We start a for-loop to assign each card object to c. We could, as an alternative, use a sum() function to do this.

    If the total with the self.softDiff is 21 or under, we have a soft hand, and these are the total points. If the total with the self.softDiff is over 21, we have a hard hand. The hard hand may total more than 21, in which case, the hand is bust.

Built-in Functions

There are two built in functions of some importance to object oriented programming. These are used to determine the class of an object, as well as the inheritance hierarchy among classes.

isinstance(object, type) → boolean
True if object is an instance of the given type or any of the subclasses of type.
issubclass(parameter, base) → boolean

True if class class is a subclass of class base.

This question is usually moot, because most programs are designed to provide the expected classes of objects. There are some occasions for deep paranoia; when working with untrusted software, your classes may need to be sure that other programmers are following the rules. In Java and C++, the compiler can check these situations. In Python, the compiler doesn’t check this, so we may want to include run-time checks.

super(type) → type
This will return the superclass of the given type.

All of the basic factory functions (str, int, float, long, complex, unicode, tuple, list, dict, set) are effectively class names. You can, therefore, use a test like isinstance( myParam ,int) to confirm that the argument value provided to this parameter is an integer.

An additional class, basestring is the parent class of both str and unicode.

The following example uses the isinstance() function to validate the type of argument values. First, we’ll define a Roulette wheel class, Wheel, and two subclasses, Wheel1 with a single zero and Wheel2 with zero and double zero.

wheel.py

import random
class Wheel( object ):
    def value( self ):
        return NotImplemented

class Wheel1( Wheel ):
    def value( self ):
        spin= random.randrange(37)
        return str(spin)

class Wheel2( Wheel ):
    def __init__( self ):
        self.values= ['00'] + map( str, range(37) )
    def value( self ):
        return random.randchoice( self.values )
  1. The Wheel class defines the interface for Roulette wheels. The actual class definition does nothing except show what the expected method functions should be. We could call this an abstract definition of a Wheel.
  2. The Wheel1 subclass uses a simple algorithm for creating the spin of a wheel. The value() method chooses a number between 0 and 36. It returns a string representation of the number. This has only a single zero.
  3. The Wheel2 subclass creates an instance variable, values, to contain all possible results. This includes the 37 values from 0 to 36, plus an additional ‘00’ value. The value() method chooses one of these possible results.

The following function expects that its parameter, w, is one of the subclasses of Wheel.

def simulate( w ):
    if not isinstance( w, Wheel ):
        raise TypeError( "Must be a subclass of Wheel" )
    for i in range(10):
        print w.value()

In this case, the simulate function checks its argument, w to be sure that it is a subclass of Wheel. If not, the function raises the built in TypeError.

An alternative is to use an assertion for this.

def simulate( w ):
    assert isinstance( w, Wheel )
    for i in range(10):
        print w.value()

Collaborating with max(), min() and sort()

The min() and max() functions can interact with our classes in relatively simple ways. Similarly, the sort() method of a list can also interact with our new class definitions.

In all three cases, a keyword parameter of key can be used to control which attributes are used for determining minimum, maximum or sort order.

The key parameter must be given a function, and that function is evaluated on each item that is being compared. Here’s a quick example.

class Boat( object ):
    def __init__( self, name, loa ):
        self.name= name
        self.loa= loa
def byName( aBoat ):
    return aBoat.name
def byLOA( aBoat ):
    return aBoat.loa
fleet = [ Boat("KaDiMa", 18 ), Boat( "Emomo", 21 ), Boat("Leprechaun", 30 ) ]
first= min( fleet, key=byName )
print "Alphabetically First:", first
longest= max( fleet, key=byLOA )
print "Longest:", longest

As min(), max() or sort traverse the sequence doing comparisons among the objects, they evaluate the key() function we provided. In this example, the provided function simply selects an attribute. Clearly the functions could do calculations or other operations on the objects.

Initializer Techniques

When we define a subclass, we are often extending some superclass to add features. One common design pattern is to do this by defining a subclass to use parameters in addition to those expected by the superclass. We must reuse the superclass constructor properly in our subclass.

Referring back to our Card and FaceCard example in Inheritance, we wrote an initializer in FaceCard that referred to Card.

The FaceCard.__init__() method evaluates super(FaceCard,self).__init__( rank, suit ). It passed the same arguments to the Card.__init__() method.

Note

Older Programs

In older programs, you’ll see an alternative to the super() function. Some classes will have an explicit call to Card.__init__( self, r, s ).

We’re naming the class (not an object of the class) and explicitly providing the self variable.

We can make use of the techniques covered in Advanced Parameter Handling For Functions to simplify our subclass initializer.

class FaceCard( Card ):
    """Model a 10-point face card: J, Q, K."""
    def __init__( self, * args ):
        super(FaceCard,self).__init__( * args )
        self.label= ("J","Q","K")[self.rank-11]
        self.pval= 10
    def __str__( self ):
        return "%2s%s" % ( self.label, self.suit )

In this case we use the def __init__( self, *args ) to capture all of the positional parameters in a single sequence, named args. We then give that entire sequence of positional parameters to Card.__init__(). By using the * operator, we tell Python to explode the list into individual positional parameters.

Let’s look at a slightly more sophisticated example.

boat.py

class Boat( object ):
    def __init__( self, name, loa ):
        """Create a new Boat( name, loa )"""
        self.name= name
        self.loa= loa

class Catboat( Boat )
    def __init__( self, sailarea, * args ):
        """Create a new Catboat( sail_area, name, loa )"""
        super(Catboat,self).__init__( * args )
        self.main_area= sail_area

class Sloop( CatBoat ):
    def __init__( self, jib_area, * args );
        """Create a new Sloop( jib_area, main_area, name, loa )"""
        super(Sloop,self).__init__( * args )
        self.jib_area= jib_area
  1. The Boat class defines essential attributes for all kinds of boats: the name and the length overall (LOA).
  2. In the case of a Catboat, we add a single sail area to be base definition of Boat. We use the superclass initialization to prepare the basic name and length overall attributes. Then our subclass adds the sailarea for the single sail on a catboat.
  3. In the case of a Sloop, we add another sail to the definition of a Catboat. We add the new parameter first in the list, and the remaining parameters are simply given to the superclass for its initialization.

Class Variables

The notion of object depends on having instance variables (or “attributes”) which have unique values for each object. We can extend this concept to include variables that are not unique to each instance, but shared by every instance of the class. Class level variables are created in the class definition itself; instance variables are created in the individual class method functions (usually __init__()).

Class level variables are usually “variables” with values that don’t change; these are sometimes called manifest constants or named constants. In Python, there’s no formal declaration for a named constant.

A class level variable that changes will be altered for all instances of the class. This use of class-level variables is often confusing to readers of your program. Class-level variables with state changes need a complete explanation.

This is an example of the more usual approach with class-level constants. These are variables whose values don’t vary; instead, they exist to clarify and name certain values or codes.

wheel.py

import random
class Wheel( object ):
    """Simulate a roulette wheel."""
    green, red, black= 0, 1, 2
    redSet= [1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32, 34,36]
    def __init__( self ):
        self.lastSpin= ( None, None )
    def spin( self ):
        """spin() -> ( number, color )

        Spin a roulette wheel, return the number and color."""
        n= random.randrange(38)
        if n in [ 0, 37 ]: n, color= 0, Wheel.green
        elif n in Wheel.redSet: color= Wheel.red
        else: color= Wheel.black
        self.lastSpin= ( n, color )
        return self.lastSpin
  1. Part of definition of the class Wheel includes some class variables. These variables are used by all instances of the class. By defining three variables, green, red and black, we can make our programs somewhat more clear. Other parts of our program that use the Wheel class can then reference the colors by name, instead of by an obscure numeric code. A program would use Wheel.green to refer to the code for green within the Wheel class.

  2. The Wheel class also creates a class-level variable called redSet. This is the set of red positions on the Roulette wheel. This is defined at the class level because it does not change, and there is no benefit to having a unique copy within each instance of Wheel.

  3. The __init__() method creates an instance variable called lastSpin. If we had multiple wheel objects, each would have a unique value for lastSpin. They all would all, however, share a common definition of green, red, black and redSet.

  4. The spin() method updates the state of the wheel. Notice that the class level variables are referenced with the class name: Wheel.green. The instance level variables are referenced with the instance parameter: self.lastSpin. The class level variables are also available using the instance parameter, Wheel.green is the same object as self.green.

    The spin() method determines a random number between 0 and 37. The numbers 0 and 37 are treated as 0 and 00, with a color of green; a number in the Wheel.redSet is red, and any other number is black. We also update the state of the Wheel by setting self.lastSpin.

    Finally, the spin() method returns a tuple with the number and the code for the color. Note that we can’t easily tell 0 from 00 with this particular class definition.

The following program uses this Wheel class definition. It uses the class-level variables red and black to clarify the color code that is returned by spin().

w= Wheel()
n,c= w.spin()
if c == Wheel.red: print n, "red"
elif c == Wheel.black: print n, "black"
else: print n

Our sample program creates an instance of Wheel, called w. The program calls the spin() method of Wheel , which updates w.lastSpin and returns the tuple that contains the number and color. We use multiple assignment to separate the two parts of the tuple. We can then use the class-level variables to decode the color. If the color is Wheel.red, we can print "red".

Static Methods and Class Method

In a few cases, our class may have methods which depend on only the argument values, or only on class variables. In this case, the self variable isn’t terribly useful, since the method doesn’t depend on any attributes of the instance. Objects which depend on argument values instead of internal status are called Lightweight or Flyweight objects.

A method which doesn’t use the self variable is called a static method. These are defined using a built-in function named staticmethod(). Python has a handy syntax, called a decorator, to make it easier to apply the staticmethod() function to our method function definition. We’ll return to decorators in Decorators .

Here’s the syntax for using the staticmethod() decorator.

@staticmethod
def name ( param, ... 〉 ) :
    suite

To evaluate a static method function, we simply reference the method of the class: Class.method() instead of using a specific instance of the class.

Example of Static Method. Here’s an example of a class which has a static method. We’ve defined a deck shuffler. It doesn’t have any attributes of its own. Instead, it applies it’s shuffle() algorithm to a Deck object.

class Shuffler( object ):
    @staticmethod
    def shuffle( aDeck ):
        for i in range(len(aDeck)):
            card= aDeck.get( random.randrange(len(aDeck)) )
            aDeck.put( i, card )
d1= Deck()
Shuffler.shuffle( d1 )

Class Method. The notion of a class method is relatively specialized. A class method applies to the class itself, not an instance of the class. A class method is generally used for “introspection” on the structure or definition of the class. It is commonly defined by a superclass so that all subclasses inherit the necessary introspection capability.

Generally, class methods are defined as part of sophisticated, dynamic frameworks. For our gambling examples, however, we do have some potential use for class methods. We might want to provide a base Player class who interacts with a particular Game to make betting decisions. Our superclass for all players can define methods that would be used in a subclass.

Design Approaches

When we consider class design, we have often have a built-in or library class which does some of the job we want. For example, we want to be able to accumulate a list of values and then determine the average: this is a very list-like behavior, extended with a new feature.

There are two overall approaches for extending a class: wrapping and inheritance.

  • Wrap an existing class (for example, a tuple, list, set or map) in a new class which adds features. This allows you to redefine the interface to the existing class, which often involves removing features.
  • Inherit from an existing class, adding features as part of the more specialized subclass. This may require you to read more of the original class documentation to see a little of how it works internally.

Both techniques work extremely well; there isn’t a profound reason for making a particular choice. When wrapping a collection, you can provide a new, focused interface on the original collection; this allows you to narrow the choices for the user of the class. When subclassing, however, you often have a lot of capabilities in the original class you are extending.

“Duck” Typing. In Polymorphism, we mentioned “Duck” Typing. In Python, two classes are practically polymorphic if they have the same inteface methods. They do not have to be subclasses of the same class or interface (which is the rule in Java.)

This principle means that the distinction between wrapping and inheritance is more subtle in Python than in other languages. If you provide all of the appropriate interface methods to a class, it behaves as if it was a proper subclass. It may be a class that is wrapped by another class that provides the same interface.

For example, say we have a class Dice, which models a set of individual Die objects.

class Dice( object ):
    def __init__( self ):
        self.theDice= [ Die(), Die() ]
    def roll( self ):
        for d in self.theDice:
            d.roll()
        return self.theDice

In essence, our class is a wrapper around a list of dice, named theDice. However, we don’t provide any of the interface methods that are parts of the built-in list class.

Even though this class is a wrapper around a list object, we can add method names based on the built-in list class: append(), extend(), count(), insert(), etc.

class Dice( object ):
    def __init__( self ):
        self.theDice= [ Die(), Die() ]
    def roll( self ):
        for d in self.theDice:
            d.roll()
        return self.theDice
    def append( self, aDie ):
        self.theDice.append( aDie )
    def __len__( self ):
        return len( self.theDice )

Once we’ve defined these list-like functions we have an ambiguous situation.

  • We could have a subclass of list, which initializes itself to two Die objects and has a roll() method.
  • We could have a distinct Dice class, which provides a roll() method and a number of other methods that make it look like a list.

For people who will read your Python, clarity is the most important feature of the program. In making design decisions, one of your first questions has to be “what is the real thing that I’m modeling?” Since many alternatives will work, your design should reflect something that clarifies the problem you’re solving.

Advanced Class Definition Exercises

Sample Class with Statistical Methods

We can create a Samples class which holds a collection of sample values. This class can have functions for common statistics on the object’s samples. For additional details on these algorithms, see the exercises in Tuples and Sequence Processing Functions: map(), filter() and reduce().

We’ll look at subclassing the built-in list class, by creating a class, Samples, which extends list. You’ll need to implement the following methods in your new class.

Samples.__init__(self, * args)
Save a sequence of samples. It could, at this time, also precompute a number of useful values, like the sum, count, min and max of this set of data. When no data is provided, these values would be set to None.
Samples.__str__(self) → string
Return string with a summary of the data. An example is a string like "%d values, min %g, max %g, mean %g" with the number of data elements, the minimum, the maximum and the mean. The superclass, list, __repr__() function will return the raw data.
Samples.mean(self) → number
Returns the sum divided by the count.
Samples.mode(self) → item

Return the most popular of the sample values. Below we’ve provided an algorithm that can be used to locate the mode of a sequence of samples.

For information on computing the mode, see Exercises.

Samples.median(self) → number
The median is the value in the middle of the sequence. First, sort the sequence. If there is an odd number of elements, pick the middle-most element. If there is an even number of elements, average the two elements that are mid-most.
Samples.variance(self) → number
For each sample, compute the difference between the sample and the mean, square this value, and sum these squares. The number of samples minus 1 is the degrees of freedom. The sum, divided by the degrees of freedom, is the variance. Note that you need two samples to meaningfully compute a variance.
Samples.stdev(self) → number
The square root of the variance.

Note that the list superclass already works correctly with the built-in min() and max() functions. In this case, this consequence of using inheritance instead of wrapping turns out to be an advantage.

Shuffling Method for the Deck class

Shuffling is a matter of taking existing cards and putting them into other positions. There are a many ways of doing this. We’ll need to try both to see which is faster. In essence, we need to create a polymorphic family of classes that we can use to measure performance.

Shuffling Variation 1 - Exchange

For i in range 0 to the number of cards

Generate a random number r in the range 0 to the number of cards.

Use Multiple Assignement to swap cards at position i and r.

Shuffling Variation 2 - Build

Create an empty result sequence, s.

While there are cards in the source self.cards sequence.

Generate a random number r in the range 0 to the number of cards.

Append card r to the result sequence; delete object r from the source self.cards sequence. The pop() method of a sequence can return a selected element and delete it from a sequence nicely.

Replace self.cards with the result sequence, s .

Shuffling Variation 3 - Sort

Create a key function which actually returns a random value.

Use the sort() method of a list with this random key-like function.

self.cards.sort( key=aRandomKeyFunction )

Shuffling Variation 4 - random.shuffle

The random module has a shuffle() method which can be used as follows.

random.shuffle( self.cards )

Of these four algorithms, which is fastest? The best way to test these is to create four separate subclasses of Deck, each of which provides a different implementation of the shuffle() method. A main program can then create an instance of each variation on Deck and do several hundred shuffles.

We can create a timer using the time module. The time.clock() function will provide an accurate time stamp. The difference between two calls to time.clock() is the elapsed time. Because shuffling is fast, we’ll do it 100 times to get a time that’s large enough to be accurate.

Because all of our variations on Deck are polymorphic, our main program should look something like the following.

d1= DeckExch()
d2= DeckBuild()
d3= DeckSortRandom()
d4= DeckShuffle()
for deck in ( d1, d2, d3, d4 ):
    start= time.clock()
    for i in range(100):
        d.shuffle()
    finish= time.clock()

Encapsulation

The Shuffling exercise built several alternate solutions to a problem. We are free to implement an algorithm with no change to the interface of Deck. This is a important effect of the principal of encapsulation: a class and the clients that use that class are only coupled together by an interface defined by method functions.

There are a variety of possible dependencies between a class and its clients.

  • Interface Method Functions. A client can depend on method functions specifically designated as an interface to a class. In Python, we can define internal methods by prefixing their names with _. Other names (without the leading _ ) define the public interface.
  • All Method Functions. A client can depend on all method functions of a class. This removes the complexity of hidden, internal methods.
  • Instance Variables. A client can depend on instance variables in addition to method functions. This can remove the need to write method functions that simply return the value of an instance variable.
  • Global Variables. Both classes share global variables. The Python global statement is one way to accomplish this.
  • Implementation. A client can depend on the specific algorithm being executed by a class. A client method can have expectations of how a class is implemented.

What are the advantages and disadvantages of each kind of dependency?

Class Responsibilities

Assigning responsibility to class can be challenging. A number of reasons can be used to justify the functions and instance variables that are combined in a single class.

  • Convenience. A class is defined to do things because – well – it’s convenient to write the program that way.
  • Similar Operations. A class is defined because it does all input, all output, or all calculations.
  • Similar Time. A class is defined to handle all initialization, all processing or all final cleanup.
  • Sequence. We identify some operations which are performed in a simple sequence and bundle these into a single class.
  • Common Data. A class is defined because it has the operations which isolate a data structure or algorithm.

What are the possible differences between theses? What are the advantages and disadvantages of each?

Style Notes

Classes are perhaps the most important organizational tool for Python programming. Python software is often designed as a set of interacting classes. There are several conventions for naming and documenting class definitions.

It is important to note that the suite within a class definition is typically indented four spaces. It is often best to set your text editor with tab stops every four spaces. This will usually yield the right kind of layout. Each function’s suite is similarly indented four spaces, as are the suites within compound statements.

Blank lines are used sparingly; most typically a single blank line will separate each function definition within the class. A lengthy class definition, with a number of one-liner set-get accessor functions may group the accessors together without any intervening blank lines.

Class names are typically MixedCase with a leading uppercase letter. Members of the class (method functions and attributes) typically begin with a lowercase letter. Class names are also, typically singular nouns. We don’t define People , we define Person. A collection might be a PersonList or PersonSet.

Note that the following naming conventions are honored by Python:

single_trailing_underscore_
Used to make a variable names different from a similar Python reserved word. For example: range_ is a legal variable name, where range would not be legal.
_single_leading_underscore
Used to make variable or method names hidden. This conceals them from the dir() function.
__double_leading_underscore
Class-private names. Use this to assure that a method function is not used directly by clients of a class.
__double_leading_and_trailing_underscore__
These are essentialy reserved by Python for its own internals.

Docstring Recommendations. The first line of a class body is the docstring; this provides an overview of the class. It should summarize the responsibilities and collaborators of the class. It should summarize the public methods and instance variables.

Individual method functions are each documented in their own docstrings. Tools like Sphinx and Epydoc will look for the __init__() docstring as part of summarizing a class.

When defining a subclass, be sure to mention the specific features added (or removed) by the subclass. There are two basic cases: overriding and extending. When overriding a superclass method function, the subclass has replaced the superclass function. When extending a superclass function, the subclass method will call the superclass function to perform some of the work. The override-extend distinctions must be made clear in the docstring.

When initializing instance variables in the __init__() function, a string placed after the assignment statement can serve as a definition of the variable.

RST Docstring. The most widely-used technique is to write reStructuredText (RST) markup in the docstrings. This is extracted and formatted by tools like Sphinx and epydoc.

For information on RST formatting, see PEP 287, as well as http://docutils.sourceforge.net/.

class Dice( object ):
    """Model two dice used for craps.  Relies on Die class.

    :ivar theDice: tuple with two Die instances

    ..  method:: roll

        roll dice and return total
    """
    def __init__(self):
        """Create a new pair of dice."""
        self.theDice = ( Die(), Die() )
    def roll(self):
        """Roll the dice and return the sum.

        :returns: number
        """
        [ d.roll() for d in self.theDice ]
        t = sum( theDice )
        return t

Generally, we have been omitting a complete docstring header on each class in the interest of saving some space for the kind of small examples presented in the text.

Table Of Contents

Previous topic

Classes

Next topic

Some Design Patterns

This Page