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 the section called “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.
We'll look closely at this in Chapter 24, Creating or Extending Data Types
.
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.