CHAPTER 8

Object-Oriented Programming

8.1 Defining a New Python Class

8.2 Examples of User-Defined Classes

8.3 Designing New Container Classes

8.4 Overloaded Operators

8.5 Inheritance

8.6 User-Defined Exceptions

8.7 Case Study: Indexing and Iterators

Chapter Summary

Solutions to Practice Problems

Exercises

Problems

THIS CHAPTER DESCRIBES how to implement new Python classes and introduces object-oriented programming (OOP).

There are several reasons why programming languages such as Python enable developers to define new classes. Classes that are custom-built for a particular application will make the application program more intuitive and easier to develop, debug, read, and maintain.

The ability to create new classes also enables a new approach to structuring application programs. A function exposes to the user its behavior but encapsulates (i.e., hides) its implementation. Similarly, a class exposes to the user the methods that can be applied to objects of the class (i.e., class instances) but encapsulates how the data contained in the objects is stored and how the class methods are implemented. This property of classes is achieved thanks to fine-grained, customized namespaces that are associated with every class and object. OOP is a software development paradigm that achieves modularity and code portability by organizing application programs around components that are classes and objects.

8.1 Defining a New Python Class

We now explain how to define a new class in Python. The first class we develop is the class Point, a class that represents points in the plane or, if you prefer, on a map. More precisely, an object of type Point corresponds to a point in the two-dimensional plane. Recall that each point in the plane can be specified by its x-axis and y-axis coordinates as shown in Figure 8.1.

image

Figure 8.1 A point in the plane. An object of type Point represents a point in the plane. A point is defined by its x and y coordinates.

Before we implement the class Point, we need to decide how it should behave, that is, what methods it should support.

Methods of Class Point

Let's describe how we would like to use the class Point. To create a Point object, we would use the default constructor of the Point class. This is no different from using the list() or int() default constructors to create a list or integer object.

>>> point = Point()

(Just a reminder: We have not implemented the class Point yet; the code here is only meant to illustrate how we want the class Point to behave.)

Once we have a Point object, we would set its coordinates using the methods setx() and sety():

>>> point.setx(3)
>>> point.sety(4)

At this point, Point object point should have its coordinates set. We could check this using method get():

>>> p.get()
(3, 4)

The method get() would return the coordinates of point as a tuple object. Now, to move point down by three units, we would use method move():

>>> p.move(0,-3)
>>> p.get()
(3, 1)

We should also be able to change the coordinates of point():

>>> p.sety(-2)
>>> p.get()
(3, -2)

We summarize the methods we want class Point to support in Table 8.1.

Table 8.1 Methods of class Point. The usage for the four methods of class Point is shown; point refers to an object of type Point.

Usage Explanation
point.setx(xcoord) Sets the x coordinate of point to xcoord
point.sety(ycoord) Sets the y coordinate of point to ycoord
point.get() Returns the x and y coordinates of point as a tuple (x, y)
point.move(dx, dy) Changes the coordinates of point from the current (x, y) to (x+dx, y+dy)

A Class and Its Namespace

As we learned in Chapter 7, a namespace is associated with every Python class, and the name of the namespace is the name of the class. The purpose of the namespace is to store the names of the class attributes. The class Point should have an associated namespace called Point. This namespace would contain the names of class Point methods, as shown in Figure 8.2.

image

Figure 8.2 Class Point and its attributes. When class Point is defined, a namespace associated with the class is defined too; this namespace contains the class attributes.

Figure 8.2 shows how each name in namespace Point refers to the implementation of a function. Let's consider the implementation of function setx().

In Chapter 7, we have learned that Python translates a method invocation like

>>> point.setx(3)

to

>>> Point.setx(point, 3)

So function setx() is a function that is defined in the namespace Point. It takes not one but two arguments: the Point object that is invoking the method and an x-coordinate. Therefore, the implementation of setx() would have to be something like:

    def setx(point, xcoord):
        # implementation of setx

Function setx() would somehow have to store the x-coordinate xcoord so that it can later be retrieved by, say, method get(). Unfortunately, the next code will not work

    def setx(point, xcoord):
        x = xcoord

because x is a local variable that will disappear as soon as function call setx() terminates. Where should the value of xcoord be stored so that it can be retrieved later?

Every Object Has an Associated Namespace

We know that a namespace is associated with every class. It turns out that not only classes but every Python object has its own, separate namespace. When we instantiate a new object of type Point and give it name point, as in

>>> point = Point()

a new namespace called point gets created, as shown in Figure 8.3(a).

image

Figure 8.3 The namespace of an object. (a) Every Point object has a namespace. (b) The statement point.x = 3 assigns 3 to variable x defined in namespace point.

Since a namespace is associated with object point, we can use it to store values:

>>> point.x = 3

This statement creates name x in namespace point and assigns it integer object 3, as shown in Figure 8.3(b).

Now let's get back to implementing the method setx(). We now have a place to the x- coordinate of a Point object. We store it the namespace associated with it. Method setx() would be implemented in this way:

    def setx(point, xcoord):
        point.x = xcoord

Implementation of Class Point

We are now ready to write the implementation of class Point:

Module: ch8.py
 1 class Point:
 2     ‘class that represents points in the plane’
 3     def setx(self, xcoord):
 4         ‘set x coordinate of point to xcoord’
 5         self.x = xcoord
 6     def sety(self, ycoord):
 7         ‘set y coordinate of point to ycoord’
 8         self.y = ycoord
 9     def get(self):
10         ‘return a tuple with x and y coordinates of the point’
11         return (self.x, self.y)
12     def move(self, dx, dy):
13         ‘change the x and y coordinates by dx and dy’
14         self.x += dx
15         self.y += dy

The reserved keyword class is used to define a new Python class. The class statement is very much like the def statement. A def statement defines a new function and gives the function a name; a class statement defines a new type and gives the type a name. (They are both also similar to the assignment statement that gives a name to an object.)

Following the class keyword is the name of the class, just as the function name follows the def statement. Another similarity with function definitions is the docstring below the class statement: It will be processed by the Python interpreter as part of the documentation for the class, just as for functions.

A class is defined by its attributes. The class attributes (i.e., the four methods of class Point) are defined in an indented code block just below the line

class Point:

The first input argument of each class method refers to the object invoking the method. We have already figured out the implementation of method setx():

    def setx(self, xcoord):
        ‘sets x coordinate of point’
        self.x = xcoord

We made one change to the implementation. The first argument that refers to the Point object invoking method setx() is named self rather than point. The name of the first argument can be anything really; the important thing is that it always refers to the object invoking the method. However, the convention among Python developers is to use name self for the object that the method is invoked on, and we follow that convention.

The method sety() is similar to setx(): It stores the y-coordinate in variable y, which is also defined in the namespace of the invoking object. Method get() returns the values of names x and y defined in the namespace of the invoking object. Finally, method move() changes the values of variables x and y associated with the invoking object.

You should now test your new class Point. First execute the class definition by running module ch8.py. Then try this, for example:

>>> a = Point()
>>> a.setx(3)
>>> a.sety(4)
>>> a.get()
(3, 4)

Practice Problem 8.1

Add method getx() to the class Point; this method takes no input and returns the x coordinate of the Point object invoking the method.

>>> a.getx()
3

Instance Variables

Variables defined in the namespace of an object, such as variables x and y in the Point object a, are called instance variables. Every instance (object) of a class will have its own namespace and therefore its own separate copy of an instance variable.

For example, suppose we create a second Point object b as follows:

>>> b = Point()
>>> b.setx(5)
>>> b.sety(-2)

Instances a and b will each have its own copies of instance variables x and y, as shown in Figure 8.4.

image

Figure 8.4 Instance variables. Each object of type Point has its own instance variables x and y, stored in the namespace associated with the object.

In fact, instance variables x and y can be accessed by specifying the appropriate instance:

>>> a.x
3
>>> b.x
5

They can, of course, be changed directly as well:

>>> a.x = 7
>>> a.x
7

Instances Inherit Class Attributes

Names a and b refer to objects of type Point, so the namespaces of a and b should have some relationship with the namespace Point that contains the class methods that can be invoked on objects a and b. We can check this, using Python's function dir(), which we introduced in Chapter 7 and which takes a namespace and returns a list of names defined it:

>>> dir(a)
[‘_ _class_ _’, ‘_ _delattr_ _’, ‘_ _dict_ _’, ‘_ _doc_ _’, ‘_ _eq_ _’,
 …
 ‘_ _weakref_ _’, ‘get’, ‘move’, ‘setx’, ‘sety’, ‘x’, ‘y’]

(We omit a few lines of output.)

As expected, instance variable names x and y appear in the list. But so do the methods of the Point class: setx, sety, get, and move. We will say that object a inherits all the attributes of class Point, just as a child inherits attributes from a parent. Therefore, all the attributes of class Point are accessible from namespace a. Let's check this:

>>> a.setx
<bound method Point.setx of <_ _main_ _.Point object at 0x14b7ef0>></code

image

Figure 8.5 Instance and class attributes. Each object of type Point has its own instance attributes x and y. They all inherit the attributes of class Point.

The relationship between namespaces a, b, and Point is illustrated in Figure 8.5. It is important to understand that the method names setx, sety, get, and move are defined in namespace Point, not in namespace a or b. Thus, the Python interpreter uses this procedure when it evaluates expression a.setx:

  1. It first attempts to find name setx in object (namespace) a.
  2. If name setx does not exist in namespace a, then it attempts to find setx in name-space Point (where it will find it).

Class Definition, More Generally

The format of the class definition statement is:

class <Class Name>:
    <class variable 1> = <value>
    <class variable 2> = <value>
    …
    def <class method 1>(self, arg11, arg12, …):
        <implementation of class method 1>
    def <class method 2>(self, arg21, arg22, …):
        <implementation of class method 2>
    …

(We will see more general version in later sections.)

The first line of a class definition consists of the class keyword followed by <Class Name>, the name of the class. In our example, the name was Point.

The definitions of the class attributes follow the first line. Each definition is indented with respect to the first line. Class attributes can be class methods or class variables. In class Point, four class methods were defined, but no class variable. A class variable is one whose name is defined in the namespace of the class.

Practice Problem 8.2

Start by defining the class Test and then creating two instances of Test in your interpreter shell:

>>> class Test:
        version = 1.02

>>> a = Test()
>>> b = Test()

The class Test has only one attribute, the class variable version that refers to float value 1.02.

  1. Draw the namespaces associated with the class and the two objects, the names—if any—contained in them, and the value(s) the name(s) refer to.
  2. Execute these statements and fill in the question marks:
    >>> a.version
    ???
    >>> b.version
    ???
    >>> Test.version
    ???
    >>> Test.version=1.03
    >>> a.version
    ???
    >>> Point.version
    ???
    >>> a.version = ‘Latest!!’
    >>> Point.version
    ???
    >>> b.version
    ???
    >>> a.version
    ???
  3. Draw the state of the namespaces after this execution. Explain why the last three expressions evaluate the way they did.

Documenting a Class

In order to get usable documentation from the help() tool, it is important to document a new class properly. The class Point we defined has a docstring for the class and also one for every method:

>>> help(Point)
Help on class Point in module _ _main_ _:

class Point(builtins.object)
 |  class that represents a point in the plane
 |
 |  Methods defined here:
 |
 |  get(self)
 |      returns the x and y coordinates of the point as a tuple
 |
 …

(We omit the rest of the output.)

Class Animal

Before we move on to the next section, let's put into practice everything we have learned so far and develop a new class called Animal that abstracts animals and supports three methods:

  • setSpecies(species): Sets the species of the animal object to species.
  • setLanguage(language): Sets the language of the animal object to language.
  • speak(): Prints a message from the animal as shown below.

Here is how we want the class to behave:

>>> snoopy = Animal()
>>> snoopy.setpecies(‘dog’)
>>> snoopy.setLanguage(‘bark’)
>>> snoopy.speak()
I am a dog and I bark.

We start the class definition with the first line:

class Animal:

Now, in an indented code block, we define the three class methods, starting with method setSpecies(). Even though the method setSpecies() is used with one argument (the animal species), it must be defined as a function that takes two arguments: the argument self that refers to the object invoking the method and the species argument:

   def setSpecies(self, species):
       self.species = species

Note that we named the instance variable species the same as the local variable species. Because the instance variable is defined in the namespace self and the local variable is defined in the local namespace of the function call, there is no name conflict.

The implementation of method setLanguage() is similar to the implementation of setSpecies. The method speak() is used without input arguments; therefore, it must be defined with just input argument self. Here is the final implementation:

Module: ch8.py
 1  class Animal:
 2      ‘represents an animal’
 3
 4      def setSpecies(self, species):
 5          ‘sets the animal species’
 6          self.spec = species
 7
 8      def setLanguage(self, language):
 9          ‘sets the animal language’
10          self.lang = language
11
12      def speak(self):
13          ‘prints a sentence by the animal’
14          print(‘I am a {} and I {}.’.format(self.spec, self.lang))

Practice Problem 8.3

Implement class Rectangle that represents rectangles. The class should support methods:

  • setSize(width, length): Takes two number values as input and sets the length and the width of the rectangle
  • perimeter(): Returns the perimeter of the rectangle
  • area(): Returns the area of the rectangle
>>> rectangle = Rectangle(3,4)
>>> rectangle.perimeter()
14
>>> rectangle.area()
12

8.2 Examples of User-Defined Classes

In order to get more comfortable with the process of designing and implementing a new class, in this section we work through the implementation of several more classes. But first, we explain how to make it easier to create and initialize new objects.

Overloaded Constructor Operator

We take another look at the class Point we developed in the previous section. To create a Point object at (x, y)-coordinates (3,4), we need to execute three separate statements:

>>> a = Point()
>>> a.setx(3)
>>> a.sety(4)

The first statement creates an instance of Point; the remaining two lines initialize the point's x- and y-coordinates. That's quite a few steps to create a point at a certain location. It would be nicer if we could fold the instantiation and the initialization into one step:

>>> a = Point(3,4)

We have already seen types that allow an object to be initialized when created. Integers can be initialized when created:

>>> x = int(93)
>>> x
93

So can objects of type Fraction from the built-in fractions module:

>>> import fractions
>>> x = fractions.Fraction(3,4)
>>> x
Fraction(3, 4)

Constructors that take input arguments are useful because they can initialize the state of the object at the moment the object is instantiated.

In order to be able able to use a Point() constructor with input arguments, we must explicitly add a method called _ _init_ _() to the implementation of class Point. When added to a class, it will be automatically called by the Python interpreter whenever an object is created. In other words, when Python executes

Point(3,4)

it will create a “blank” Point object first and then execute

self._ _init_ _(3, 4)

where self refers to the newly created Point object. Note that since _ _init_ _() is a method of the class Point that takes two input arguments, the function _ _init_ _() will need to be defined to take two input arguments as well, plus the obligatory argument self:

Module: ch8.py
1  class Point:
2      ‘represents points in the plane’
3
4      def _ _init_ _(self, xcoord, ycoord):
5          ‘initializes point coordinates to (xcoord, ycoord)’
6          self.x = xcoord
7          self.y = ycoord
8
9      # implementations of methods setx(), sety(), get(), and move()

Function _ _init_ _() Is Called Every Time an Object Is Created image

Because the _ _init_ _() method is called every time an object is instantiated, the Point() constructor must now be called with two arguments. This means that calling the constructor without an argument will result in an error:

>>> a = Point()
Traceback (most recent call last):
  File “<pyshell#23>”, line 1, in <module>
    a = Point()
  TypeError: _ _init_ _() takes exactly 3 positional arguments
  (1 given)

It is possible to rewrite the _ _init_ _() function so it can handle two arguments, or none, or one. Read on.

Default Constructor

We know that constructors of built-in classes can be used with or without arguments:

>>> int(3)
3
>>> int()
0

We can do the same with user-defined classes. All we need to do is specify the default values of the input arguments xcoord and ycoord if input arguments are not provided. In the next reimplementation of the _ _init_ _() method, we specify default values of 0:

Module: ch8.py
1  class Point:
2     ‘represents points in the plane’
3
4     def _ _init_ _(self, xcoord=0, ycoord=0):
5         ‘initializes   point   coordinates   to  (xcoord, ycoord)’
6         self.x = xcoord
7         self.y = ycoord
8
9     # implementations of methods setx(), sety(), get(), and move()

This Point constructor can now take two input arguments

>>> a = Point(3,4)
>>> a.get()
(3, 4)

or none

>>> b = Point()
>>> b.get()
(0, 0)

or even just one

>>> c = Point(2)
>>> c.get()
(2, 0)

The Python interpreter will assign the constructor arguments to the local variables xcoord and ycoord from left to right.

Playing Card Class

In Chapter 6, we developed a blackjack application. We used strings such as ‘3 image to represent playing cards. Now that we know how to develop new types, it makes sense to develop a Card class to represent playing cards.

This class should support a two-argument constructor to create Card objects:

>>> card = Card(‘3’, ‘u2660’)

The string ‘u2660’ is the escape sequence that represents Unicode character image. The class should also support methods to retrieve the rank and suit of the Card object:

image

That should be enough. We want the class Card to support these methods:

  • Card(rank, suit): Constructor that initializes the rank and suit of the card
  • getRank(): Returns the card's rank
  • getSuit(): Returns the card's suit

Note that the constructor is specified to take exactly two input arguments. We choose not to provide default values for the rank and suit because it is not clear what a default playing card would really be. Let's implement the class:

Module: cards.py
 1  class Card:
 2      ‘represents a playing card’
 3
 4      def _ _init_ _(self, rank, suit):
 5         ‘initialize   rank   and   suit   of   playing   card’
 6         self.rank = rank
 7         self.suit = suit
 8
 9      def getRank(self):
10      ‘return rank’
11      return self.rank
12
13      def getSuit(self):
14         ‘return  suit’
15        return self.suit

Note that the method _ _init_ _() is implemented to take two arguments, which are the rank and suit of the card to be created.

Practice Problem 8.4

Modify the class Animal we developed in the previous section so it supports a two, one, or no input argument constructor:

>>> snoopy = Animal(‘dog’, ‘bark’)
>>> snoopy.speak()
I am a dog and I bark.
>>> tweety = Animal(‘canary’)
>>> tweety.speak()
I am a canary and I make sounds.
>>> animal = Animal()
>>> animal.speak()
I am a animal and I make sounds.

8.3 Designing New Container Classes

While Python provides a diverse set of container classes, there will always be a need to develop container classes tailored for specific applications. We illustrate this with a class that represents a deck of playing cards and also with the classic queue container class.

Designing a Class Representing a Deck of Playing Cards

We again use the blackjack application from Chapter 6 to motivate our next class. In the blackjack program, the deck of cards was implemented using a list. To shuffle the deck, we used the shuffle() method from the random module, and to deal a card, we used the list method pop(). In short, the blackjack application was written using nonapplicationspecific terminology and operations.

The blackjack program would have been more readable if the list container and operations were hidden and the program was written using a Deck class and Deck methods. So let's develop such a class. But first, how would we want the Deck class to behave?

First, we should be able to obtain a standard deck of 52 cards using a default constructor:

>>> deck = Deck()

The class should support a method to shuffle the deck:

>>> deck.shuffle()

The class should also support a method to deal the top card from the deck.

image

The methods that the Deck class should support are:

  • Deck(): Constructor that initializes the deck to contain a standard deck of 52 playing cards
  • shuffle(): Shuffles the deck
  • getSuit(): Pops and returns the card at the top of the deck

Implementing the Deck (of Cards) Class

Let's implement the Deck class, starting with the Deck constructor. Unlike the two examples from the previous section (classes Point and Card), the Deck constructor does not take input arguments. It still needs to be implemented because its job is to create the 52 playing cards of a deck and store them somewhere.

To create the list of the 52 standard playing cards, we can use a nested loop that is similar to the one we used in function shuffledDeck() of the blackjack application. There we created a set of suits and a set of ranks

suits = {‘u2660’, ‘u2661’, ‘u2662’, ‘u2663’}
ranks = {‘2’,‘3’,‘4’,‘5’,‘6’,‘7’,‘8’,‘9’,‘10’,‘J’,‘Q’,‘K’,‘A’}

and then used a nested for loop to create every combination of rank and suit

    for suit in suits:
        for rank in ranks:
            # create card with given rank and suit and add to deck

We need a container to store all the generated playing cards. Since the ordering of cards in a deck is relevant and the deck should be allowed to change, we choose a list just as we did in the blackjack application in Chapter 6.

Now we have some design decisions to make. First, should the list containing the playing cards be an instance or class variable? Because every Deck object should have its own list of playing cards, the list clearly should be an instance variable.

We have another design question to resolve: Where should the sets suits and ranks be defined? They could be local variables of the _ _init_ _() function. They could also be class variables of the class Deck. Or they could be instance variables. Because the sets will not be modified and they are shared by all Deck instances, we decide to make them class variables.

Take a look at the implementation of the method _ _init_ _() in module cards.py. Since the sets suits and ranks are class variables of the class Deck, they are defined in namespace Deck. Therefore, in order to access them in lines 12 and 13, you must specify a namespace:

    for suit in Deck.suits:
        for rank in Deck.ranks:
            # add Card with given rank and suit to deck

We now turn our attention to the implementation of the two remaining methods of class Deck. The method shuffle() should just call random module function shuffle() on instance variable self.deck.

For method dealCard(), we need to decide where the top of the deck is. Is it at the beginning of list self.deck or at the end of it? We decide to go for the end. The complete class Deck is:

Module: cards.py
 1 from random import shuffle
 2 class Deck:
 3    ‘represents a deck of 52 cards’
 4
 5    # ranks and suits are Deck class variables
 6    ranks = {‘2’,‘3’,‘4’,‘5’,‘6’,‘7’,‘8’,‘9’,‘10’,‘J’,‘Q’,‘K’,‘A’}
 7
 8    # suits is a set of 4 Unicode symbols representing the 4 suits
 9    suits = {‘u2660’, ‘u2661’, ‘u2662’, ‘u2663’}
10
11    def _ _init_ _(self):
12        ‘initialize deck of 52 cards’
13        self.deck = []          # deck is initially empty
14
15        for suit in Deck.suits: # suits and ranks are Deck
16            for rank in Deck.ranks: # class variables
17                # add Card with given rank and suit to deck
18                self.deck.append(Card(rank, suit))
19
20    def dealCard(self):
21        ‘deal (pop and return) card from the top of the deck’
22        return self.deck.pop()
23
24    def shuffle(self):
25        ‘shuffle the deck’
26        shuffle(self.deck)

Practice Problem 8.5

Modify the constructor of the class Deck so the class can also be used for card games that do not use the standard deck of 52 cards. For such games, we would need to provide the list of cards explicitly in the constructor. Here is a somewhat artificial example:

>>> deck = Deck([‘1’, ‘2’, ‘3’, ‘4’])
>>> deck.shuffle()
>>> deck.dealCard()
‘3’
>>> deck.dealCard()
‘1’

Container Class Queue

A queue is a container type that abstracts a queue, such as a queue of shoppers in a supermarket waiting at the cashier's. In a checkout queue, shoppers are served in a first-in first-out (FIFO) fashion. A shopper will put himself at the end of the queue and the first person in the queue is the next one served by the cashier. More generally, all insertions must be at the rear of the queue, and all removals must be from the front.

We now develop a basic Queue class that abstracts a queue. It will support very restrictive accesses to the items in the queue: method enqueue() to add an item to the rear of the queue and method dequeue() to remove an item from the front of the queue. As shown in Table 8.2, the Queue class will also support method isEmpty() that returns true or false depending on whether the queue is empty or not. The Queue class is said to be a FIFO container type because the item removed is the item that entered the queue earliest.

Before we implement the Queue class, we illustrate its usage. We start by instantiating a Queue object:

>>> fruit = Queue()

We then insert a fruit (as a string) into it:

>>> fruit.enqueue(‘apple’)

Let's insert a few more fruits:

>>> fruit.enqueue(‘banana’)
>>> fruit.enqueue(‘coconut’)

We can then dequeue the queue:

>>> fruit.dequeue()
‘apple’

The method dequeue() should both remove and return the item at the front of the queue.

Table 8.2 Queue methods. A queue is a container of a sequence of items; the only accesses to the sequence are enqueue(item) and dequeue().

Method Description
enqueue(item) Add item to the end of the queue
dequeue() Remove and return the element at the front of the queue
isEmpty() Returns True if the queue is empty, False otherwise

image

Figure 8.6 Queue operations. Shown is the state of the queue fruit after the statements:

fruit.enqueue(‘apple’)
fruit.enqueue(‘banana’)
fruit.enqueue(‘coconut’)
fruit.dequeue()
fruit.dequeue()

We dequeue two more times to get back an empty queue:

>>> fruit.dequeue()
‘banana’
>>> fruit.dequeue()
‘coconut’
>>> fruit.isEmpty()
True

Figure 8.6 shows the sequence of states the queue fruit went through as we executed the previous commands.

Implementing a Queue Class

Let's discuss the implementation of the Queue class. The most important question we need to answer is how are we going to store the items in the queue. The queue can be empty or contain an unbounded number of items. It also has to maintain the order of items, as that is essential for a (fair) queue. What built-in type can be used to store, in order, an arbitrary number of items and allow insertions on one end and deletions from the other?

The list type certainly satisfies these constraints, and we go with it. The next question is: When and where in the Queue class implementation should this list be created? In our example, it is clear that we expect that the default Queue constructor gives us an empty queue. This means that we need to create the list as soon as the Queue object is created—that is, in an _ _init_ _() method:

    def _ _init_ _(self):
        ‘instantiates an empty list that will contain queue items’
        self.q = []
… # remainder of class definition

Now we move to the implementation of the three Queue methods. The method isEmpty() can be implemented easily just by checking the length of list self.q:

def isEmpty(self):
    ‘returns True if queue is empty, False otherwise’
    return (len(self.q) == 0)

The method enqueue() should put items into the rear of list self.q, and the method dequeue() should remove items from the front of list self.q. We now need to decide what is the front of the list self.q. We can choose the front to be the leftmost list element (i.e., at index 0) or the rightmost one (at index 1). Both will work, and the benefit of each depends on the underlying implementation of the built-in class list—which is beyond the scope of this chapter.

In Figure 8.6, the first element of the queue is shown on the left, which we usually associate with index 0, and we thus do the same in our implementation. Once we make this decision, the Queue class can be implemented:

Module: ch8.py
1 class Queue:
2     ‘a classic queue class’
3
4     def _ _init_ _(self):
5         ‘instantiates an empty list’
6         self.q = []
7
8     def isEmpty(self):
9         ‘returns True if queue is empty, False otherwise’
10        return (len(self.q) == 0)
11
12    def enqueue (self, item):
13        ‘insert item at rear of queue’
14        return self.q.append(item)
15
16    def dequeue(self):
17        ‘remove and return item at front of queue’
18        return self.q.pop(0)

8.4 Overloaded Operators

There are a few inconveniences with the user-defined classes we have developed so far. For example, suppose you create a Point object:

>>> point = Point(3,5)

and then tried to evaluate it:

>>> point
<_ _main_ _.Point object at 0x15e5410>

Not very user-friendly, is it? By the way, the code says that point refers of an object of type Point—where Point is defined in the namespace of the top module—and that its object ID—memory address, effectively—is 0x15e5410, in hex. In any case, probably that is not the information we wanted to get when we evaluated point.

Here is another problem. To obtain the number of characters in a string or the number of items in a list, dictionary, tuple, or set, we use the len() function. It seems natural to use the same function to obtain the number of items in a Queue container object. Unfortunately, we do not get that:

>>> fruit = Queue()
>>> fruit.enqueue(‘apple’)
>>> fruit.enqueue(‘banana’)
>>> fruit.enqueue(‘coconut’)
>>> len(fruit)
Traceback (most recent call last):
  File “<pyshell#356>”, line 1, in <module>
    len(fruit)
TypeError: object of type ‘Queue’ has no len()

The point we are making is this: The classes we have developed so far do not behave like built-in classes. For user-defined classes to be useful and easy to use, it is important to make them more more familiar (i.e., more like built-in classes). Fortunately, Python supports operator overloading, which makes this possible.

Operators Are Class Methods

Consider the operator +. It can be used to add numbers:

>>> 2 + 4
6

It can also be used to concatenate lists and strings:

>>> [4, 5, 6] + [7]
[4, 5, 6, 7]
>>> ‘strin’ + ‘g’
‘string’

The + operator is said to be an overloaded operator. An overloaded operator is an operator that has been defined for multiple classes. For each class, the definition—and thus the meaning—of the operator is different. So, for example, the + operator has been defined for the int, list, and str classes. It implements integer addition for the int class, list concatenation for the list class, and string concatenation for the str class. The question now is: How is operator + defined for a particular class?

Python is an object-oriented language and, as we have said, any “evaluation,” including the evaluation of an arithmetic expression like 2 + 4, is really a method invocation. To see what method exactly, you need to use the help() documentation tool. Whether you type help(int), help(str), or help(list), you will see that the documentation for the + operator is:

…
| _ _add_ _(…)
|     x._ _add_ _(y) <==> x+y
…

This means that whenever Python evaluates expression x + y, it first substitutes it with expression x._ _add_ _(y), a method invocation by object x with object y as input argument, and then evaluates the new, method invocation, expression. This is true no matter what x and y are. So you can actually evaluate 2 + 3, [4, 5, 6] + [7] and ‘strin’+ ‘g’ using invocations to method _ _add_ _() instead:

>>> int(2)._ _add_ _(4)
6
>>> [4, 5, 6]._ _add_ _([7])
[4, 5, 6, 7]
>>> ‘strin’. _ _add_ _(‘g’)
‘string’

image Addition Is Just a Function, After All

The algebraic expression

>>> x+y

gets translated by the Python interpreter to

>>> x._ _add_ _(y)

which is a method invocation. In Chapter 7, we learned that this method invocation gets translated by the interpreter to

>>> type(x)._ _add_ _(x,y)

(Recall that type(x) evaluates to the class of object x.) This last expression is the one that really gets evaluated.

This is true, of course, for all operators: Any expression or method invocation is really a call by a function defined in the namespace of the class of the first operand.

The + operator is just one of the Python overloaded operators; Table 8.3 shows some others. For each operator, the corresponding function is shown as well as an explanation of the operator behavior for the number types, the list type, and the str type. All the operators listed are also defined for other built-in types (dict, set, etc.) and can also be defined for user-defined types, as shown next.

Note that the last operator listed is the overloaded constructor operator, which maps to function _ _init_ _(). We have already seen how we can implement and an overloaded constructor in a user-defined class. We will see that implementing other overloaded operators is very similar.

Making the Class Point User Friendly

Recall the example we started this section with:

>>> point = Point(3,5)
>>> point
<_ _main_ _.Point object at 0x15e5410>

What would we prefer point to evaluate to instead? Suppose that we want:

>>> point
Point(3, 5)

Table 8.3 Overloaded operators. Some of the commonly used overloaded operators are listed, along with the corresponding methods and behaviors for the number, list, and string types.

image

To understand how we can achieve this, we first need to understand that when we evaluate point in the shell, Python will display the string representation of the object. The default string representation of an object is its type and address, as in

<_ _main_ _.Point object at 0x15e5410>

To modify the string representation for a class, we need to implement the overloaded operator repr() for the class. The operator repr() is called automatically by the interpreter whenever the object must be represented as a string. One example of when that is the case is when the object needs to be displayed in the interpreter shell. So the familiar representation [3, 4, 5] of a list lst containing numbers 3, 4, and 5

>>> lst
[3, 4, 5]

is really the display of the string output by the call repr(lst)

>>> repr(lst)
‘[3, 4, 5]’

All built-in classes implement overloaded operator repr() for this purpose. To modify the default string representation of objects of user-defined classes, we need to do the same. We do so by implementing the method corresponding to operator repr() in Table 8.3, method _ _repr_ _().

To get a Point object displayed in the format Point(<x>, <y>), all we need to do is add the next method to the class Point:

Module: cards.py
1 class Point:
2
3     # other Point methods
4
5     def _ _repr_ _(self):
6         ‘return canonical string representation Point(x, y)’
7         return ‘Point({}, {})’.format(self.x, self.y)

Now, when we evaluate a Point object in the shell, we get what we want:

>>> point = Point(3,5)
>>> point
Point(3, 5)

image String Representations of Objects

There are actually two ways to get a string representation of an object: the overloaded operator repr() and the string constructor str().

The operator repr() is supposed to return the canonical string representation of the object. Ideally, but not necessarily, this is the string representation you would use to construct the object, such as ‘[2, 3, 4]’ or ‘Point(3, 5)’.

In other words, the expression eval(repr(o)) should give back the original object o. The method repr() is automatically called when an expression evaluates to an object in the interpreter shell and this object needs to be displayed in the shell window.

The string constructor str() returns an informal, ideally very readable, string representation of the object. This string representation is obtained by method call o._ _str_ _(), if method _ _str_ _() is implemented. The Python interpreter calls the string constructor instead of the overloaded operator repr() whenever the object is to be “pretty printed” using function print(). We illustrate the difference with this class:

class Representation:
    def _ _repr_ _(self):
        return ‘canonical string representation’
    def _ _str_ _(self):
        return ‘Pretty string representation.’

Let's test it:

>>> rep = Representation()
>>> rep
canonical string representation
>>> print(rep)
Pretty string representation.

Contract between the Constructor and the repr() Operator

The last caution box stated that the output of the overloaded operator repr() should be the canonical string representation of the object. The canonical string representation of Point object Point(3, 5) is ‘Point(3, 5)’. The output of the repr() operator for the same Point object is:

>>> repr(Point(3, 5))
‘Point(3, 5)’

It seems we have satisfied the contract between the constructor and the representation operator repr(): They are the same. Let's check:

>>> Point(3, 5) == eval(repr(Point(3, 5)))
False

What did we do wrong?

Well, the problem is not with the constructor or the operator repr() but with the operator ==: It does not consider two cards with the same rank and suit necessarily equal. Let's check:

>>> Point(3, 5) == Point(3, 5)
False

The reason for this somewhat strange behavior is that for user-defined classes the default behavior for operator == is to return True only when the two objects we are comparing are the same object. Let's show that this is indeed the case:

>>> point = Point(3,5)
>>> point == point
True

As shown in Table 8.3, the method corresponding to the overloaded operator == is method _ _eq_ _(). To change the behavior of overloaded operator ==, we need to implement method _ _eq_ _() in class Point. We do so in this final version of class Point:

Module: ch8.py
1 class Point:
2     ‘class that represents a point in the plane’
3
4     def _ _init_ _(self, xcoord=0, ycoord=0):
5         ‘initializes point coordinates to (xcoord, ycoord)’
6         self.x = xcoord
7         self.y = ycoord
8     def setx(self, xcoord):
9         ‘sets x coordinate of point to xcoord’
10        self.x = xcoord
11    def sety(self, ycoord):
12        ‘sets y coordinate of point to ycoord’
13        self.y = ycoord
14    def get(self):
15        ‘returns the x and y coordinates of the point as a tuple’
16        return (self.x, self.y)
17    def move(self, dx, dy):
18        ‘changes the x and y coordinates by i and j, respectively’
19        self.x += dx
20        self.y += dy
21    def _ _eq_ _(self, other):
22        ‘self == other is they have the same coordinates’
23        return self.x == other.x and self.y == other.y
24    def _ _repr_ _(self):
25        ‘return canonical string representation Point(x, y)’

The new implementation of class Point supports the == operator in a way that makes sense

>>> Point(3, 5) == Point(3, 5)
True

and also ensures that the contract between the constructor and the operator repr() is satisfied:

>>> Point(3, 5) == eval(repr(Point(3, 5)))
True

Practice Problem 8.6

Implement overloaded operators repr() and == for the Card class. Your new Card class should behave as shown:

image

Making the Queue Class User Friendly

We now make the class Queue from the previous section friendlier by overloading operators repr(), ==, and len(). In the process we find it useful to extend the constructor.

We start with this implementation of Queue:

Module: ch8.py
 1 class Queue:
 2    ‘a classic queue class’
 3
 4    def _ _init_ _(self):
 5        ‘instantiates an empty list’
 6        self.q = []
 7
 8    def isEmpty(self):
 9        ‘returns True if queue is empty, False otherwise’
10        return (len(self.q) == 0)
11
12    def enqueue (self, item):
13        ‘insert item at rear of queue’
14        return self.q.append(item)
15
16    def dequeue(self):
17        ‘remove and return item at front of queue’
18        return self.q.pop(0)

Let's first take care of the “easy” operators. What does it mean for two queues to be equal? It means that they have the same elements in the same order. In other words, the lists that contain the items of the two queues are the same. Therefore, the implementation of operator _ _eq_ _() for class Queue should consist of a comparison between the lists corresponding to the two Queue objects we are comparing:

def _ _eq_ _(self, other):
    ‘‘‘returns True if queues self and other contain
       the same items in the same order’’’
    return self.q == other.q

The overloaded operator function len() returns the number of items in a container. To enable its use on Queue objects, we need to implement the corresponding method _ _len_ _() (see Table 8.3) in the Queue class. The length of the queue is of course the length of the underlying list self.q:

def _ _len_ _(self):
    ‘return number of items in queue’
    return len(self.q)

Let's now tackle the implementation of the repr() operator. Suppose we construct a queue like this:

>>> fruit = Queue()
>>> fruit.enqueue(‘apple’)
>>> fruit.enqueue(‘banana’)
>>> fruit.enqueue(‘coconut’)

What do we want the canonical string representation to look like? How about:

>>> fruit
Queue([‘apple’, ‘banana’, ‘coconut’])

Recall that when implementing the overloaded operator repr(), ideally we should satisfy the contract between it and the constructor. To satisfy it, we should be able to construct the queue as shown:

>>> Queue([‘apple’, ‘banana’, ‘coconut’])
Traceback (most recent call last):
  File “<pyshell#404>”, line 1, in <module>
    Queue([‘apple’, ‘banana’, ‘coconut’])
TypeError: _ _init_ _() takes exactly 1 positional argument (2 given)

We cannot because we have implemented the Queue constructor so it does not take any input arguments. So, we decide to change the constructor, as shown next. The two benefits of doing this are that (1) the contract between the constructor and repr() is satisfied and (2) newly created Queue objects can now be initialized at instantiation time.

Module: ch8.py
 1 class Queue:
 2    ‘a classic queue class’
 3
 4    def _ _init_ _(self, q = None):
 5        ‘initialize queue based on list q, default is empty queue’
 6        if q == None:
 7           self.q = []
 8        else:
 9           self.q = q
10
11   # methods enqueue, dequeue, and isEmpty defined here

12
13   def _ _eq_ _(self, other):
14       ‘‘‘return True if queues self and other contain
15          the same items in the same order’’’
16       return self.q == other.q
17
18   def _ _len_ _(self):
19       ‘returns number of items in queue’
20       return len(self.q)
21
22   def _ _repr_ _(self):
23       ‘return canonical string representation of queue’
24       return ‘Queue({})’.format(self.q)

Practice Problem 8.7

Implement overloaded operators len(), repr(), and == for the Deck class. Your new Deck class should behave as shown:

>>> len(Deck()))
52
>>> Deck() == Deck()
True
>>> Deck() == eval(repr(Deck()))
True

8.5 Inheritance

Code reuse is a fundamental software engineering goal. One of the main reasons for wrapping code into functions is to more easily reuse the code. Similarly, a major benefit of organizing code into user-defined classes is that the classes can then be reused in other programs, just as it is possible to use a function in the development of another. A class can be (re)used as-is, something we have been doing since Chapter 2. A class can also be “extended” into a new class through class inheritance. In this section, we introduce the second approach.

Inheriting Attributes of a Class

Suppose that in the process of developing an application, we find that it would be very convenient to have a class that behaves just like the built-in class list but also supports a method called choice() that returns an item from the list, chosen uniformly at random.

More precisely, this class, which we refer to as MyList, would support the same methods as the class list and in the same way. For example, we would be able to create a MyList container object:

>>> mylst = MyList()

We also would be able to append items to it using list method append(), compute the number of items in it using overloaded operator len(), and count the number of occurrences of an item using list method count():

>>> mylst.append(2)
>>> mylst.append(3)
>>> mylst.append(5)
>>> mylst.append(3)
>>> len(mylst)
4
>>> mylst.count(3)
2

In addition to supporting the same methods that the class list supports, the class MyList should also support method choice() that returns an item from the list, with each item in the list equally likely to be chosen:

>>> mylst.choice()
5
>>> mylst.choice()
2
>>> mylst.choice()
5

One way to implement the class MyList is the approach we took when developing classes Deck and Queue. A list instance variable self.lst would be used to store the items of MyList:

import random
class MyList:
    def _ _init_ _(self, initial = []):
        self.lst = initial
    def _ _len_ _(self):
        return len(self.lst)
    def append(self, item):
        self.lst.append(self, item)
    # implementations of remaining “list” methods
    def choice(self):
      return random.choice(self.lst)

This approach to developing class MyList would require us to write more than 30 methods. It would take a while and be tedious. Wouldn't it be nicer if we could define class MyList in a much shorter way, one that essentially says that class MyList is an “extension” of class list with method choice() as an additional method? It turns out that we can:

Module: ch8.py
1 import random
2 class MyList(list):
3     ‘a subclass of list that implements method choice’
4
5     def choice(self):
6         ‘return item from list chosen uniformly at random’
7         return random.choice(self)

This class definition specifies that class MyList is a subclass of the class list and thus supports all the methods that class list supports. This is indicated in the first line

class MyList(list):

The hierarchical structure between classes list and MyList is illustrated in Figure 8.7.

image

Figure 8.7 Hierarchy of classes list and MyList. Some of the attributes of class list are listed, all of which refer to appropriate functions. Class MyList is a subclass of class list and inherits all the attributes of class list. It also defines an additional attribute, method choice(). The object referred to by mylst inherits all the class attributes from its class, MyList, which includes the attributes from class list.

Figure 8.7 shows a MyList container object called mylst that is created in the interpreter shell (i.e., in the _ _main_ _ namespace):

>>> mylst = MyList([2, 3, 5, 3])

The object mylst is shown as a “child” of class MyList. This hierarchical representation illustrates that object mylst inherits all the attributes of class MyList. We saw that objects inherit the attributes of their class in Section 8.1.

Figure 8.7 also shows class MyList as a “child” of class list. This hierarchical representation illustrates that class MyList inherits all the attributes of list. You can check that using the built-in function dir():

>>> dir(MyList)
[‘_ _add_ _’, ‘_ _class_ _’, ‘_ _contains_ _’, ‘__delattr_ _’,
…
‘append’, ‘choice’, ‘count’, ‘extend’, ‘index’, ‘insert’,
‘pop’, ‘remove’, ‘reverse’, ‘sort’]

What this means is that object mylst will inherit not only method choice() from class MyList but also all the attributes of list. You can, again, check that:

>>> dir(mylst)
[‘_ _add_ _’, ‘_ _class_ _’, ‘_ _contains_ _’, ‘_ _delattr_ _’,
…
‘append’, ‘choice’, ‘count’, ‘extend’, ‘index’, ‘insert’,
‘pop’, ‘remove’, ‘reverse’, ‘sort’]

The class MyList is said to be a subclass of class list. The class list is the superclass of class MyList.

Class Definition, in General

When we implemented classes Point, Animal, Card, Deck, and Queue, we used this format for the first line of the class definition statement:

class <Class Name>:

To define a class that inherits attributes from an existing class <Super Class>, the first line of the class definition should be:

class <Class Name>(<Super Class>):

It is also possible to define a class that inherits attributes from more than just one existing class. In that case, the first line of the class definition statement is:

class <Class Name>(<Super Class 1>, <Super Class 2>, …):

Overriding Superclass Methods

We illustrate class inheritance using another simple example. Suppose that we need a class Bird that is similar to the class Animal from Section 8.1. The class Bird should support methods setSpecies() and setLanguage(), just like class Animal:

>>> tweety = Bird()
>>> tweety.setSpecies(‘canary’)
>>> tweety.setLanguage(‘tweet’)

The class Bird should also support a method called speak(). However, its behavior differs from the behavior of the Animal method speak():

>>> tweety.speak()
tweet! tweet! tweet!

Here is another example of the behavior we expect from class Bird:

>>> daffy = Bird()
>>> daffy.setSpecies(‘duck’)
>>> daffy.setLanguage(‘quack’)
>>> daffy.speak()
quack! quack! quack!

Let's discuss how to implement class Bird. Because class Bird shares attributes with existing class Animal (birds are animals, after all), we develop it as a subclass of Animal. Let's first recall the definition of class Animal from Section 8.1:

Module: ch8.py
1 class Animal:
2     ‘represents an animal’
3
4     def setSpecies(self, species):
5         ‘sets the animal species’
6         self.spec = species
7
8     def setLanguage(self, language):
9         ‘sets the animal language’
10        self.lang = language
11
12    def speak(self):
13        ‘prints a sentence by the animal’
14        print(‘I am a {} and I {}.’.format(self.spec, self.lang))

If we define class Bird as a subclass of class Animal, it will have the wrong behavior for method speak(). So the question is this: Is there a way to define Bird as a subclass of Animal and change the behavior of method speak() in class Bird?

There is, and it is simply to implement a new method speak() in class Bird:

Module: ch8.py
1 class Bird(Animal):
2     ‘represents a bird’
3
4     def speak(self):
5         ‘prints bird sounds’
6         print(‘{}! ’.format(self.language) * 3)

Class Bird is defined to be a subclass of Animal. Therefore, it inherits all the attributes of class Animal, including the Animal method speak(). There is a method speak() defined in class Bird, however; this method replaces the inherited Animal method. We say that the Bird method overrides the superclass method speak().

Now, when method speak() is invoked on a Bird object like daffy, how does the Python interpreter decide which method speak() to invoke? We use Figure 8.8 to illustrate how the Python interpreter searches for attribute definitions.

image

Figure 8.8 Namespaces associated with classes Animal and Bird, object daffy, and the shell. Omitted are the values of instance variables and implementations of class methods.

When the interpreter executes

>>> daffy = Bird()

it creates a Bird object named daffy and a namespace, initially empty, associated with it. Now let's consider how the Python interpreter finds the definition of setSpecies() in:

>>> daffy.setSpecies(‘duck’)

The interpreter looks for the definition of attribute setSpecies starting with the name-space associated with object daffy and continuing up the class hierarchy. It does not find the definition in the namespace associated with object daffy or in the namespace associated with class Bird. Eventually, it does find the definition of setSpecies in the names-pace associated with class Animal.

The search for the method definition when the interpreter evaluates

>>> daffy.setLanguage(‘quack’)

also ends with the namespace of class Animal.

However, when the Python interpreter executes

>>> daffy.speak()
quack! quack! quack!

the interpreter finds the definition of method speak() in class Bird. In other words, the search for attribute speak never reaches the class Animal. It is the Bird method speak() that is executed.

Attribute Names Issues image

Now that we understand how object attributes are evaluated by the Python interpreter, we can discuss the problems that can arise with carelessly chosen attribute names. Consider, for example, this class definition

class Problem:
    def value(self, v):
        self.value = v

and try:

>>> p = Problem()
>>> p.value(9)
>>> p.value
9

So far, so good. When executing p.value(9), the object p does not have an instance variable value, and the attribute search ends with the function value() in class Problem. An instance variable value is then created in the object itself, and that is confirmed by the evaluation of the statement that follows, p.value.

Now suppose we try:

>>> p.value(3)
Traceback (most recent call last):
  File “<pyshell#324>”, line 1, in <module>
    p.value(9)
TypeError: ‘int’ object is not callable

What happened? The search for attribute value started and ended with the object p: The object has an attribute called value. That attribute refers to an integer object, 9, which cannot be called like a function.

Extending Superclass Methods

We have seen that a subclass can inherit a method from a superclass or override it. It is also possible to do extend a superclass method. We illustrate this using an example that compares the three inheritance patterns.

When designing a class as a subclass of another class, inherited attributes are handled in several ways. They can be inherited as is, they can be replaced, or they can be extended. The next module shows three subclasses of class Super. Each illustrates one of the ways an inherited attribute is handled.

Module: ch8.py
 1 class Super:
 2     ‘a generic class with one method’
 3     def method(self):                     # the Super method
 4         print(‘in Super.method’)
 5
 6 class Inheritor(Super):
 7     ‘class that inherits method’
 8     pass
 9
10 class Replacer(Super):
11     ‘class that overrides method’
12     def method(self):
13         print(‘in Replacer.method’)
14
15 class Extender(Super):
16     ‘class that extends method’
17     def method(self):
18         print(‘starting Extender.method’)
19         Super.method(self)                # calling Super method
20         print(‘ending Extender.method’)

In class Inheritor, attribute method() is inherited as is. In class Replacer, it is completely replaced. In Extender, attribute method() is overridden, but the implementation of method() in class Extender calls the original method() from class Super. Effectively, class Extender adds additional behavior to the superclass attribute.

In most cases, a subclass will inherit different attributes in different ways, but each inherited attribute will follow one of these patterns.

Practice Problem 8.8

Implement a class Vector that supports the same methods as the class Point we developed in Section 8.4. The class Vector should also support vector addition and product operations. The addition of two vectors

>>> v1 = Vector(1, 3)
>>> v2 = Vector(-2, 4)

is a new vector whose coordinates are the sum of the corresponding coordinates of v1 and v2:

>>> v1 + v2
Vector(-1, 7)

The product of v1 and v2 is the sum of the products of the corresponding coordinates:

>>> v1 * v2
10

In order for a Vector object to be displayed as Vector(.,.) instead of Point(.,.), you will need to override method _ _repr_ _().

Implementing a Queue Class by Inheriting from list

The class Queue we developed in Sections 8.3 and 8.4 is just one way to design and implement a queue class. Another implementation becomes natural after we recognize that every Queue object is just a “thin wrapper” for a list object. So why not design the Queue class so that every Queue object is a list object? In other words, why not design the Queue class as a subclass of list? So let's do it:

Module: ch8.py
1 class Queue2(list):
2     ‘a queue class, subclass of list’
3
4     def isEmpty(self):
5         ‘returns True if queue is empty, False otherwise’
6         return (len(self) == 0)
7
8     def dequeue(self):
9         ‘remove and return item at front of queue’
10        return self.pop(0)
11
12    def enqueue (self, item):
13        ‘insert item at rear of queue’
14        return self.append(item)

Note that because variable self refers to a Queue2 object, which is a subclass of list, it follows that self is also a list object. So list methods like pop() and append() are invoked directly on self. Note also that methods _ _repr_ _() and _ _len_ _() do not need to be implemented because they are inherited from the list superclass.

Developing class Queue2 involved a lot less work than developing the original class Queue. Does that make it better?

Inheriting Too Much image

While inheriting a lot is desirable in real life, there is such a thing as too much inheritance in OOP. While straightforward to implement, class Queue2 has the problem of inheriting all the list attributes, including methods that violate the spirit of a queue. To see this, consider this Queue2 object:

>>> q2
[5, 7, 9]

The implementation of Queue2 allows us to remove items from the middle of the queue:

>>> q2.pop(1)
7
>>> q2
[5, 9]

It also allows us to insert items into the middle of the queue:

>>> q2.insert(1,11)
>>> q2
[5, 11, 9]

So 7 got served before 5 and 11 got into the queue in front of 9, violating queue rules. Due to all the inherited list methods, we cannot say that class Queue2 behaves in the spirit of a queue.

8.6 User-Defined Exceptions

There is one problem with the implementation of class Queue we developed in Section 8.4. What happens when we try to dequeue an empty queue? Let's check. We first create an empty queue:

>>> queue = Queue()

Next, we attempt to dequeue it:

>>> queue.dequeue()
Traceback (most recent call last):
  File “<pyshell#185>”, line 1, in <module>
    queue.dequeue()
File “/Users/me/ch8.py”,
  line 156, in dequeue
  return self.q.pop(0)
IndexError: pop from empty list

An IndexError exception is raised because we are trying to remove the item at index 0 from empty list self.q. What is the problem?

The issue is not the exception: Just as for popping an empty list, there is no other sensible thing to do when trying to dequeue an empty queue. The issue is the type of exception. An IndexError exception and the associated message ‘pop from empty list’ are of little use to the developer who is using the Queue class and who may not know that Queue containers use list instance variables.

Much more useful to the developer would be an exception called EmptyQueueError with a message like ‘dequeue from empty queue’. In general, often it is a good idea to define your own exception type rather than rely on a generic, built-in exception class like IndexError. A user-defined class can, for example, be used to customize handling and the reporting of errors.

In order to obtain more useful error messages, we need to learn two things:

  1. How to define a new exception class
  2. How to raise an exception in a program

We discuss how to do the latter first.

Raising an Exception

In our experience so far, when an exception is raised during the execution of a program, it is raised by the Python interpreter because an error condition occurred. We have seen one type of exception not caused by an error: It is the KeyboardInterrupt exception, which typically is raised by the user. The user would raise this exception by simultaneously clicking keys image to terminate an infinite loop, for example:

>>> while True:
        pass

Traceback (most recent call last):
  File “<pyshell#210>”, line 2, in <module>
    pass
KeyboardInterrupt

(The infinite loop is interrupted by a KeyboardInterrupt exception.)

In fact, all types of exceptions, not just KeyboardInterrupt exceptions, can be raised by the user. The raise Python statement forces an exception of a given type to be raised. Here is how we would raise a ValueError exception in the interpreter shell:

>>> raise ValueError()
Traceback (most recent call last):
  File “<pyshell#24>”, line 1, in <module>
    raise ValueError()
ValueError

Recall that ValueError is just a class that happens to be an exception class. The raise statement consists of the keyword raise followed by an exception constructor such as ValueError(). Executing the statement raises an exception. If it is not handled by the try/except clauses, the program is interrupted and the default exception handler prints the error message in the shell.

The exception constructor can take an input argument that can be used to provide information about the cause of the error:

>>> raise ValueError(‘Just joking …’)
Traceback (most recent call last):
  File “<pyshell#198>”, line 1, in <module>
    raise ValueError(‘Just joking …’)
ValueError: Just joking …

The optional argument is a string message that will be associated with the object: It is, in fact, the informal string representation of the object, that is, the one returned by the _ _str_ _() method and printed by the print() function.

In our two examples, we have shown that an exception can be raised regardless of whether it makes sense or not. We make this point again in the next Practice Problem.

Practice Problem 8.9

Reimplement method dequeue() of class Queue so that a KeyboardInterrupt exception (an inappropriate exception type in this case) with message ‘dequeue from empty queue’ (an appropriate error message, actually) is raised if an attempt to dequeue an empty queue is made:

>>> queue = Queue()
>>> queue.dequeue()
Traceback (most recent call last):
  File “<pyshell#30>”, line 1, in <module>
    queue.dequeue()
  File “/Users/me/ch8.py”, line 183, in dequeue
    raise KeyboardInterrupt(‘dequeue from empty queue’)
KeyboardInterrupt: dequeue from empty queue

User-Defined Exception Classes

We now describe how to define our own exception classes.

Every built-in exception type is a subclass of class Exception. In fact, all we have to do to define a new exception class is to define it as a subclass, either directly or indirectly, of Exception. That's it.

As an example, here is how we could define a new exception class MyError that behaves exactly like the Exception class:

>>> class MyError(Exception):
        pass

(This class only has attributes that are inherited from Exception; the pass statement is required because the class statement expects an indented code block.) Let's check that we can raise a MyError exception:

>>> raise MyError(‘test message’)
Traceback (most recent call last):
  File “<pyshell#247>”, line 1, in <module>
    raise MyError(‘test message’)
MyError: test message

Note that we were also able to associate error message ‘test message’ with the exception object.

Improving the Encapsulation of Class Queue

We started this section by pointing out that dequeueing an empty queue will raise an exception and print an error message that has nothing to do with queues. We now define a new exception class EmptyQueueError and reimplement method dequeue() so it raises an exception of that type if it is invoked on an empty queue.

We choose to implement the new exception class without any additional methods:

Module: ch8.py
1 class EmptyQueueError(Exception):
2     pass

Shown next is the new implementation of class Queue, with a new version of method dequeue(); no other Queue method is modified.

Module: ch8.py
1 class Queue:
2     ‘a classic queue class’
3     # methods _ _init_ _(), enqueue(), isEmpty(), _ _repr_ _(),
4     # _ _len_ _(), _ _eq_ _() implemented here
5
6     def dequeue(self):
7         if len(self) == 0:
8            raise EmptyQueueError(‘dequeue from empty queue’)
9         return self.q.pop(0)

With this new Queue class, we get a more meaningful error message when attempting to dequeue an empty queue:

>>> queue = Queue()
>>> queue.dequeue()
Traceback (most recent call last):
  File “<pyshell#34>”, line 1, in <module>
    queue.dequeue()
  File “/Users/me/ch8.py”, line 186, in dequeue
    raise EmptyQueueError(‘dequeue from empty queue’)
EmptyQueueError: dequeue from empty queue

We have effectively hidden away the implementation details of class Queue.

8.7 Case Study: Indexing and Iterators

In this case study, we will learn how to make a container class feel more like a built-in class. We will see how to enable indexing of items in the container and how to enable iteration, using a for loop, over the items in the container.

Because iterating over a container is an abstract task that generalizes over different types of containers, software developers have developed a general approach for implementing iteration behavior. This approach, called the iterator design pattern, is just one among many OOP design patterns that have been developed and cataloged for the purpose of solving common software development problems.

Overloading the Indexing Operators

Suppose that we are working with a queue, whether of type Queue or Queue2, and would like to see what item is in the 2nd, 3rd, or 24th position in the queue. In other words, we would like to use the indexing operator [] on the queue object.

We implemented the class Queue2 as a subclass of list. Thus Queue2 inherits all the attributes of class list, including the indexing operator. Let's check that. We first build the Queue2 object:

>>> q2 = Queue2()
>>> q2.enqueue(5)
>>> q2.enqueue(7)
>>> q2.enqueue(9)

Now we use the indexing operator on it:

>>> q2[1]
7

Let's now turn our attention to the original implementation, Queue. The only attributes of class Queue are the ones we implemented explicitly. It therefore should not support the indexing operator:

>>> q = Queue()
>>> q.enqueue(5)
>>> q.enqueue(7)
>>> q.enqueue(9)
>>> q
[5, 7, 9]
>>> q[1]
Traceback (most recent call last):
  File “<pyshell#18>”, line 1, in <module>
    q[1]
TypeError: ‘Queue’ object does not support indexing

In order to be able to access Queue items using the indexing operator, we need to add method _ _getitem_ _() to the Queue class. This is because when the indexing operator is used on an object, as in q[i], the Python interpreter will translate that to a call to method _ _getitem_ _(), as in q._ _getitem(i); if method _ _getitem_ _() is not implemented, then the object's type does not support indexing.

Here is the implementation of _ _getitem_ _() we will add to class Queue:

def _ _getitem_ _(self, key):
    return self.q[key]

The implementation relies on the fact that lists support indexing: To get the queue item at index key, we return the item at index key of the list self.q. We check that it works:

>>> q = queue()
>>> q.enqueue(5)
>>> q.enqueue(7)
>>> q.enqueue(9)
>>> q[1]
7

OK, so we now can use the indexing operator to get the item of a Queue at index 1. Does this mean we can change the item at index 1?

>>> q[1] = 27
Traceback (most recent call last):
  File “<pyshell#48>”, line 1, in <module>
    q[1] = 27
TypeError: ‘queue’ object does not support item assignment

That's a no. Method _ _getitem_ _() gets called by the Python interpreter only when we evaluate self[key]. When we attempt to assign to self[key], the overloaded operator _ _setitem_ _() is called by the Python interpreter instead. If we wanted to allow assignments such as q[1] = 27, then we would have to implement a method _ _setitem_ _() that takes a key and an item as input and places the item at position key.

A possible implementation of _ _setitem_ _() could be:

def _ _setitem_ _(self, key, item):
    self.q[key] = item

This operation, however, does not make sense for a queue class, and we do not to add it.

One benefit of implementing the method _ _getitem_ _() is that it allows us to iterate over a Queue container, using the iteration loop pattern:

>>> for item in q:
        print(item)

5
7
9

Before implementing the method _ _getitem_ _(), we could not have done that.

Practice Problem 8.10

Recall that we can also iterate over a Queue container using the counter loop pattern (i.e., by going through the indexes):

>>> for i in range(len(q)):
        print(q[i])

3
5
7
9

What overloaded operator, in addition to the indexing operator, needs to be implemented to be able to iterate over a container using this pattern?

Iterators and OOP Design Patterns

Python supports iteration over all the built-in containers we have seen: strings, lists, dictionaries, tuples, and sets. We have just seen that by adding the indexing behavior to a user-defined container class, we can iterate over it as well. The remarkable thing is that the same iteration pattern is used for all the container types:

for c in s: # s is a string
    print(char)

for item in lst: # lst is a list
    print(item)

for key in d: # d is a dictionary
    print(key)

for item in q: # q is a Queue (user-defined class)
    print(item)

The fact that the same code pattern is used to iterate over different types of containers is no accident. Iteration over items in a container transcends the container type. Using the same familiar pattern to encode iteration simplifies the work of the developer when reading or writing code. That said, because each container type is different, the work done by the for loop will have to be different depending on the type of container: Lists have indexes and dictionaries do not, for example, so the for loop has to work one way for lists and another way for dictionaries.

To explore iteration further, we go back to iterating over a Queue container. With our current implementation, iteration over a queue starts at the front of the queue and ends at the rear of the queue. This seems reasonable, but what if we really, really wanted to iterate from the rear to the front, as in:

>>> q = [5, 7, 9]
>>> for item in q:
        print(item)

9
7
5

Are we out of luck?

Fortunately, Python uses an approach to implement iteration that can be customized. To implement the iterator pattern, Python uses classes, overloaded operators, and exceptions in an elegant way. In order to describe it, we need to first understand how iteration (i.e., a for loop) works. Let's use the next for loop as an example:

>>> s = ‘abc’
>>> for c in s:
        print(c)

a
b
c

What actually happens in the loop is this: The for loop statement causes the method _ _iter_ _() to be invoked on the container object (string ‘abc’ in this case.) This method returns an object called an iterator; the iterator will be of a type that implements a method called _ _next_ _(); this method is then used to access items in the container one at a time. Therefore, what happens behind the scenes when the last for loop executes is this:

>>> s = ‘abc’
>>> it = s._ _iter_ _()
>>> it._ _next_ _()
‘a’
>>> it._ _next_ _()
‘b’
>>> it._ _next_ _()
‘c’
>>> it._ _next_ _()
Traceback (most recent call last):
  File “<pyshell#173>”, line 1, in <module>
    it._ _next_ _()
StopIteration

After the iterator has been created, the method _ _next_ _() is called repeatedly. When there are no more elements, _ _next_ _() raises a StopIteration exception. The for loop will catch that exception and terminate the iteration.

In order to add custom iterator behavior to a container class, we need to do two things:

  1. Add to the class method _ _iter_ _(), which returns an object of a iterator type (i.e., of a type that supports the _ _next()_ _ method).
  2. Implement the iterator type and in particular the method _ _next_ _().

We illustrate this by implementing iteration on Queue containers in which queue items are visited from the rear to the front of the queue. First, a method _ _iter_ _() needs to be added to the Queue class:

Module: ch8.py
1 class Queue:
2     ‘a classic queue class’
3
4     # other Queue methods implemented here
5
6     def _ _iter_ _(self):
7         ‘returns Queue iterator’
8         return QueueIterator(self)

The Queue method _ _iter_ _() returns an object of type QueueIterator that we have yet to implement. Note, however, that argument self is passed to the QueueIterator() constructor: In order to have an iterator that iterates over a specific queue, it better have access to the queue.

Now let's implement the iterator class QueueIterator. We need to implement the QueueIterator class constructor so it takes in a reference to the Queue container it will iterate over:

class QueueIterator:
    ‘iterator for Queue container class’

    def _ _init_ _(self, q):
        ‘constructor’
        self.q = q

    # method next to be implemented

The method _ _next_ _() is supposed to return the next item in the queue. This means that we need to keep track of what the next item is, using an instance variable we will call index. This variable will need to be initialized, and the place to do that is in the constructor. Here is the complete implementation:

Module: ch8.py
1 class QueueIterator:
2     ‘iterator for Queue container class’
3
4     def _ _init_ _(self, q):
5         ‘constructor’
6         self.index = len(q)-1
7         self.q = q
8

9     def _ _next_ _(self):
10        ‘‘‘returns next Queue item; if no next item,
11           raises StopIteration exception’’’
12        if self.index < 0:        # no next item
13            raise StopIteration()
14
15        # return next item
16        res = self.q[self.index]
17        self.index -= 1
18        return res

The method _ _next_ _() will raise an exception if there are no more items to iterate over. Otherwise, it will store the item at index index, decrement index, and return the stored item.

Practice Problem 8.11

Develop subclass oddList of list that behaves just like a list except for the peculiar behavior of the for loop:

>>> lst = oddList([‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’, ‘g’, ‘h’])
>>> lst
[‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’, ‘g’, ‘h’]
>>> for item in lst:
        print(item, end=‘ ’)

a c e g

The iteration loop pattern skips every other item in the list.

Chapter Summary

In this chapter, we describe how to develop new Python classes. We also explain the benefits of the object-oriented programming (OOP) paradigm and discuss core OOP concepts that we will make use of in this chapter and in the chapters that follow.

A new class in Python is defined with the class statement. The body of the class statement contains the definitions of the attributes of the class. The attributes are the class methods and variables that specify the class properties and what can be done with instances of the class. The idea that a class object can be manipulated by users through method invocations alone and without knowledge of the implementation of these methods is called abstraction. Abstraction facilitates software development because the programmer works with objects abstractly (i.e., through “abstract” method names rather than “concrete” code).

In order for abstraction to be beneficial, the “concrete” code and data associated with objects must be encapsulated (i.e., made “invisible” to the program using the object). Encapsulation is achieved thanks to the fact that (1) every class defines a namespace in which class attributes (variables and methods) live, and (2) every object has a namespace that inherits the class attributes and in which instance attributes live.

In order to complete the encapsulation of a new, user-defined class, it may be necessary to define class-specific exceptions for it. The reason is that if an exception is thrown when invoking a method on an object of the class, the exception type and error message should be meaningful to the user of the class. For this reason, we introduce user-defined exceptions in this chapter as well.

OOP is an approach to programming that achieves modular code through the use of objects and by structuring code into user-defined classes. While we have been working with objects since Chapter 2, this chapter finally shows the benefits of the OOP approach.

In Python, it is possible to implement operators such as + and == for user-defined classes. The OOP property that operators can have different, and new, meanings depending on the type of the operands is called operator overloading (and is a special case of the OOP concept of polymorphism). Operator overloading facilitates software development because (well-defined) operators have intuitive meanings and make the code look sparser and cleaner.

A new, user-defined class can be defined to inherit the attributes of an already existing class. This OOP property is referred to as class inheritance. Code reuse is, of course, the ultimate benefit of class inheritance. We will make heavy use of class inheritance when developing graphical user interfaces in Chapter 9 and HTML parsers in Chapter 11.

Solutions to Practice Problems

8.1 The method getx() takes no argument, other than self and returns xcoord, defined in namespace self.

def getx(self):
    ‘return x coordinate’
    return self.xcoord

8.2 The drawing for part (a) is shown in Figure 8.9(a). For part (b), you can fill in the question marks by just executing the commands. The drawing for part (c) is shown in Figure 8.9(c). The last statement a.version returns string ‘test’. This is because the assignment a.version creates name version in namespace a. object a object b

image

Figure 8.9 Solution for Practice Problem 8.2.

8.3 When created, a Rectangle object has no instance variables. The method setSize() should create and initialize instance variables to store the width and length of the rectangle. These instance variables are then used by methods perimeter() and area(). Shown next is the implementation of class Rectangle.

class Rectangle:
    ‘class that represents rectangles’

    def setSize(self, xcoord, ycoord):
        ‘constructor’
        self.x = xcoord
        self.y = ycoord

   def perimeter(self):
       ‘returns perimeter of rectangle’
       return 2*(self.x+self.y)

   def area(self):
       ‘returns area of rectangle’
       return self.x*self.y

8.4 An _ _init_ _() method is added to the class. It includes default values for input arguments species and language:

def _ _init_ _(self, species=‘animal’, language=‘make sounds’):
    ‘constructor’
    self.spec = species
    self.lang = language

8.5 Since we allow the constructor to be used with or without a list of cards, we need to implement the function _ _init_ _() with one argument and have a default value for it. This default value should really be a list containing the standard 52 playing cards, but this list has not been created yet. We choose instead to set the default value to None, a value of type NoneType and used to represent no value. We can thus start implementing the _ _init_ _() as shown:

def _ _init_ _(self, cardList = None):
    ‘constructor’
    if cardList != None:     # input deck provided
        self.deck = cardList
    else:                    # no input deck
        # self.deck is a list of 52 standard playing cards

8.6 The string returned by operator repr() must look like a statement that constructs a Card object. Operator == returns True if and only if the two cards being compared have the same rank and suit.

class Card:
    # other Card methods

    def _ _repr_ _(self):
        ‘return formal representation’
        return “Card(‘{}’, ‘{}’)”.format(self.rank, self.suit)

    def _ _eq_ _(self, other):
        ‘self = other if rank and suit are the same’
        return self.rank == other.rank and self.suit == other.suit

8.7 The implementations are shown next. The operator == decides that two decks are equal if they have the same cards and in the same order.

class Deck:
    # other Deck methods

    def _ _len_ _(self):
        ‘returns size of deck’
        return len(self.deck)

    def _ _repr_ _(self):
        ‘returns canonical string representation’
        return ‘Deck({})’.format(self.deck)

def _ _eq_ _(self, other):
    ‘‘‘returns True if decks have the same cards
       in the same order’’’
    return self.deck == other.deck

8.8 The complete implementation of the Vector class is:

class Vector(Point):
    ‘a 2D vector class’

    def _ _mul_ _(self, v):
        ‘vector product’
        return self.x * v.x + self.y * v.y

    def _ _add_ _(self, v):
        ‘vector addition’
        return Vector(self.x+v.x, self.y+v.y)

    def _ _repr_ _(self):
        ‘returns canonical string representation’
        return ‘Vector{}’.format(self.get())

8.9 If the length of the Queue object (i.e., self)is 0, a KeyboardInterrupt exception is raised:

def dequeue(self):
    ‘‘‘removes and returns item at front of the queue
       raises KeyboardInterrupt exception if queue is empty’’’
    if len(self) == 0:
        raise KeyboardInterrupt(‘dequeue from empty queue’)

    return self.q.pop(0)

8.10 The operator len(), which returns the length of the container, is used explicitly in a counter loop pattern.

8.11 The class oddList inherits all the attributes of list and overloads the _ _iter_ _() method to return a ListIterator object. Its implementation is shown next.

class oddList(list):
    ‘list with peculiar iteration loop pattern’

    def _ _iter_ _(self):
        ‘returns list iterator object’
        return ListIterator(self)

An object of type ListIterator iterates over a oddList container. The constructor initializes the instance variables lst, which refers to the oddList container, and index, which stores the index of the next item to return:

class ListIterator:
    ‘peculiar iterator for oddList class’

    def _ _init_ _(self, l):
        ‘constructor’
        self.lst = lst
        self.index = 0

    def _ _next_ _(self):
        ‘returns next oddList item’
        if self.index >= len(self.l):
            raise StopIteration
        res = self.l[self.index]
        self.index += 2
        return res

The _ _next_ _() method returns the item at position index and increments index by 2.

Exercises

8.12 Add method distance() to the class Point. It takes another Point object as input and returns the distance to that point (from the point invoking the method).

>>> c = Point()
>>> c.setx(0)
>>> c.sety(1)
>>> d = Point()
>>> d.setx(1)
>>> d.sety(0)
>>> c.distance(d)
1.4142135623730951

8.13 Add to class Animal methods setAge() and getAge() to set and retrieve the age of the Animal object.

>>> flipper = Animal()
>>> flipper.setSpecies(‘dolphin’)
>>> flipper.setAge(3)
>>> flipper.getAge()
3

8.14 Add to class Point methods up(), down(), left(), and right() that move the Point object by 1 unit in the appropriate direction. The implementation of each should not modify instance variables x and y directly but rather indirectly by calling existing method move().

>>> a = Point(3, 4)
>>> a.left()
>>> a.get()
(2, 4)

8.15 Add a constructor to class Rectangle so the length and width of the rectangle can be set at the time the Rectangle object is created. Use default values of 1 if the length or width are not specified.

>>> rectangle = Rectangle(2, 4)
>>> rectange.perimeter()
12
>>> rectangle = Rectangle()
>>> rectangle.area()
1

8.16 Translate these overloaded operator expressions to appropriate method calls:

  1. x > y
  2. x!=y
  3. x % y
  4. x // y
  5. x or y

8.17 Overload appropriate operators for class Card so that you can compare cards based on rank:

image

8.18 Implement a class myInt that behaves almost the same as the class int, except when trying to add an object of type myInt. Then, this strange behavior occurs:

>>> x = myInt(5)
>>> x * 4
20
>>> x * (4 + 6)
50
>>> x + 6
‘Whatever …’

8.19 Implement your own string class myStr that behaves like the regular str class except that:

  • The addition (+) operator returns the sum of the lengths of the two strings (instead of the concatenation).
  • The multiplication (*) operator returns the product of the lengths of the two strings.

The two operands, for both operators, are assumed to be strings; the behavior of your implementation can be undefined if the second operand is not a string.

>>> x = myStr(‘hello’)
>>> x + ‘universe’
13
>>> x * ‘universe’
40

8.20 Develop a class myList that is a subclass of the built-in list class. The only difference between myList and list is that the sort method is overridden. myList containers should behave just like regular lists, except as shown next:

>>> x = myList([1, 2, 3])
>>> x
[1, 2, 3]
>>> x.reverse()
>>> x
[3, 2, 1]
>>> x[2]
1
>>> x.sort()
You wish…

8.21 Suppose you execute the next statements using class Queue2 from Section 8.5:

>>> queue2 = Queue2([‘a’, ‘b’, ‘c’])
>>> duplicate = eval(repr(queue2))
>>> duplicate
[‘a’, ‘b’, ‘c’]
>>> duplicate.enqueue(‘d’)
Traceback (most recent call last):
  File “<pyshell#22>”, line 1, in <module>
    duplicate.enqueue(‘d’)
AttributeError: ‘list’ object has no attribute ‘enqueue’

Explain what happened and offer a solution.

8.22 Modify the solution of Practice Problem 8.11 so two list items are skipped in every iteration of a for loop.

>>> lst = oddList([‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’, ‘g’, ‘h’])
>>> for item in lst:
        print(item, end=‘ ’)

a d g

Problems

8.23 Develop a class BankAccount that supports these methods:

  • _ _init_ _(): Initializes the bank account balance to the value of the input argument, or to 0 if no input argument is given
  • withdraw(): Takes an amount as input and withdraws it from the balance
  • deposit(): Takes an amount as input and adds it to the balance
  • balance(): Returns the balance on the account
>>> x = BankAccount(700)
>>> x.balance())
700.00
>>> x.withdraw(70)
>>> x.balance()
630.00
>>> x.deposit(7)
>>> x.balance()
637.00

8.24 Implement a class Polygon that abstracts regular polygons and supports class methods:

  • _ _init_ _(): A constructor that takes as input the number of sides and the side length of a regular n-gon (n-sided polygon) object
  • perimeter(): Returns the perimeter of n-gon object
  • area(): returns the area of the n-gon object

Note: The area of a regular polygon with n sides of length s is

image

>>> p2 = Polygon(6, 1)
>>> p2.perimeter()
6
>>> p2.area()
2.5980762113533165

8.25 Implement class Worker that supports methods:

  • _ _init_ _(): Constructor that takes as input the worker's name (as a string) and the hourly pay rate (as a number)
  • changeRate(): Takes the new pay rate as input and changes the worker's pay rate to the new hourly rate
  • pay(): Takes the number of hours worked as input and prints ‘Not Implemented’

Next develop classes HourlyWorker and SalariedWorker as subclasses of Worker. Each overloads the inherited method pay() to compute the weekly pay for the worker. Hourly workers are paid the hourly rate for the actual hours worked; any overtime hours above 40 are paid double. Salaried workers are paid for 40 hours regardless of the number of hours worked. Because the number of hours is not relevant, the method pay() for salaried workers should also be callable without an input argument.

>>> w1 = Worker(‘Joe’, 15)
>>> w1.pay(35)
Not implemented
>>> w2 = SalariedWorker(‘Sue’, 14.50)
>>> w2.pay()
580.0
>>> w2.pay(60)
580.0
>>> w3 = HourlyWorker(‘Dana’, 20)
>>> w3.pay(25)
500
>>> w3.changeRate(35)
>>> w3.pay(25)
875

8.26 Create a class Segment that represents a line segment in the plane and supports methods:

  • _ _init_ _(): Constructor that takes as input a pair of Point objects that represent the endpoints of the line segment
  • length(): Returns the length of the segment
  • slope(): Returns the slope of the segment or None if the slope is unbounded
>>> p1 = Point(3,4)
>>> p2 = Point()
>>> s = Segment(p1, p2)
>>> s.length()
5.0
>>> s.slope()
0.75

8.27 Implement a class Person that supports these methods:

  • _ _init_ _(): A constructor that takes as input a person's name (as a string) and birth year (as an integer)
  • age(): Returns the age of the person
  • name(): Returns the name of the person

Use function localtime() from the Standard Library module time to compute the age.

8.28 Develop a class Textfile that provides methods to analyze a text file. The class Textfile will support a constructor that takes as input a file name (as a string) and instantiates a Textfile object associated with the corresponding text file. The Textfile class should support methods nchars(), nwords(), and nlines() that return the number of characters, words, and lines, respectively, in the associated text file. The class should also support methods read() and readlines() that return the content of the text file as a string or as a list of lines, respectively, just as we would expect for file objects.

Finally, the class should support method grep() that takes a target string as input and searches for lines in the text file that contain the target string. The method returns the lines in the file containing the target string; in addition, the method should print the line number, where line numbering starts with 0.

File: raven.txt
>>> t = Textfile(‘raven.txt’)
>>> t.nchars()
6299
>>> t.nwords()
1125
>>> t.nlines()
126
>>> print(t.read())
Once upon a midnight dreary, while I pondered weak and weary,
…
Shall be lifted - nevermore!
>>> t.grep(‘nevermore’)
75: Of ‘Never-nevermore.’
89: She shall press, ah, nevermore!
124: Shall be lifted - nevermore!

8.29 Add method words() to class Textfile from Problem 8.28. It takes no input and returns a list, without duplicates, of words in the file.

8.30 Add method occurrences() to class Textfile from Problem 8.28. It takes no input and returns a dictionary mapping each word in the file (the key) to the number of times it occurs in the file (the value).

8.31 Add method average() to class Textfile from Problem 8.28. It takes no input and returns, in a tuple object, (1) the average number of words per sentence in the file, (2) the number of words in the sentence with the most words, and (3) the number of words in the sentence with the fewest words. You may assume that the symbols delimiting a sentence are in ‘!?.’.

8.32 Implement class Hand that represents a hand of playing cards. The class should have a constructor that takes as input the player ID (a string). It should support method addCard() that takes a card as input and adds it to the hand and method showHand() that displays the player's hand in the format shown.

image

8.33 Reimplement the blackjack application from the case study in Chapter 6 using classes Card and Deck developed in this chapter and class Hand from Problem 8.32.

8.34 Implement class Date that support methods:

  • _ _init_ _(): Constructor that takes no input and initializes the Date object to the current date
  • display(): Takes a format argument and displays the date in the requested format

Use function localtime() from the Standard Library module time to obtain the current date. The format argument is a string

  • ’MDY’ : MM/DD/YY (e.g., 02/18/09)
  • ’MDYY’ : MM/DD/YYYY (e.g., 02/18/2009)
  • ’DMY’ : DD/MM/YY (e.g., 18/02/09)
  • ’DMYY’ : DD/MM/YYYY (e.g., 18/02/2009)
  • ’MODY’ : Mon DD, YYYY (e.g., Feb 18, 2009)

You should use methods localtime() and strftime() from Standard Library module time.

>>> x = Date()
>>> x.display(‘MDY’)
‘02/18/09’
>>> x.display(‘MODY’)
‘Feb 18, 2009’

8.35 Develop a class Craps that allows you to play craps on your computer. (The craps rules are described in Problem 6.31.) Your class will support methods:

  • _ _init_ _(): Starts by rolling a pair of dice. If the value of the roll (i.e., the sum of the two dice) is 7 or 11, then a winning message is printed. If the value of the roll is 2, 3, or 12, then a losing message is printed. For all other roll values, a message telling the user to throw for point is printed.
  • forPoint(): Generates a roll of a pair of dice and, depending on the value of the roll, prints one of three messages as appropriate (and as shown):
>>> c = Craps()
Throw total: 11. You won!
>>> c = Craps()
Throw total: 2. You lost!
>>> c = Craps()
Throw total: 5. Throw for Point.
>>> c.forPoint()
Throw total: 6. Throw for Point.
>>> c.forPoint()
Throw total: 5. You won!
>>> c = Craps()
Throw total: 4. Throw for Point.
 >>> c.forPoint()
Throw total: 7. You lost!

8.36 Implement class Pseudorandom that is used to generate a sequence of pseudorandom integers using a linear congruential generator. The linear congruential method generates a sequence of numbers starting from a given seed number x. Each number in the sequence will be obtained by applying a (math) function f(x) on the previous number x in the sequence. The precise function f(x) is defined by three numbers: a (the multiplier), c (the increment), and m (the modulus):

image

For example, if m = 31, a = 17, and c = 7, the linear congruential method would generate the next sequence of numbers starting from seed x = 12:

image

because f (12)= 25, f (25)= 29, f (29)= 4, and so on. The class Pseudorandom should support methods:

  • _ _init_ _(): Constructor that takes as input the values a, x, c, and m and initializes the Pseudorandom object
  • next(): Generates and returns the next number in the pseudorandom sequence
>> x = pseudorandom(17, 12, 7, 31)
>>> x.next()
25
>>> x.next()
29
>>> x.next()
4

8.37 Implement the container class Stat that stores a sequence of numbers and provides statistical information about the numbers. It supports an overloaded constructor that initializes the container and the methods shown.

>>> s = Stat()
>>> s.add(2)      # adds 2 to the Stat container
>>> s.add(4)
>>> s.add(6)
>>> s.add(8)
>>> s.min()       # returns minimum value in container
2
>>> s.max()       # returns maximum value in container
8
>>> s.sum()       # returns sum of values in container
20
>>> len(s)        # returns number of items in container
4
>>> s.mean()      # returns average of items in container
5.0
>>> 4 in s        # returns True if in the container
True
>>> s.clear()     # Empties the sequence

8.38 A stack is a sequence container type that, like a queue, supports very restrictive access methods: All insertions and removals are from one end of the stack, typically referred to as the top of the stack. Implement container class Stack that implements a stack. It should be a subclass of object, support the len() overloaded operator, and support the methods:

  • push(): Take an item as input and push it on top of the stack
  • pop(): Remove and return the item at the top of the stack
  • isEmpty(): Return True if the stack is empty, False otherwise

A stack is often referred to as a last-in first-out (LIFO) container because the last item inserted is the first removed. The stack methods are illustrated next.

>>> s = Stack()
>>> s.push(‘plate 1’)
>>> s.push(‘plate 2’)
>>> s.push(‘plate 3’)
>>> s
[‘plate 1’, ‘plate 2’, ‘plate 3’]
>>> len(s)
3
>>> s.pop()
‘plate 3’
>>> s.pop()
‘plate 2’
>>> s.pop()
‘plate 1’
>>> s.isEmpty()
True

8.39 Write a container class called PriorityQueue. The class should supports methods:

  • insert(): Takes a number as input and adds it to the container
  • min(): Returns the smallest number in the container
  • removeMin(): Removes the smallest number in the container
  • isEmpty(): Returns True if container is empty, False otherwise

The overloaded operator len() should also be supported.

>>> pq = PriorityQueue()
>>> pq.insert(3)
>>> pq.insert(1)
>>> pq.insert(5)
>>> pq.insert(2)
>>> pq.min()
1
>>> pq.removeMin()
>>> pq.min()
2
>>> pq.size()
3
>>> pq.isEmpty()
False

8.40 Implement classes Square and Triangle as subclasses of class Polygon from Problem 8.24. Each will overload the constructor method _ _init_ _ so it takes only one argument l (the side length), and each will support an additional method area() that returns the area of the n-gon object. The method _ _init_ _ should make use of the superclass _ _init_ _ method, so no instance variables (l and n) are defined in subclasses. Note: The area of an equilateral triangle of side length s is image.

>>> s = Square(2)
>>> s.perimeter()
8
>>> s.area()
4
>>> t = Triangle(3)
>>> t.perimeter()
9
>>> t.area()
6.3639610306789285

8.41 Consider the class tree hierarchy:

image

Implement six classes to model this taxonomy with Python inheritance. In class Animal, implement method speak() that will be inherited by the descendant classes of Animal as is. Complete the implementation of the six classes so they exhibit this behavior:

>>> garfield = Cat()
>>> garfield.speak()
Meeow
>>> dude = Hacker()
>>> dude.speak()
Hello world!

8.42 Implement two subclasses of class Person described in Problem 8.27. The class Instructor supports methods:

  • _ _init_ _(): Constructor that takes the person's degree in addition to name and birth year
  • degree(): Returns the degree of the instructor

The class Student, also a subclass of class Person, supports:

  • _ _init_ _(): Constructor that takes the person's major in addition to name and birth year
  • major(): Returns the major of the student

Your implementation of the three classes should behave as shown in the next code:

>>> x = Instructor(‘Smith’, 1963, ‘PhD’)
>>> x.age()
45
>>> y = Student(‘Jones’, 1987, ‘Computer Science’)
>>> y.age()
21
>>> y.major()
‘Computer Science’
>>> x.degree()
‘PhD’

8.43 In Problem 8.23, there are some problems with the implementation of the class BankAccount, and they are illustrated here:

>>> x = BankAccount(-700)
>>> x.balance()
-700
>>> x.withdraw(70)
>>> x.balance()
-770
>>> x.deposit(-7)
>>> x.balance()
Balance: -777

The problems are: (1) a bank account with a negative balance can be created, (2) the withdrawal amount is greater than the balance, and (3) the deposit amount is negative. Modify the code for the BankAccount class so that a ValueError exception is thrown for any of these violations, together with an appropriate message: ‘Illegal balance’, ‘Overdraft’, or ‘Negative deposit’.

>>> x = BankAccount2(-700)
Traceback (most recent call last):
…
ValueError: Illegal balance

8.44 In Problem 8.43, a generic ValueError exception is raised if any of the three violations occur. It would be more useful if a more specific, user-defined exception is raised instead. Define new exception classes NegativeBalanceError, OverdraftError, and DepositError that would be raised instead. In addition, the informal string representation of the exception object should contain the balance that would result from the negative balance account creation, the overdraft, or the negative deposit.

For example, when trying to create a bank account with a negative balance, the error message should include the balance that would result if the bank account creation was allowed:

>>> x = BankAccount3(-5)
Traceback (most recent call last):
…
NegativeBalanceError: Account created with negative balance -5

When a withdrawal results in a negative balance, the error message should also include the balance that would result if the withdrawal was allowed:

>>> x = BankAccount3(5)
>>> x.withdraw(7)
Traceback (most recent call last):
…
OverdraftError: Operation would result in negative balance -2

If a negative deposit is attempted, the negative deposit amount should be included in the error message:

>>> x.deposit(-3)
Traceback (most recent call last):
…
DepositError: Negative deposit -3

Finally, reimplement the class BankAccount to use these new exception classes instead of ValueError.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset