Thanks to visit codestin.com
Credit goes to www.scribd.com

0% found this document useful (0 votes)
84 views29 pages

148 Notes

This document outlines key concepts in computer science abstraction. It discusses how abstraction involves focusing on meaningful representations of data rather than low-level implementation details. Abstract data types (ADTs) specify intended meaning and operations of data without specifying how it is stored and manipulated. The document provides examples of objects, ADTs like stacks, and how interfaces expose ADT functionality to users while hiding implementation details.

Uploaded by

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

148 Notes

This document outlines key concepts in computer science abstraction. It discusses how abstraction involves focusing on meaningful representations of data rather than low-level implementation details. Abstract data types (ADTs) specify intended meaning and operations of data without specifying how it is stored and manipulated. The document provides examples of objects, ADTs like stacks, and how interfaces expose ADT functionality to users while hiding implementation details.

Uploaded by

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

CSC148

Introduction to Computer Science

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

.
.
.
.
.
.
.
.
.
.
.
.

names reduce repetition


follow recursion . . . . .
design recursion . . . . .
use recursive structures
3.4.1 lists of lists . . .
3.4.2 trees . . . . . . .
3.4.3 linked lists . . .

19

efficiency

23

4.1 searching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.2 sorting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.3 de nitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
i

4.4 classi cation by big-Oh . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

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 scienti c
rigour: we try to understand causes and e ects, 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 bene t 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

1.2 abstract data types (ADTs)


Objects, with their information and behaviour, can represent entities that we wish to store and manipulate
to solve problems, usually on a computer. It is often useful to focus on the semantics (the meaning of the
real-world entity being represented) rather than the details of how this is implemented. There are at least
two reasons this is an advantage:
1. We can think about algorithms, or recipes, for solving problems more freely if we don't have to include
all the details of how our objects are implemented.
2. Details of how objects, and their components, are stored and accessed vary between programming
languages, whereas a really good algorithm can be translated into any programming language.
These are the motivations for Abstract Data Types (ADTs). An ADT speci es the intended meaning of
the data it stores, and the operations it provides on that data. It does not talk about the how to store and
manipulate the data in a particular programming language.
Stacks provide an example of an ADT that brings out some of the general features of ADTs. Stacks can
be implemented in all computer languages, and in the real world | you may have several on your shelves
or desk. A stack
 stores items
 provides an operation to remove (pop) the top item
 provides an operation to add (push) a new item onto the top of the stack
 provides a way to tell whether the stack is empty (is empty)

1.2.1 hide details


Notice that the itemized list in the previous section said nothing about the concrete details of how a stack is
implemented. The items could be dishes on your shelf, held together by gravity and friction, the push and
pop methods being implemented by your body, and the is empty method being implemented by your eyes
and visual cortex.
Or a stack could be implemented in Python as a list, where push and pop add items to the end of the list,
and is empty uses some Python code to check the length of the list and see whether it is 0.
In a stack ADT, we don't consider these grubby details, because we're more interested in what sort of
things we can do with a stack. There are many algorithms that use stacks, and we don't want to obscure
the deep thinking about algorithms by worrying about the low-level implementation.
Here's a simple problem that you can probably solve with a stack. Given a sequence of items, produce
the reverse of that sequence, using a stack and its operations push, pop, and is empty (probably more than
once).
2

1.2.2 expose interfaces


When you implement an ADT in your favourite programming language, you will want it to be available for
others (and yourself!) to use. You'll need to tell the users (clients) of your code what it does. This means
that your documentation will specify an interface that tells clients what operations your ADT is capable of.
Here's an example of a python docstring for a stack:
"""
Stack ADT.
Operations:
pop: remove and return top item, provided
stack is non-empty
push(item): store item on top of stack
is_empty: return whether stack is empty.
"""

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.2.3 bundle information and behavior


Notice that the stack ADT combines information (the stored data) with operations (push, pop, and is empty).
You could certainly get the same results by having a separate structure (e.g. a list) and some functions
that use it. However, this burdens your clients with the task of keeping track of the separate pieces |
information and behaviour | in their own code.
An important feature of ADTs is that they combine information (implemented as attributes in Python,
see below) and behaviour (implemented as methods in Python, also see below).

1.3 concretize ADTs in Python


An ADT doesn't specify how you implement it in a particular programming language. In Python, types of
similar data are speci ed by classes | you've used classes such as str or list.
Eventually you'll want a class that isn't already implemented. A class declaration creates a new class for
creating objects of that type.

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)
...

Now, if p is a point, then initialize(p, 3, 5) will give it x-coordinate 3 and y-coordinate 5.


The distance from the origin of point (x, y) is given by the formula x2 + y 2 . Of course, there is a
sqrt function in the module math, but you can do the same thing by raising to the power 1=2. Here's an
implementation of a function that returns the distance of a point from the origin (assuming the point has
attributes x and y):
>>> def distance_from_origin(point):
...
return (point.x**2 + point.y**2)**(1/2)
...

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 di erent 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 di erent 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 de ne 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 de ne 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 de ned when we have
a de nition 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 de nition 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 de ne 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 de ne 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-de ned 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 de ned 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 de nition 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"

(isinstance(other, Point) and


self.x == other.x and self.y == other.y)

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
di erent 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.

1.3.3 methods versus module-level functions


Class methods are very useful, but this isn't the only sort of function you should be ready to de ne. You
already know how to de ne functions that are not inside any class, but simply inside a module | the def
keyword sits at the left margin of the *.py le that de nes a module.
How do you decide when to de ne a module-level function, and when to de ne a method inside a class?
As yourself whether the function is closely associated with a class: does it change the class attributes or
report on the state of the attributes in the class? If so, it is a good candidate for a method. On the other
hand, if the function is less closely associated with a class, and quite reasonably takes in all the information
it needs through its list of arguments, then it is a good candidate for a module-level function.
Another decision to make is whether your function should return some non-NoneType value (for example
str or int), or is it called in order to change some object or produce some input/output e ect (for example,
some print statement). Explicit non-NoneType return values are particularly straightforward to test, and you
should consider this approach. In any case, think about these issues when you write your type signature.
Some beginners mess up by expecting a function to return some non-NoneType value, but leaving one or
more branch for NoneType to sneak through. An example of how this could happen is to have an if... without
a matching else. One way to guard against this is to make the last statement in the function an Exception
something like:
raise Exception(Should not inadvertently return None!)

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
de ned, 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 e ect, 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 di erent values for di erent 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

Initialize new Point self with horizontal coordinate x,


vertical coordinate y, and depth component z.
"""
self.x, self.y, self.f = float(x), float(y), float(z)

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 di erent 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 de ned 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))

This approach is called extending the parent class.


HeavyPoint. add needs to produce a new HeavyPoint using the sums of all three parts: the x, y and m
part. I don't see an easy way to re-cycle the existing code, so I override it | create a completely new method
with the same name:
def __add__(self, other):
"""(HeavyPoint, HeavyPoint) -> HeavyPoint
Return component-by-component sum of self and other.
>>> hp1 = HeavyPoint(1.0, 2.0, 3.0)
>>> hp2 = HeavyPoint(4.0, 5.0, 6.0)
>>> hp1 + hp2
HeavyPoint(5.0, 7.0, 9.0)
"""
return HeavyPoint(self.x + other.x, self.y + other.y,
self.m + other.m)

Some of the methods from Point work without any modi cation 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 de ned in a class, its
superclass (or the superclass of the superclass. . . )? It checks rst in the most speci c class that has been
declared. If it doesn't nd the method de ned 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 de ned in HeavyPoint and ignores the one de ned
in Point. We have a mixture of approaches for HeavyPoint. eq , since Python nds a de nition in HeavyPoint,
but this de nition includes a reference to Point. eq . This means it uses the de nition from HeavyPoint,
which in turn uses the de nition 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

def f(self, n):


return self.g(n)
class B(A):
def g(self, n):
return 2*n
if __name__ == __main__:
a = A()
b = B()
print("a.f(1): {}".format(a.f(1)))
print("b.f(1): {}".format(b.f(1)))

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 de ning 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 de ned in Python. Try
out:
>>> raise ValueError
>>> # or...
>>> raise ValueError("that value is wrong in so many ways!")

What is the advantage of de ning 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 speci c 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 names reduce repetition


As a programmer, you would never dream of typing and re-typing the literal value of an approximation of
the ratio between the circumference and diameter of a circle,  :
3:141592653589793:::
.

(3.1)

We would either name a constant:

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

# reuse: isinstance, sum, sum_list !


if isinstance(L, list):
return sum([sum_list(x) for x in L])
else: # L is an int
return L

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.

3.2 follow recursion


To understand the workings of a recursive function such as sum list, you should trace examples that work up
from simple to more complex. Each time you see a recursive function call that is equivalent to something
you have already traced stop tracing and ll in the value directly. Here are examples:
sum_list(17) -> 17
sum_list([1, 2, 3]) -> sum([sum_list(1), sum_list(2), sum_list(3)])
-> sum([1, 2, 3]) -> 6
sum_list([1, [2, 3, 4], 5])
-> sum([sum_list(1), sum_list([2, 3, 4]), sum_list(5)])
-> sum([1, 9, 5]) -> 15

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

3.3 design recursion


3.4 use recursive structures
3.4.1 lists of lists
3.4.2 trees
general trees
binary trees
binary search trees

3.4.3 linked lists

21

22

Chapter 4

efficiency
4.1 searching
4.2 sorting
4.3 definitions
4.4 classification by big-Oh

23

You might also like