148 Notes
148 Notes
Danny Heap
Contents
introduction
iii
abstraction
1.1 objects . . . . . . . . . . . . . . . . . . . . . .
1.2 abstract data types (ADTs) . . . . . . . . . .
1.2.1 hide details . . . . . . . . . . . . . . .
1.2.2 expose interfaces . . . . . . . . . . . .
1.2.3 bundle information and behavior . . .
1.3 concretize ADTs in Python . . . . . . . . . .
1.3.1 classes . . . . . . . . . . . . . . . . . .
1.3.2 methods . . . . . . . . . . . . . . . . .
1.3.3 methods versus module-level functions
1.3.4 attributes . . . . . . . . . . . . . . . .
1.3.5 inheritance . . . . . . . . . . . . . . .
1.3.6 exceptions . . . . . . . . . . . . . . . .
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 1
. 2
. 2
. 3
. 3
. 3
. 3
. 5
. 8
. 9
. 11
. 13
collaborate . . . . . . .
break problems down . .
re-use and re-implement
document . . . . . . . .
test . . . . . . . . . . . .
refactor . . . . . . . . .
debug . . . . . . . . . .
17
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
17
17
17
17
17
17
17
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
19
20
21
21
21
21
21
recursion
3.1
3.2
3.3
3.4
.
.
.
.
.
.
.
.
.
.
.
.
maintenance
2.1
2.2
2.3
2.4
2.5
2.6
2.7
3
.
.
.
.
.
.
.
.
.
.
.
.
19
efficiency
23
4.1 searching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.2 sorting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.3 denitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
i
ii
introduction
Computers are to human beings as sand is to oysters: they irritate us into producing beautiful pearls.
Boundless promise of machines automatically carrying out our instructions for solving problems is reined in
by the constraints of computing devices and human organization. Tension between promise and constraint
leads us to form careful observations and rules about how we work with these wonderful automatic problemsolving devices. We evolve from hackers into computer scientists.
We remain true to our hacker intuition: when we see code examples, we try them out, modify them, and
try to improve them. Remember that when you see code fragments in these notes. We add our scientic
rigour: we try to understand causes and eects, we work out rules to make our code development faster and
less error-prone, then we test and reconsider those rules.
We work in a community of computer scientists, so we test our own understanding against other critical
minds, and we benet from discoveries in the past.
Welcome to computer science.
iii
iv
Chapter 1
abstraction
Computer scientists need to emphasize some details, and suppress others, in order to understand computations. You've already seen this as a programmer | you give meaningful names to functions and data, rather
than repeating the gory details of how you compute them. The user of your code deals with the high-level
| function name, and the value of its arguments | rather than low-level details of how it works.
1.1 objects
Most modern computers store and manipulate data as binary digits, represented by bits | strings of 0s and
1s, high and low voltage, or magnetic north and south dipoles. Luckly, we don't often deal with raw bits
when working with computer programs: the bits are grouped into meaningful chunks that represent number,
strings, images, toasters, and other program components. We'll call these chunks objects.
Every value in a Python program is an object and has a (virtual) memory address. You can check this
using id, or is (try typing these in a Python interpreter):
id(5)
id("five")
5 is "five"
Two separate objects may represent the same value. Since the values may be computed in independent ways,
Python may not realize that it could use just one object to represent both:
>>>
>>>
>>>
>>>
w1
w2
w1
w1
= "words"
= "swords"[1:]
== w2
is w2
Objects can contain both information and behaviour. We get at these using ".", the dot operator. Turtle
objects, used for drawing, provide an example (type these in a Python interpreter):
>>>
>>>
>>>
>>>
import turtle
t = turtle.Turtle()
t.DEFAULT_ANGLEOFFSET
t.forward(100)
turtle.Turtle is a class, or blueprint, for creating turtles. Python allows you to add new parts to an existing
class, simply by assigning to a name. If you try typing the example below, you can add wings to turtles, on
the
y:
1
>>>
>>>
>>>
>>>
>>>
import turtle
t = turtle.Turtle()
t.wings
turtle.Turtle.wings = "waterproof feathers!"
t.wings
In addition to this class docstring, you would have a docstring for each of the methods in your class. Taken
together, these provide a public interface for the stack ADT.
Once you have published an interface, it becomes incredibly awkward to modify it. Imaging billions of
enthusiastic clients are already using your ADT. That means they like it, and have written their own code
that depends on it. If sometime next week you change the behaviour of, say, push, you create two issues.
First, you have to inform your clients of the change | they may not notice that your docstring has changed
with your code update. Second (and most serious) your clients may need to re-write all their code that used
your ADT. They won't be happy.
This means you should carefully design and test your ADT before you publish an interface. Then you
should stick to that interface! There are techniques for extending an interface (see inheritance below, as well
as composition).
1.3.1 classes
You'll rst need to identify the parts, the attributes and behaviour of your new class. Below is an example
from How to think like a computer scientist. Identify the most important noun | a good candidate for your
3
class name. Then identify verbs the noun tends to use | good candidates for behaviour, or operations, of
your class. There may be some less-important nouns | good candidates for attributes of your class.
In two dimensions, a point is two numbers (coordinates) that are treated collectively as a
single object. Points are often written in parentheses with a comma separating the coordinates.
For example, (0, 0) represents the origin, and (x, y) represents the point x units to the right
and y units up from the origin. Some of the typical operations that one associates with points
might be calculating the distance of a point from the origin, or from another point, or nding a
midpoint of two points, or asking if a point falls within a given rectangle or circle.
deeply wrong class implementation
We can build the Point class in a rough-and-ready way, to get an idea of how the class mechanism works.
However dont try this at home, or at least when you continue to declare classes in your subsequent work.
The minimum you need is to tell Python your new class name. We start out with a class that has no
methods or attributes:
>>> class Point:
...
pass
...
Of course, we could add x and y coordinates directly to a point p, using something like p.x = 5. But we're
programmers, so we can write a function (not part of our class... yet):
>>> def initialize(point, x, y):
...
point.x, point.y = float(x), float(y)
...
You now have enough machinery in place to create a point, give it coordinates, and nd its distance from
the origin:
>>> p = Point()
>>> initialize(p, 3, 4)
>>> distance_from_origin(p)
5.0
It's inconvenient to have the functions initialize and distance from origin separate from the class declaration
for Point. We can add them to class Point as methods | basically functions that are part of each instance of
Point. Every class expects to have a standard function called init , so we can use our initialize function for
that:
4
>>>
>>>
>>>
12
>>>
5
>>>
>>>
3
Point.__init__ = initialize
p2 = Point(12, 5)
p2.x
p2.y
Point.__init__(p2, 3, 4)
p2.x
Notice there are two ways of using initialize once it is renamed init . You can access it using the name of
the class, Point, and provide it with values for a Point, and the desired x and y coordinates | exactly the
way initialize works, just using a dierent name for the function.
The other way to access initialize happens automatically whenever you create a new point, since init
is a standard special method. There is no need to provide a particular Point value, since it's obvious that
we're referring to the one being created.
Similarly, we can add distance to origin to class Point:
>>> Point.distance_from_origin = distance_from_origin
>>> p3 = Point(12, 5)
>>> p3.distance_from_origin()
13.0
>>> Point.distance_from_origin(p3)
13.0
Notice that when we access distance from origin using the name of Point p3, there's no need to provide it with
the rst argument specifying a Point. However, if we access it using the name of class Point, we need to tell
it a name for a particular point.
Notice the dierent number of parameters: Point.distance from origin(...) takes one parameter, a reference
to a Point. On the other hand p3.distance from origin() takes zero parameters, since it already has the name
of Point it needs | p3.
1.3.2 methods
In the previous section we dene some Python functions that were closely associated with instances of the
class Point: distance from origin and init . Such functions are called methods, and they may compute and
report some property related to a class instance (that's what distance from origin does), or they may change
(aka mutate) some properties of the instance (that's what init does). What makes them methods is that
they are attached to a class instance (indeed, we call them using a reference to a class instance), and they
usually use information from that instance to do their work.
Normally we don't dene a function outside a class and then attach it to form a method, as we did in
the previous section. Methods are so useful and common, that they are automatically dened when we have
a denition indented within the scope of a class declaration. Here's what we'd do for Point. init :
>>> class Point:
...
def __init__(self, x, y):
...
"""(Point, float, float) -> NoneType
...
...
Initialize new point self with horizontal coordinate x and
...
vertical coordinate y.
...
...
"""
self.x, self.y = float(x), float(y)
Now that we're doing things properly, our method has a docstring that includes a type contract and a brief
description that mentions every parameter. If you're wondering why the type contract returns NoneType, it
helps to know that this method takes a vanilla new object and adds attributes to it. Also, there's nothing
special about the name self for the rst parameter. We could have used me, yo, or ego and Python would
have understood this rst parameter as referring to the new instance of Point in the body of the init
method. What counts is the position: the rst parameter in a method denition is a reference to the class
instance the method belongs to.
init is an example of a method that changes, or mutates, its instance. We can also dene a method
that computes some information about an instance of Point. In the example below, we assume that the class
header for Point has already been dene somewhere above (perhaps in the previous example...):
...
...
...
...
...
...
...
...
...
...
def distance_from_origin(self):
"""(Point) -> float
Return the distance of self from origin.
>>> p = Point(3.0, 4.0)
>>> p.distance_from_origin()
5.0
"""
return (self.x**2 + self.y**2)**(1/2)
Once again our properly-dened method has a docstring with a type signature and description. In addition,
it has an example of how we expect the function to behave. When you write methods that return nonNoneType values, you should write one or more usage examples. These help shape your thinking when you
write the body of the method, and they can provide an automatic sanity-check that your function behaves
as you expect, by simply adding:
>>> if __name__ == __main__:
...
import doctest
...
doctest.testmod()
...
. . . to the end of the module with your class declaration in it. Suppose I had dened Point in a le called
point.py. Then evaluating the module point would report whether the examples matched their expected
output.
Make a habit of providing docstring examples whenever your write a method or function that returns a
non-NoneType value. Be sure to verify that your examples work as you expect.
special methods
Python classes inherit special methods from their common ancestor object. If a class developer does nothing,
the methods are used as-is, which may be inappropriate. We also have the alternative of overriding the
inherited methods, so that they have new behaviour.
You've already seen an example of a special method: init . The clue that it's a special method are
those leading and trailing s. If our declaration of Point had no denition of init , you could still invoke:
p = Point()
. . . and end up with an instance of Point that has no x or y coordinates. That omission would, in turn, mess
up distance from origin, which expects to use x and y.
Another useful special method is eq , to check whether some object is equivalent to a given object. In
the case of Point, two points are equivalent if they have the same x and y values, so a reasonable implementation of eq might be:
def __eq__(self, other):
"""(Point, object) -> bool
Return whether Point self is equivalent to other.
>>> p1
>>> p2
>>> p3
>>> p1
False
>>> p1
True
>>> p1
False
>>> p1
False
"""
return
= Point(3.0, 4.0)
= Point(3.0, 4.0)
= Point(12.0, 5.0)
is p2
== p2
== p3
== "point"
Again, we start with the function header and a docstring. This time we have several examples, to show how
that we expect two separately-declared Points with the same x and y values to not be the same | they are
dierent objects! However, we expect them to be equivalent, and the == operator is an alias for eq , and
we expect it to evaluate to True | they are equivalent. Finally, we don't expect our Point to be equivalent to
any instance of another class, for example a str.
The implementation returns a boolean expression that combines (with and) all the things we require to
be true.
While you are developing a class you may need to see a str representation of a class instance, and even
be able to evaluate that representation into an equivalent instance, so that you can examine it. The special
method repr is your friend. One way to think of it is as a method that gives you a constructor for an
equivalent object:
def __repr__(self):
"""(Point) -> str
Return a str representing self that produces an equivalent
Point when evaluated in Python.
>>> p = Point(3.0, 4.0)
>>> repr(p)
Point(3.0, 4.0)
>>> p.__repr__()
Point(3.0, 4.0)
>>> p
Point(3.0, 4.0)
"""
return "Point({}, {})".format(repr(self.x), repr(self.y))
Notice that there are several ways to call special method repr .
Sometimes you want a str representation of an object that easy to understand, but perhaps won't create
an equivalent object in a Python interpreter. If you don't implement str , Python will use repr in its
place. If you don't like that, you should implement str :
def __str__(self):
"""(Point) -> str
Return a convenient str representation of self.
>>> p = Point(3.0, 4.0)
>>> p.__str__()
(3.0, 4.0)
>>> print(p)
(3.0, 4.0)
>>> str(p)
(3.0, 4.0)
"""
return "({}, {})".format(str(self.x), str(self.y))
Notice that there are a two ways of calling str , and that str is what's used when we print an instance of
a class.
In this case, you should write your function to have explicit return statements before reaching the Exception.
The purpose of the Exception is to stop the program with a meaningful message if there is an inadvertent
execution path that would return NoneType.
Another tricky feature of Python for some beginners involves default parameters that are of mutable
(changeable) type. Keep in mind that default parameters have a value that is stored when the function is
dened, and persists for each use of the function. Here is some code that illustrates this surprising feature:
>>> def f(n, L=[]):
...
L.append(n)
...
return L
...
>>> f.__defaults__
([],)
>>> f(3)
[3]
>>> f.__defaults__
([3],)
>>> f(4)
[3, 4]
>>> f.__defaults__
([3, 4],)
May Python programmers get bitten by this feature. Unless you intend this eect, you should never use a
default parameter that can be changed (such as list L). A better idea is to use a special default value, such
as None, and use that as a condition to create an empty list.
1.3.4 attributes
As well as representing the behaviour of a class, using methods, we need to represent its attributes. We
could simply place an assignment statement within the body of a class, like so:
# [detail for class point omitted]
z = 15
def __init__(self, x, y):
"""(Point, float, float) -> NoneType
[detail omitted]
"""
# [detail omitted]
With this set-up, every Point would have a z with value 15. That's ne if z is intended to be a constant of
class Point, although a common convention for constants is to name them with a capital letter.
If, on the other hand, we want z to have dierent values for dierent points, then we should set that
value when we initialize the Point. Just add another parameter to init :
def __init__(self, x, y, z):
"""(Point, float, float, float) -> NoneType
Now we're initializing a 3-dimensional Point! This probably messes up distance from origin (again).
Attributes in Python are exposed (public) to any code that uses the class. This means it is completely
possible for incompetent or malicious code to mess with a Point, for instance:
>>> p = Point(3.0, 4.0)
>>> p
Point(3.0, 4.0)
>>> p.x = "three"
>>> p
Point(three, 4.0)
Python's approach is to, at rst, allow any client (code that uses our code) of our class instance to be able
to both evaluate, and to change, its attributes. There is a Python convention (programming good manners)
that any attribute that is preceded by an underline should not be used by client code. For example, if we
had named the horizontal component of a Point x rather than x, then well-behaved Pythonistas wouldn't
look at, or change, p. x for Point p. It's important to note that Python doesn't enforce this convention |
nothing stops a rude Python programmer from changing p. x to refer to a dierent value.
The advantage of this approach is that we can create classes and attributes without, initially, creating
methods to control access to the attributes. This makes initial code development easier.
The downside is, as we saw above, unrestricted access to attributes can completely destroy the intended
use of class instances. In many cases we'd like to lter the possible values that an attribute can refer to, or
even make the attribute read-only once an instance of a class has been created. For example, it would be
reasonable to make the coordinates of p = Point(3.0, 4.0) read-only, but still allow client code to see these
values.
Python has a mechanism to allow us to change the access to an attribute after-the-fact, without changing
the public interface our client code uses.
privacy and property
Python has no notion of attributes being private (some languages do), so they are all accessible to client
code. However, Python does provide the property mechanism to re-direct access to an attribute, so that it
must use a method.
Here's an example. In our original implementation of Point there was no control over access to coordinates
x and y. Suppose this code was shipped, and then the developers decided that they wanted to make sure
that the coordinates could not be changed after they were initialized. Since clients around the globe are
already using version 0.1 of Point, it is not feasible to ask them to change all their code that uses Point | it is
essential to keep the public interface the same. But we use Python's built-in property() function to re-direct
access to the coordinates to methods that check whether the coordinate is being re-set. Here's how we do
this for x, and the approach for y is completely analogous.
First, we create a method to set x to it's initial value. This can only be done during initialization, because
we check whether the name _x is already dened for the object self. Notice that, internally, we store this
value in x, using the underlined naming that Pythonistas are so polite about not tampering with.
def set_x(self, x):
"""(Point) -> NoneType
10
Assign x to self.x
"""
if _x in dir(self): # already set!
raise Exception(Cannot change x coordinate.)
else:
self._x = float(x)
Next, we make sure that anybody wanting to use the value of attribute x actually uses the value referred
to by x:
def get_x(self):
"""(Point) -> float
Return the value of coordinate x
>>> p = Point(3.0, 4.0)
>>> p.get_x()
3.0
"""
return self._x
None of what we've done would help unless we tell Python to re-direct all attempts to assign to, or
evaluate, x to the appropriate methods. We use function property() which takes four arguments: a method
to get the value of x, a method to set the value, a method to delete attribute x, and some documentation.
The arguments may, optionally, be None:
x = property(get_x, set_x, None, None)
And that's all there is to it. All the client code that evaluates or assigns to x is now redirected to get x
and set x. Even the code we have written in the methods of class Point itself (for example in init ) gets
redirected!
1.3.5 inheritance
Sometimes we create a new class that adds a few features to an existing class, but still represents the same
sort of thing. In order to save code, we could always include an instance of the existing class in our new
class, and use its features. This approach is called composition. However, if we decided that our new class
really is a special case of the existing class, we can create a subclass that, by default, inherits all the methods
and attributes of the class we declare to be its parent.
We do this in the class declaration by adding the name of one or more existing classes in parentheses:
from point import Point
class HeavyPoint(Point):
# ... lots of stuff omitted
Recall the Point class from earlier. If we want to specialize to a Point that also has a mass, it doesn't make
sense to re-write the code from point.py. That's tedious, and tedium breeds errors.
Inheritance allows us to inherit some of the methods of Point, to use and modify, or extend some of its
methods, and to completely replace, or override other methods. Here are some examples.
11
HeavyPoint. str needs to add a factor of f to the str representation of self. Notice how it re-uses the
alread-existing code by calling Point. str :
def __str__(self):
"""(HeavyPoint) -> str
Return a convenient str representation of self.
>>> p = HeavyPoint(1.0, 2.0, 3.0)
>>> print(p)
3.0(1.0, 2.0)
"""
return {}{}.format(str(self.m), Point.__str__(self))
Some of the methods from Point work without any modication in HeavyPoint: distance from origin and
scale, for example. Without writing any code in HeavyPoint, we get these for free.
How does Python decide which version of a method to use when it might be dened in a class, its
superclass (or the superclass of the superclass. . . )? It checks rst in the most specic class that has been
declared. If it doesn't nd the method dened there, it checks the superclass (there may be more than one).
If it doesn't nd it there, it checks the superclass's superclass. And so on. The rst method it nds is the
one it uses.
Using this approach, Python uses the repr method dened in HeavyPoint and ignores the one dened
in Point. We have a mixture of approaches for HeavyPoint. eq , since Python nds a denition in HeavyPoint,
but this denition includes a reference to Point. eq . This means it uses the denition from HeavyPoint,
which in turn uses the denition from Point.
In order to challenge your understanding of inheritance, read over the bare-bones class declarations below,
and predict the output of the two print statements before running the code. Explain your results.
class A:
def g(self, n):
return n
12
1.3.6 exceptions
Programmers usually plan for ordinary circumstances. For example, IntStack.pop is designed to return an
integer. Under exceptional circumstances, if this is called on an empty IntStack, it doesn't return any value:
it raises a PopEmptyStackException. If the client code, which called IntStack.pop knows what to do with a
PopEmptyStackException, it does, otherwise it also stops running and passes it on to what ever code called it.
If no part of the program knows what to do, execution stops with information about what happened:
>>> i.pop()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/heap/148W15/Notes/int_stack.py", line 53, in pop
raise PopEmptyStackException
int_stack.PopEmptyStackException
Without dening a special Exception, every programmer has (usually inadvertently) triggered Exceptions of
their own. Try out these expressions:
>>> int("seven")
>>> a = 1/0
>>> [1, 2][2]
Each expression raises some sort of variety, and if the client code that calls it (in this case, the prompt at the
Python interpreter) doesn't know what to do, it passes it on until the program eventually stops ungraciously
or \crashes."
You can also deliberately raise an Exception of some variety that has already been dened in Python. Try
out:
>>> raise ValueError
>>> # or...
>>> raise ValueError("that value is wrong in so many ways!")
What is the advantage of dening and raising Exceptions, rather than just writing code to deal with
exceptional circumstances in the same part of the program where they might occur? A problem with
that approach is that exceptional circumstances may occur in many places (dividing by zero might occur
almost anywhere), and it would take a lot of repetitive code to deal with them in every piece of potentially
13
problematic code. Python uses a pair of try... except blocks to detect and deal with those Exceptions that the
programmer knows how to deal with. We might deal with the possibility of an empty IntStack by letting the
user know what's up:
try:
n = i.pop()
except PopEmptyStackException:
print("You cannot get an integer from an empty stack!")
In this case, rather than crash, the program prints some information and continues. The try block can
contain multiple lines of code, and in our example, any code that raised a PopEmptyStackException | even
indirectly | would be dealt with.
Exceptions are classes, all descended from class Exception, even if they have an empty body:
class PopEmptyStackException(Exception):
"""Raised when pop from empty stack attempted"""
pass
This means that a PopEmptyStackException is also an Exception, so any try... except blocks that deal with
vanilla Exceptions will also deal with PopEmptyStackExceptions the same way. You can even design a hierarchy
of Exception subclasses and sub-subclasses. Then your try... except blocks can be specialized to rst try to
deal with the most specic Exceptions, then more general Exceptions, and then deal with all the Exceptions
that weren't covered by any other block. Here's an example:
class SpecialException(Exception):
pass
class ExtremeException(SpecialException):
pass
if __name__ == __main__:
try:
#1/0
#raise SpecialException(I am a SpecialException)
#raise Exception(I am an Exception)
raise ExtremeException(I am an ExtremeException)
1/0
# use name se if a SpecialException detected
except SpecialException as se:
print(se)
print(caught as SpecialException)
# use name ee if ExtremeException detected
except ExtremeException as ee:
print(ee)
print(caught as ExtremeException)
# name all other Exceptions e
except Exception as e:
print(e)
print(caught as Exception)
14
You should copy and experiment with this code. Try removing and inserting the # character on various
lines in the try block. Explain the result. Is there any expression you can put in the try block that will cause
an ExtremeException to be detected in this code? Why or why not?
15
16
Chapter 2
maintenance
2.1 collaborate
2.2 break problems down
2.3 re-use and re-implement
2.4 document
2.5 test
2.6 refactor
2.7 debug
17
18
Chapter 3
recursion
Recursion is a useful and beautiful problem-solving technique. Every time we can break a problem into
smaller problems with the same structure, solve them, and combine the solution into a solution to the
original problem, we have used recursion.
Most programming languages provide some support for recursion.
(3.1)
PI = 3.141592653589793...
. . . or, better yet, use the name of a constant that developers of standard modules have already created:
import math
math.pi
Similarly, if you were implementing a function that required the length of a list, you would either name
and implement a helper function that performed this task, or (even better) use the built-in len:
def square_list(L):
(list) -> list
Produce a new list with Ls elements
repeated len(L) times.
return L * len(L)
This is the insight that recursion continues. If you are in the middle of implementing a function that
adds all the numbers in an arbitrarily-nested list of numbers, and you nd that you need to add all the
numbers in an arbitrarily-nested sublist of numbers, you don't re-invent the wheel (or the function).
You simply use the name of the function that you are in the middle of implementing to direct program
execution to the solution to the sub-problem (or sublist, in this case):
19
def sum_list(L):
(list or int) -> int
Return L if its an int, or sum of the numbers in possibly nested list L
>>> sum_list(17)
17
>>> sum_list([1, 2, 3])
6
>>> sum_list([1, [2, 3, 4], 5])
15
The above implementation names built-in functions isinstance and sum as helper functions. It also uses
sum list | the function that is being implemented! In the next section we trace how this self-reference can
yield the solution we need.
Function sum list will sum numeric elements of lists no matter how deeply and complexly nested the lists
are. We say that a simple list such as [1, 2, 3] has depth 1. In general, we say that a list has depth 1 more
than the depth of its most deeply-nested list element (non-lists have depth 0). So:
[1, [2, 3, 4], 5] has depth 2;
[6, [1, [2, 3, 4], 5], 7, [1, [2, 3, 4], 5]] has depth 3;
. . . and so on.
It is very important in the example above that we didn't trace sum_list([2, 3, 4]) any further. We simply
replaced it by the value we knew it must have, since we had already seen that with a list of depth 1 sum list
returns the sum of the elements. We don't re-trace recursive calls we understand, since the resulting explosion
of recursive calls would obscure our understanding of recursion.
20
21
22
Chapter 4
efficiency
4.1 searching
4.2 sorting
4.3 definitions
4.4 classification by big-Oh
23