0% found this document useful (0 votes)
11 views6 pages

Object Oriented Concepts

Uploaded by

sushma-icb
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views6 pages

Object Oriented Concepts

Uploaded by

sushma-icb
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 6

Polymorphism

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

s are hashable, so they can be used as keys in d.

>>> t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam']

>>> histogram(t)

{'bacon': 1, 'egg': 1, 'spam': 4}

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

sequence, works as long as the elements of the sequence support addition.

Since Time objects provide an add method, they work with sum:

>>> t1 = Time(7, 43)

>>> t2 = Time(7, 41)

>>> t3 = Time(7, 37)


>>> total = sum([t1, t2, t3])

>>> print(total)

23:01:00

In general, if all of the operations inside a function work with a given type, the function

works with that type.

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

require operations like adding and removing cards.

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.

This relationship between classes—similar, but different—lends itself to inheritance. To

define a new class that inherits from an existing class, you put the name of the existing

class in parentheses:

class Hand(Deck):

"""Represents a hand of playing cards."""

This definition indicates that Hand inherits from Deck; that means we can use methods like

pop_card and add_card for Hands as well as Decks.

When a new class inherits from an existing one, the existing one is called the parent and

the new class is called the child.

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:

# inside class Hand:

def __init__(self, label=''):

self.cards = []

self.label = label

When you create a Hand, Python invokes this init method, not the one in Deck.

>>> hand = Hand('new hand')

>>> 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:

>>> deck = Deck()

>>> card = deck.pop_card()

>>> hand.add_card(card)

>>> print(hand)

King of Spades

A natural next step is to encapsulate this code in a method called move_cards:

# inside class Deck:

def move_cards(self, hand, num):

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

Hand, and hand, despite the name, can also be a Deck.

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

design easier to understand.

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

can be done as well or better without it.

Data encapsulation
The previous chapters demonstrate a development plan we might call “object-oriented

design”. We identified objects we needed—like Point, Rectangle and Time—and defined

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

function interfaces by encapsulation and generalization, we can discover class interfaces

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

for some interesting generated text).

To run multiple analyses, and keep them separate, we can encapsulate the state of each

analysis in an object. Here’s what that looks like:

class Markov:

def __init__(self):

self.suffix_map = {}

self.prefix = ()

Next, we transform the functions into methods. For example, here’s process_word:

def process_word(self, word, order=2):

if len(self.prefix) < order:

self.prefix += (word,)

return

try:

self.suffix_map[self.prefix].append(word)

except KeyError:

# if there is no entry for this prefix, make one

self.suffix_map[self.prefix] = [word]

self.prefix = shift(self.prefix, word)

Transforming a program like this—changing the design without changing the behavior—is

another example of refactoring.

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

and the functions that use them.


3. Encapsulate related variables as attributes of an object.

4. Transform the associated functions into methods of the new class.

As an exercise, download my Markov code from https://thinkpython.com/code/

markov.py, and follow the steps described above to encapsulate the global variables

as attributes of a new class called Markov

You might also like