Object Oriented Concepts
Object Oriented Concepts
Type-based dispatch is useful when it is necessary, but (fortunately) it is not always neces_sary. Often
you can avoid it by writing functions that work correctly for arguments with different types.
Many of the functions we wrote for strings also work for other sequence types. For exam_ple, in Section
11.2 we used histogram to count the number of times each letter appears in
a word.
def histogram(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] = d[c]+1
return d
This function also works for lists, tuples, and even dictionaries, as long as the elements of
>>> histogram(t)
Functions that work with several types are called polymorphic. Polymorphism can fa_cilitate code reuse.
For example, the built-in function sum, which adds the elements of a
Since Time objects provide an add method, they work with sum:
>>> print(total)
23:01:00
In general, if all of the operations inside a function work with a given type, the function
Inheritance
Inheritance is the ability to define a new class that is a modified version of an existing class.
As an example, let’s say we want a class to represent a “hand”, that is, the cards held by
one player. A hand is similar to a deck: both are made up of a collection of cards, and both
A hand is also different from a deck; there are operations we want for hands that don’t
make sense for a deck. For example, in poker we might compare two hands to see which
one wins. In bridge, we might compute a score for a hand in order to make a bid.
define a new class that inherits from an existing class, you put the name of the existing
class in parentheses:
class Hand(Deck):
This definition indicates that Hand inherits from Deck; that means we can use methods like
When a new class inherits from an existing one, the existing one is called the parent and
In this example, Hand inherits __init__ from Deck, but it doesn’t really do what we want:
instead of populating the hand with 52 new cards, the init method for Hands should ini_tialize cards
with an empty list.
If we provide an init method in the Hand class, it overrides the one in the Deck class:
self.cards = []
self.label = label
When you create a Hand, Python invokes this init method, not the one in Deck.
>>> hand.cards
[]
>>> hand.label
'new hand'
The other methods are inherited from Deck, so we can use pop_card and add_card to deal
a card:
>>> hand.add_card(card)
>>> print(hand)
King of Spades
for i in range(num):
hand.add_card(self.pop_card())
move_cards takes two arguments, a Hand object and the number of cards to deal. It modi_fies both self
and hand, and returns None.
In some games, cards are moved from one hand to another, or from a hand back to the
deck. You can use move_cards for any of these operations: self can be either a Deck or a
Inheritance is a useful feature. Some programs that would be repetitive without inheritance
can be written more elegantly with it. Inheritance can facilitate code reuse, since you can
customize the behavior of parent classes without having to modify them. In some cases,
the inheritance structure reflects the natural structure of the problem, which makes the
On the other hand, inheritance can make programs difficult to read. When a method is
invoked, it is sometimes not clear where to find its definition. The relevant code may be
spread across several modules. Also, many of the things that can be done using inheritance
Data encapsulation
The previous chapters demonstrate a development plan we might call “object-oriented
classes to represent them. In each case there is an obvious correspondence between the
object and some entity in the real world (or at least a mathematical world).
But sometimes it is less obvious what objects you need and how they should interact. In
that case you need a different development plan. In the same way that we discovered
by data encapsulation.
suffix_map = {}
prefix = ()
Because these variables are global, we can only run one analysis at a time. If we read two
texts, their prefixes and suffixes would be added to the same data structures (which makes
To run multiple analyses, and keep them separate, we can encapsulate the state of each
class Markov:
def __init__(self):
self.suffix_map = {}
self.prefix = ()
Next, we transform the functions into methods. For example, here’s process_word:
self.prefix += (word,)
return
try:
self.suffix_map[self.prefix].append(word)
except KeyError:
self.suffix_map[self.prefix] = [word]
Transforming a program like this—changing the design without changing the behavior—is
This example suggests a development plan for designing objects and methods:
1. Start by writing functions that read and write global variables (when necessary).
2. Once you get the program working, look for associations between global variables
markov.py, and follow the steps described above to encapsulate the global variables