cmpt141 Readings
cmpt141 Readings
to
Computer Science
Mark Eramian
CS . USASK . CA
LaTeX style files used under the Creative Commons Attribution-NonCommercial 3.0 Unported Li-
cense, Mathias Legrand ([email protected]) downloaded from www.LaTeXTemplates.
com.
Cover and chapter heading images are in the public domain downloaded from http://wallpaperspal.
com.
This document is licensed under the Creative Commons Attribution-NonCommercial 3.0 Unported
License (the “License”). You may not use this file except in compliance with the License. You
may obtain a copy of the License at http://creativecommons.org/licenses/by-nc/3.0. Un-
less required by applicable law or agreed to in writing, software distributed under the License is
distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the specific language governing permissions
and limitations under the License.
The author would like to thank Brittany Chan, Michael Horsch, and Jeff Long for their invaluable
contributions to this work including advice on content, organization, and help with proofreading.
We also thank the following individuals for discovering errors or otherwise contributing to the
text: Chad Mckellar, Babfunmise Adebowale
Contents
3 Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.1 Functions and Abstraction 33
3.2 Calling Functions 34
3.2.1 Functions as Expressions: Obtaining/Using a Function’s Return Value . . . . . . . . . 34
3.2.2 Calling Functions with No Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.2.3 Functions That Do Not Return a Value: Procedures . . . . . . . . . . . . . . . . . . . . . . . 35
3.2.4 More Built-In Python Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4 Creating Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.1 Defining Functions and Parameters: The def Statement 39
4.1.1 Functions that Perform Simple Subtasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.1.2 Functions that Accept Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.1.3 Returning A Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.1.4 Returning Nothing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.1.5 Defining Before Calling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.1.6 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.2 Variable Scope 43
4.3 Console I/O vs Function I/O 45
4.4 Documenting Function Behaviour 45
4.5 Generalization 46
4.6 Cohesion 47
5 Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.1 Objects and Encapsulation 49
5.1.1 Calling Methods in Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.1.2 Mutable vs Immutable Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5.1.3 Defining Our Own Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
6 Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.1 Modules: What Are They and Why Do We Need Them? 53
6.2 How to Use Modules 53
6.3 What Other Modules Are There? 54
6.4 Finding Module Documentation 56
7 Indexing and Slicing of Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
7.1 Sequences 57
7.2 Indexing 57
7.2.1 Offsets from the End . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
7.2.2 Invalid Offsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
7.3 Slicing 59
7.3.1 Slicing with a Non-Unit Step Size . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
7.3.2 Slicing with Invalid Offsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
8 Control Flow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8.1 Relational Operators and Boolean Expressions 61
8.2 Logical Operators 62
8.2.1 The and Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
8.2.2 The or Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.2.3 The not Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.2.4 Mixing Logical Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.2.5 Variables in Relational and Logical Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . 64
8.3 Branching and Conditional Statements 64
11 Dictionaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
11.1 Dictionaries 85
11.1.1 Creating a Dictionary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
11.1.2 Looking Up Values by Key . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
11.1.3 Adding and Modifying Key-Value Pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
11.1.4 Removing Key-Value Pairs from a Dictionary . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
11.1.5 Checking if a Dictionary has a Key . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
11.1.6 Iterating over a Dictionary’s Keys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
11.1.7 Obtaining all of the Keys or Values of a Dictionary . . . . . . . . . . . . . . . . . . . . . . . 88
11.1.8 Dictionaries vs. Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
11.1.9 Common Uses of Dictionaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
11.2 Combining Lists, Tuples, Dictionaries 91
12 File I/O . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
12.1 Data File Formats 93
12.1.1 Common Text File Formats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
12.2 File Objects in Python – Open and Closing Files 95
12.3 Reading Text Files 96
12.3.1 Reading List Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
12.3.2 Reading Tabular Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
12.4 Writing Text Files 99
12.4.1 The write() method. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
12.4.2 Writing List Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
12.4.3 Writing Tabular Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
12.5 Pathnames 101
13 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
13.1 Arrays 103
13.1.1 Arrays vs. Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
13.2 Arrays in Python – The numpy Module 105
13.3 Programming with numpy Arrays 105
13.3.1 Creating Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
13.3.2 Important Array Attributes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
13.3.3 Indexing and Slicing Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
13.3.4 Arithmetic with Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
13.3.5 Relational Operators with Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
13.3.6 Iterating Over Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
13.3.7 Logical Indexing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
13.3.8 Copying Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
13.3.9 Passing Arrays to Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
Programming in Python
Algorithms
Blocks, Repetition, and Conditionals
Variables
Input and Output
Methods of Writing Algorithms
Abstraction and Refinement
Problems vs. Algorithms
Learning Objectives
1.1 Algorithms
An algorithm is a list of actions that describe how to perform a task or solve a problem. A recipe for
making bread is an algorithm. The recipe describes what actions you must take, and the order in
which you must take them, if you want to end up with something that looks and tastes like bread. If
you deviate from the algorithm, there’s a good chance you end up with something quite un-bread-like.
Other examples of algorithms are:
• instructions for assembling a bookshelf;
• steps to operate a coffee maker; and
• a list of things to do in case of a fire.
14 Algorithms and Computer Programs
Here is a concrete example showing the specific steps in an algorithm to make ramen noodles:
Algorithm MakeRamen :
boil water
add noodles to water
wait 6 -8 minutes
drain the noodles
stir in contents of flavour packet
place cooked noodles in bowl
This algorithm consists of six actions to solve the problem of making ramen noodles. The actions are
taken in the order given, and the end result, or output of the algorithm is a prepared bowl of steaming
hot noodles, ready to eat. The important thing to remember about algorithms is that the given actions
must be taken in the given order, otherwise you are not following the algorithm and likely will not
get the desired output.
Indentation is almost always used to denote blocks.1 Thus, when reading algorithms, be aware
that the indentation is not arbitrary or accidental, but rather conveys important meaning.
1.1.2 Variables
Algorithms often deal with numbers and are easier to write if we are allowed to give names to
numbers. A variable is a name given to a particular numeric value.2 We can write algorithms
with variables that refer to values, change the values they refer to whenever we want, and use them
to compute other values. For example, here’s an algorithm for computing the average of a set of
numbers that uses a variable to keep track of a running sum:
Algorithm Average :
Input : a set of numbers
let total = 0;
for each number x in the set of numbers :
let total = total + x
let average = total / size of the set of numbers
In this algorithm the first action, let total = 0, means that we associate the variable name total
with the value 0. Then we have a single-action block which is repeated for each number x in the
input set. That action adds x to the existing value referred to by total and associates total with
the new resulting value. Then the output of the algorithm, the average, is associated with another
variable called average.
let total = 0
for each number x in the set {2 , 4 , 6 , 8 , 10}:
let total = total + x
average = total / 5
This is exactly the same algorithm as the Average algorithm in the previous section except that it
does not have any inputs and it only works for one specific set of numbers which is written right into
the algorithm. By allowing the set of numbers to be an input to the algorithm, we get an algorithm
that is reusable in vastly more situations and can compute the average of any set of numbers instead
of just one specific set.
1 There are other ways to denote blocks in algorithms besides indentation such as enclosing a block in a pair of curly
braces { ... }. But even then, indentation is almost always used as well to make algorithms more readable to humans.
2 Later we’ll learn that variables can refer to values that are things other than numbers.
16 Algorithms and Computer Programs
Algorithms have one or more outputs. An output is data that is the result of the task that the
algorithm was intended to carry out. In the Average algorithm from the previous section we didn’t
explicitly specify that the variable average is the algorithm’s output, but we could, for example:
Algorithm Average :
Input : a set of numbers
Output : a variable ’ average ’ which refers to the average
of the numbers in the input set
let total = 0;
for each number x in the set of numbers :
let total = total + x
let average = total / size of the set of numbers
Now the output of the algorithm is explicitly specified. If an algorithm has more than one input or
output, then the additional inputs and outputs can be described in the same fashion.
Notice that for every algorithm that we have shown you so far, we have given it a name, and
sometimes specified its input and output, and these things have appeared before the algorithm’s
first action. The description of an algorithm’s name, inputs, and outputs, are collectively called the
algorithm’s header. The items in the header are not actions in the algorithm to be carried out, but
rather describe the algorithm and its intended usage.
Figure 1.1: Examples of algorithms written using pictures. Left: LEGO instructions; right: aircraft
safety procedures card.
required by the programming language. Moreover, they are ready to implement the algorithm in any
programming language that they know!
In this course we will be using the Python programming language. Let’s look at what the
Average algorithm looks like when it is translated from pseudocode to Python. Don’t worry if you
don’t understand why the algorithm is written the way it is in Python. We’ll get to that later.
Algorithm Average :
Input : a set of numbers
let total = 0;
for each number x in the set of numbers :
let total = total + x
let average = total / size of the set of numbers
Listing 1.1: The Average algorithm in pseudocode.
def Average ( S ):
total = 0
for x in S :
total = total + x
average = total / len ( S )
return average
Listing 1.2: The Average algorithm in Python.
You should be able to appreciate that the two versions of the algorithm are doing the same thing.
The header in the pseudocode algorithm has been translated to the line starting with def in Python,
so if you’re thinking that this is some Python syntax for saying that we want to define an algorithm,
18 Algorithms and Computer Programs
give it a name, and say what its inputs are, then you’re right. The S in the round brackets indicates
that S is the input to the algorithm. You should also be able to appreciate that total and average
are variables in the Python version, just as they are in the pseudocode version. The repetition for x
in S looks much the same as in the pseudocode and indicates that we do the same thing to each
element of the set S. The len(S) syntax tells us how many numbers are in the set S. The blue
words in the Python code have specific, well-defined meanings in Python. The last line contains the
command return which tells Python that the variable average is the output of the algorithm.
The Python code can be understood and carried out by a computer, but the pseudocode algorithm
cannot, even though it doesn’t look that different. Depending on how a pseudocode algorithm is
written, there may or may not be an easy, line-by-line translation of the pseudocode algorithm into
Python (though in this case, it’s pretty close).
boil water
add noodles to water
wait 6 -8 minutes
drain the noodles
stir in contents of flavour packet
place cooked noodles in bowl
The first instruction in this pseudocode is boil water. This is a great example of abstraction,
because the action boil water glosses over all of the details of how to boil water. For humans,
these details are pretty unimportant, because adult humans all know how to boil water. But imagine
that this algorithm has to be carried out by a humanoid cooking robot. The robot does not intuitively
know how to boil water. The action boil water is too abstract for it. It needs more details.
The process of describing more detail about how an instruction should be carried out is called
refinement. For example, we might refine the boil water action by replacing it with a sequence of
actions (shown in red text) that describe how to boil water in more detail:
Algorithm MakeRamen :
Each of these new actions describes an action that partially carries out the original boil water
action. But even these actions may not be detailed enough for our robot to carry out the task. At
some point, the robot needs to know exactly where, and for how long to position its legs and arms
to carry out these actions. This would require that we further refine the actions place pot under
faucet, add water to pot, etc. to the level of detail where we tell the robot exactly where and
how to move its limbs by replacing each of these actions with sequences of even more detailed
actions. This is called stepwise refinement. We repeatedly replace actions that are too abstract with a
sequence of less abstract, more detailed actions until we reach a level of detail that can be directly
carried out. Each level of refinement results in actions that are at a lower level of abstraction and are
closer to the individual actions that the robot (or computer) can carry out natively.
The ability to think at different levels of abstraction and mentally move between them is critical
to success as a programmer and a computer scientist. We abstract away details when they are not
important, and refine abstractions later when we are ready for the detail. Defining an algorithm and
giving it a name is, itself, a form of abstraction. Naming an algorithm and describing its inputs and
outputs allows someone to use the algorithm without knowing how the algorithm works. In other
words, an algorithm, once written, hides the details of how the algorithm is performed, allowing it
to be used to produce results without knowledge of the algorithm’s details. For example, having
defined the algorithm Average, we no longer have to remember that to compute an average, you add
all the numbers up and divide by how many numbers there were. All we have to do is say “perform
the algorithm Average on the set of numbers 1, 2, 3, 4, and 5”, and we’ll get the correct answer of 3.
We take abstraction for granted all the time. So many of the things we do on computers that look
really simple are actually abstractions of breathtakingly complex algorithms and hardware details.
These are things like Google Search, fingerprint ID on your phone, and face recognition in your
digital photography software. There is a tendency for people who are used to such technology to
underestimate its complexity. Keep this in mind the next time you think to yourself that it would be
really “easy” to add some desired feature to your phone or a piece of software.
To summarize, abstraction allows us to think about performing higher-level, more complex
actions without worrying about how they are performed. Abstraction doesn’t mean that the lower-
level details of how an abstracted algorithm is carried out don’t exist or never have to be written at
some point. It is just a mechanism that allows us to ignore such details when it is convenient or until
they are needed.
putting a hand of cards in order. Both achieve the same result, the but algorithms themselves are
fundamentally different processes.
Data
Atomic Data
Compound Data
Data Types
Expressions
Literals
Variables
Variable Names
Variable Assignment
Variables as Expressions
Operators
Console Input and Output
Outputting Text to the Screen
Reading Strings from the Keyboard
Reading Numbers from the Keyboard
Comments
Learning Objectives
We’re going to cover a lot of material fairly quickly in this chapter. But remember that CMPT
141 is is intended for students who have done some programming before. If you’ve programmed
before, even if you didn’t program in Python, then most of the concepts in this chapter should be at
least a little bit familiar.
2.1 Data
Data is information. Computer programs need data to do anything useful. All input and output is
data. Data can take many forms (numbers, text, pictures, etc.), but ultimately, at a low enough level
of abstraction, all data is numbers because that is what computers know how to store. It is abstraction
22 Data, Expressions, Variables, and I/O
that makes it appear that we can store things more interesting than numbers, such as images, video,
text, web pages, etc. These things are all just large collections of numbers interpreted in different
ways — the different interpretations are abstractions! At an even lower level of abstraction, all data
is just sequences of 0’s and 1’s, because computer hardware stores data as binary numbers using
different electric voltages to represent the binary digits 0 and 1. Fortunately, computer programmers
don’t have to work at such a low level of abstraction. In the rest of this section we’ll look at the kinds
of data we, as programmers, can use.
Boolean: Boolean data can only be one of two values: True or False. Note that capitalization
matters – true and false are not valid boolean values in Python, but True and False are.
2.2 Expressions
Expressions in a programming language are combinations of data and special symbols called opera-
tors, which have specific meaning in the programming language. Operators perform computations on
one or more pieces of data to produce a new piece of data. When all of the computations associated
with operators in an expression have been carried out, the result is a new piece of data whose value
is the result of the expression. In Python, every valid expression describes some computation that
results in a value which we call the value of the expression.
2.2.1 Literals
A literal is a number or string written right into the program, that is, literally typed right into the
program’s code, such as 42. Literals are one of the fundamental components of expressions. If we
are to write more complex expressions, we first need to learn about literals.
Literals are one of the simplest forms of expressions. The value of an expression containing
a single literal is the value of the literal itself. We can see this right away by running Python in
interactive mode. If we start up Python from the terminal, and type in the number 42, Python
responds by telling us that the value of the expression 42 is 42.
iroh : CMPT141 mark$ python
Python 3.5.1 | Anaconda 2.4.1 ( x86_64 )| ( default , Dec 7 2015 , 11:24:55)
[ GCC 4.2.1 ( Apple Inc . build 5577)] on darwin
Type " help " , " copyright " , " credits " or " license " for more information .
>>> 42
42
>>>
Literals in Python have a data type that is inferred by the manner in which the literal is written.
Integer literals: Any number written without a decimal point is an integer literal. Thus, 42, -17,
and 65535 are integer literals.
Floating-point literals: Any number written with a decimal point is a floating-point literal. Exam-
ples are: 42.0, -9.8, and 3.14159. Note with care that even the literal 42. (decimal point
included) is a floating-point literal because it contains a decimal point. An empty sequence
of digits after the decimal point is different from no decimal point at all! We can see the
24 Data, Expressions, Variables, and I/O
difference in Python; note how when we enter 42., Python responds with the value 42.0, a
floating-point value:
>>> 42.
42.0
>>>
Floating-point literals can also be written in scientific notation. For example, the speed of
light is 3 × 108 m/s, a quantity which can be written as the literal 3e8. See Python’s response
when we enter the expression 3e8:
>>> 3 e8
300000000.0
>>>
Literals written in scientific notation are always floating point, never integers. Here are some
more examples of floating-point literals:
• 6.022e23 (6.022 × 1023 )
• 9.11e-31 (9.11 × 10−31 )
• 1e+3 (1000)
String literals: Recall that in the previous section we said that strings are sequences of characters.
A string literal is specified by enclosing a sequence of characters with a pair of single or double
quotes. "Hello world." and ’The night is dark and full of terrors.’ are both
examples of string literals. Strings can contain spaces because spaces are characters too. Any
symbol that appears on the keyboard is a character.1 Note that there is a difference between the
literals ’7’ and 7; the former is a string literal and does not actually have the numeric value
of 7, while the latter is an integer literal, which does. Similarly the literals "3.14159" and
3.14159 are different. The former is a string literal, which does not actually have the numeric
value 3.14159, and the latter is a floating-point literal, which does. However, ’Bazinga!’
and "Bazinga!" are exactly the same, as shown if we enter them in Python:
>>> " Bazinga ! "
’ Bazinga ! ’
>>> ’ Bazinga ! ’
’ Bazinga ! ’
>>>
So why have two ways of writing string literals? It is so that we can conveniently include single
or double quotes as part of a string. The string ’The card says "Moops".’ is enclosed in
single quotes and contains two double quotes as part of the string. The single quotes are not
part of the string, but the double quotes are!
>>> ’ The card says " Moops ". ’
’ The card says " Moops ". ’
>>>
1 Other, stranger things can be characters too, but we’ll avoid that discussion for now.
2.3 Variables 25
But look what happens when we try to write the same string literal in Python instead with
double quotes enclosing the whole string:
>>> " The card says " Moops " . "
File " < stdin > " , line 1
" The card says " Moops " . "
^
SyntaxError : invalid syntax
>>>
Oh my, Python sure didn’t like that. The reason this results in an error is because Python
interprets the characters between the first two double quotes (the first one and the one right
before the M) as a string literal. Then the word Moops makes no sense to Python because
Python thinks it’s not part of a string literal, so Python tries to interpret it as part of the Python
language, which it isn’t, so Python doesn’t know what to do and gives up. Thus, you can write
single quotes inside string literals enclosed in double quotes, and double quotes inside string
literals enclosed in single quotes.
So should you use single or double quotes for strings? Well, there’s no right or wrong answer
to this question. Unless you need single or double quotes within a string literal, it doesn’t
matter. Normally, one chooses to use either single or double quotes as one’s "default" style,
and only uses the other when necessary.
In most other programming languages, there is only one way to write a string literal,
and single and double quotation marks have very different meanings. For example, in
C, C++, and Java, strings literals must be enclosed in double-quotes.
2.3 Variables
If we only had literal values, we couldn’t write very interesting or useful programs because the
program would use the same data, and produce exactly the same results every time it is run. Variables
are a way of giving names to data. Giving a name to data allows a program to operate on different
data values each time a problem is run. We can then ask Python to do something to the data with
a certain name. If we only had literals, we could only ask Python to do something with a specific
literal data value.
x = 5 # assign the name x to the integer 5.
y = 42.0 # assign the name y to the floating - point number 42.0
# assign the name error_message to the string : " That didn ’t work !"
error_message = " That didn ’t work ! "
It may seem strange to think of the name being assigned to the value. Indeed, in most other
programming languages we tend to think of assigning values to variables. But in Python, it is safer,
and more reflective of how Python actually works, to think of assigning variable names to values. In
Python, an assignment statement causes the variable to refer to its value. If you re-assign a variable
to a new value, using another assignment statement, Python changes the variable’s reference, not its
value. In Python, more than one variable can refer to a given value.
The data to which a variable refers always has a type, but you cannot tell from the variable name
what type of data it refers to. You can even change the type that a variable refers to:
x = 10; # x refers to the integer 10
x = 10.0; # now x refers to the floating - point value 10.0
There are ways to determine the type of data that a variable refers to, but we’ll leave that for a later
discussion. For now, just be aware that there is no way to guarantee that a variable always refers
to data of a specific type. You can write a program so that a variable is always supposed to be of a
certain type, but the type might change as a result of a bug, and there is no way to force Python to
notify you of this. This is a contrast to many other programming languages (e.g. C++, Java) where
variables must be defined to have a specific data type, and attempting to assign a value of a different
type to that variable will result in an error.
2.3.4 Operators
Operators can be used to write expressions that compute new values from existing pieces of data.
We say that the operator operates on these pieces of data. For example, the expression 2 + 3 has
the value 5.
>>> 2 + 3
5
>>>
In the above example the addition operator + computes the sum of 2 and 3. Python responds with the
value 5 because that is the value of the expression 2 + 3. The data items that an operator operates
on are call operands. Operands can be any expression. Most of the operators we will see are binary
operators because they require two operands.3 Operands need not be literals, they can be variables
too:
>>> x = 2
>>> y = 3
>>> x + y
5
>>>
Since x refers to the integer 2, and y refers to the integer 3, the value of the expression x + y is 5.
This is also a good time to note that a variable name cannot be used in an expression if it has not
been assigned to a value. For example:
>>> x = 2
>>> x + z
Traceback ( most recent call last ):
File " < stdin > " , line 1 , in < module >
NameError : name ’z ’ is not defined
In the above example, when we try to add together the value referred to by x and the value referred
to by z (which refers to no value because none was assigned), Python cannot perform the addition
operation, and issues a NameError which is its way of saying that the identifier z was never assigned
to a value.
Arithmetic Operators
The basic arithmetic operators in Python are summarized in the following table:
3 Herethe word “binary” only conveys that the operator requires two operands, as opposed to unary operators which
only require one operand. Do not confuse binary operands with binary numbers — the latter are entirely different.
28 Data, Expressions, Variables, and I/O
Now that we now all of these operators, we can use Python in interactive mode like a calculator!
>>> 2 + 3 * 5
17
>>> 2 ** 8 + 1
257
>>> 3.5 - 1
2.5
>>> 2 * 4 + 10 * 3
38
>>>
The usual order of operations applies. The operators higher in the above table are evaluated
before operations lower in the table. Multiplication, division, integer division, and modulo have the
same precedence and if more than one of these appears in the same expression, they are evaluated
from left to right. Addition and subtraction have the same precedence (but lower than the others)
and again, are evaluated from left to right. Thus, in the last expression above, 2*4 happens first,
followed by 10*3, then the values of these two expressions become the operands for the addition
which results in 38.
Notice that the data type of the answer depends on whether any of the operands were floating
point numbers. The first expression 2 + 3 * 5 resulted in an integer result because all of the literals
in the expression were integers, and none of the operators generated any floating-point results. But
the expression 3.5 - 1 resulted in a floating-point number. This is because the first operand was
floating point. If any operand is floating point, the result will be too because operators must operate
on operands of the same type. Much of the time, however, you can use operands of different types,
and the data types will be automatically converted by Python to a common type. This is called type
coercion. Coercion only takes place when operands of an operator are of different types, and only
when the different types are compatible. Python will try its best to coerce operands of different
types into a compatible type, but in some situations this isn’t possible. For example, you cannot use
addition with a string and an integer because a string cannot be coerced into an integer; trying to do
this will result in an error.
The division operator is an exception. The result of division is always floating-point:
>>> 12 / 2
6.0
>>> 12 / 8
1.5
>>> 12 // 8
1
>>> 12.0 // 8.0
1.0
>>>
In the first example, division of the integers 12 and 2 results in a floating point number even though
both operands are integer. But observe that integer division (and modulo) follow the usual rule where
the result is only floating point if one or both of its operands are.
30 Data, Expressions, Variables, and I/O
Division in Python 3 behaves differently from division in most other languages, including
Python 2. In languages like Python 2, C++, and Java, division follows the same rules as
the other operators such that the result of division is only floating-point if at least one of its
operands are. Remember that the situation is different in Python 3!
To conclude this section, we’ll observe that, as you might expect, you can override the normal
order of operations by enclosing things in parentheses:
>>> 2 * 4 + 10 * 3
38
>>> 2 * (4 + 10) * 3
84
>>>
The parenthesis have higher precedence than any of the operators. Thus, the addition occurs first,
then the multiplications occur in left-to-right order. The 2 is multiplied with the value of (4 + 10)
giving us 28, then this multiplied by 3, resulting in 84.
Operators on Strings
Some operators can be applied to string operands, but their meanings are different. The “addition”
of two strings results in their concatenation. The “multiplication” of a string and a number n
concatenates the string with itself n times. Here are some examples:
>>> ’ Winter ’ + ’ is ’ + ’ coming ! ’
’ Winteriscoming ! ’
>>> ’ Na ’ * 8 + ’ BATMAN ! ’
’ NaNaNaNaNaNaNaNa BATMAN ! ’
>>>
The print syntax is different in Python 3 compared to Python 2. In Python 2 the parentheses
are optional because print was a statement, not a function. In Python 3, they are needed
because print is a function in Python 3.
This line will pause the program, and wait for the user to type something and press the Enter key.
Whatever the user typed will be stored as a string associated with the variable x. You can optionally
ask for Python to print out a prompt to the user by providing a string inside of the parentheses. Here’s
an example:
x = input ( ’ Please enter your name : ’)
print ( ’ Hello , ’ , x )
Here’s what happens when we run this:
Please enter your name : Mark
Hello , Mark
The line x = input(’Please enter your name: ’) first prints the string provided, then it
waits for the user to type text and press enter. The bright red text was typed by the user, and this text
is given the variable name x. The program responds by printing out a greeting using the text that was
entered.
2.5 Comments
After reading the Python code examples in this chapter, you may have noticed that we have often
included lines that begin with the # symbol, followed by some plain english text. The # symbol
denotes a single line comment in Python. Syntactically, the # symbol must be the first character
on a line, and it tells Python to simply ignore the content of the entire line. Python will not try to
interpret whatever follows the # as Python code, so you can write whatever you want. The purpose
of comments is to document your code, making it easier for others to understand it and to help you
remember what you were thinking when you come back to it yourself later.
Functions and Abstraction
Calling Functions
Functions as Expressions: Obtaining/Using
a Function’s Return Value
Calling Functions with No Arguments
Functions That Do Not Return a Value:
Procedures
More Built-In Python Functions
3 — Functions
Learning Objectives
In Chapter 1 we talked about giving names to algorithms, as well as algorithms receiving data
as input, and generating new data as output. In Python, such named algorithms are implemented
as functions. Functions allow us to give a name to a block of Python code. If an algorithm is
implemented as a function in Python, we can run the algorithm by using the function’s name in a
Python program. This is called calling the function.
Section 3.1 of this chapter introduces functions as a method of abstraction, and their relationship
to algorithms (Chapter 1). Later in Section 3.2 we will introduce the Python syntax for calling
functions, as well as some of the built-in functions available in Python.
Python prints out the value of the expression. So here’s what happens when we enter the expressions
in the listing from the previous section:
>>> S = " No , I am your father ! "
>>> len ( S )
21
>>> len ( " No . No , that ’s not true . That ’s impossible ! " )
45
>>>
Python prints out the return values of the function calls because the function calls are expressions
whose value is the return value of the function call. Since the value of a function call is its return
value, you can use a function call wherever we can use an expression! Thus, function calls can be
used...
• as operands of operators:
>>> len ( S ) + len ( " No . No , that ’s not true . That ’s impossible ! " )
66
• as values in assignment statements (give a name to the return value of a function):
>>> L = len ( S )
>>> print ( L )
21
The return value of len is 21, which gets assigned the name L. Since L now refers to the value
21, the print function call outputs 21 to the console.
• as arguments to other functions:
>>> print ( len ( S ) , len ( ’ Search your feelings ! ’ ))
21 21
>>>
The return values of the len function are the arguments to the print function. The two calls
len happen first, and their return values are used as arguments to print. Some people call
this a nested function call, because a call to one function (len) is being made as part of a call
to another function (print). When nested function calls are used, the calls are made in order
from inner-most to outer-most.
min ≥2 Works like max but returns the minimum value of all arguments. Accepts
any number of arguments.
pow 2 Calling pow(x,y) returns the value of x raised to the power of y.
int 1 Converts the argument to the integer data type (if possible) and returns
the result. Strings can be converted if they contain only digits 0–9 and
possibly a decimal point.
float 1 Similar to int; converts the argument to the float-point data type (if
possible) and returns the result.
str 1 Similar to int; converts the argument to the string data type and returns
the result.
input 1 (optional) This function may optionally be given a string argument. If given, the
argument is printed as a prompt, then the function waits for the user to
enter a string and press the enter key. The function returns the text entered.
Yes, this is the same function we used for console input in Section 2.4.2!
4 — Creating Functions
Learning Objectives
• compose functions in Python that perform a subtask and return the result;
• compose functions in Python that accept arguments as input;
• describe the role of a function’s parameters;
• distinguish between arguments and parameters;
• explain the role and behaviour of the return statement;
• differentiate between function input/output and console input/output;
• author appropriately descriptive comments to document a function;
• define the concept of generalization;
• define the concept of cohesion and explain why it is desirable for functions to have
high cohesion; and
• show by example how functions with parameters can be used to achieve generalization.
The ability to create your own functions and create abstractions of your own algorithms is a
tremendously powerful feature in any programming language. The main reasons for writing your
own functions are abstraction and decomposition of large programs into manageable pieces. We
can give names to our algorithms and abstract away their details by writing them as functions. The
purpose of this section is to learn how to do this in Python.
X = False
functi onW it hMa ny Ar gum en ts (42.0 , ’ Good Morning ’ , 17 , X )
then the function call would assign the parameter name a to refer to the argument 42.0, the parameter
name b to refer to the argument ’Good Morning’, the parameter name c to refer to the argument
17, and the parameter name d to refer to the parameter X. When an argument is a variable, like X
in this example, the parameter name is assigned to refer to the value that the argument refers to, so
actually, the parameter d ends up referring to the value False.
42 Creating Functions
Parameters are variables that get their values from the arguments in a function call; Python does
the assignment of parameter to argument behind the scenes, but it is the normal kind of assignment.
A key point to understand is that a parameter always refers to a value that was created outside the
function. The parameter is simply the function’s name for it. The value might have other variables
referring to it as well, as in the above example: the value False has two variables referring to it,
namely X outside the function, and the parameter d for as long as the function is active. Another key
point is that if you assign a parameter to a new value, you are changing what the parameter refers to,
and you are not changing the old value.
We’ve seen how to use parameters to define a function that accepts inputs (arguments). We’ve also
seen built-in Python functions that return a value. So how do we have one of our own functions
return a value?
The answer is pretty simple: Write the keyword word return, followed by an expression. The
value of the expression becomes the return value for the function, and the value of the call that
invoked the function. For example, we could modify our introduction function to return the name
that the user entered, so that it can be used by the caller for future reference:
# defines the function only :
def introductions ( greeting ):
print ( greeting )
x = input ( ’ Please enter your name : ’)
print ( ’ Hello , ’ , x )
return x
In this example, the return statement at the end of introductions causes the value referred to by x
(the text the user entered) to be returned. The execution of the program then resumes immediately
after the function call to introductions, and since the name username was assigned to the return
value of the function call, it now refers to the text that the user entered. Once the function has
returned, and execution has resumed after the function call, the variable x no longer exists. Returning
a value is one way of getting data out of a function.
We will see later that functions may have more than one return statement1 . As soon as a
return statement is executed, regardless of where it appears in the function, execution of the function
immediately ceases (even if there are lines of code after it!), the value of the accompanying expression
is returned, and execution continues from the line immediately after the call that invoked the function
(or, in some cases, the line containing the function call continues executing, e.g. if the function call
was part of a variable assignment, the assignment occurs after the function call returns).
1 Someare of the opinion that functions with more than one return statement is bad style! Some believe otherwise. We
really aren’t too worried about it.
4.2 Variable Scope 43
4.1.6 Summary
If we want to write a function that has inputs, we need to give it parameters. Parameters are the
variable names that are used within the function to refer to the values of a function’s arguments;
they are given in the function’s definition. Arguments are the input values for the function; they are
provided when the function is called. A function can be instructed to return a value (i.e. produce
an output!) using the return keyword. The code for the function must be indented in a block.
Indentation has semantic meaning in Python and must be used properly and with care.
damage = 0
D = fireball_damage ()
print ( damage )
44 Creating Functions
In this example, you might expect the value 30 to be printed. In fact, the value 0 is printed. To see
why this is, you must realize that we have two different variables called damage in this program.
The damage variable defined in the function fireball_damage only exists within the function – we
say that it is a local variable. Likewise, the damage variable defined outside of the function only
exists outside of the function. It’s scope is said to be global. Thus, the fireball_damage function
is changing only what the local variable damage refers to, not what the damage variable defined
outside of the function refers to. Moreover, two different functions can use the same variable name,
but they are, in fact, completely different and unrelated variables!
To complicate matters further, global scope variables are accessible from within all functions
but they cannot be modified. Consider this example:
base_damage = 10 # this is defined at global scope
acid_blast_damage (25)
Unlike the earlier example with the damage variable, since the usage of base_damage within the
function does not appear on the left side of an assignment operator (=), it refers to the variable that
was defined at global scope that is defined outside of any function. However, if we added a line to
the acid_blast_damage function that read base_damage = 20 this would define a new variable
that was local to the acid_blast_damage function which is different from the global scope variable
of the same name. The base_damage variable at global scope would continue to refer to the value
10, and any subsequent references to base_damage in the function would refer to the local variable
with value 20.
If this seems confusing to you, it’s because it is! This is precisely why we tell you not to
try to use variables defined at global scope within functions. It is much safer and easier to
understand if, when you need data from global scope variables, to pass them into functions as
arguments!!! In other words, don’t use global scope variables within functions at all and you
don’t have to worry about this!
If you took CMPT 140
In CMPT 140 you probably encountered the Python keyword global which allows functions
to access variables defined outside of the function. This was necessary because of the
specialized Processing programming framework in which you were working. In general, we
normally avoid the use of global variables because they can cause unexpected side effects and
bugs when we inadvertently refer to the same variable when we don’t mean to. In this class
we do not allow the use of global variables. In almost all situations where global variables
seem appropriate there exists a better way.
4.3 Console I/O vs Function I/O 45
def introductions ( greeting ):
"""
Greet the user and asks them for their name .
Some types , such as ints , are able to use a more efficient algorithm when
invoked using the three argument form .
4.5 Generalization
Generalization of functions (or algorithms) is the process of modifying a function/algorithm that
solves a specific problem so that it can solve a wider range of problems, or a larger number of
instances of the same problem. Let’s consider a totally imaginary video streaming service, Netflux.
2 Thefirst set of triple double-quotes must be indented, but the rest of the docstring need not be because Python
interprets the entire docstring as a single line of text.
4.6 Cohesion 47
Suppose we want to compute how many users can simultaneously stream a movie on Netflux on
an 25Mbps internet connection, and we know that each user that is streaming requires 3Mbps. We
could write a function to do this:
def ho w_ma ny_n etfl ux_ stre ams ():
return 25 // 3; # use integer division since we
# can ’t have a fraction of a user
Now we can call this function whenever we need to know how many users can simultaneously stream
Netflux, without having to remember how that is calculated. But what if some people have faster
or slower internet connections? We could generalize this function to apply to those situations by
making the speed of the internet connection a parameter:
def ho w_ma ny_n etfl ux_ stre ams ( speed ):
return speed // 3;
Now we have a function that can solve the same problem in a much wider range of situations! Can
you think of how we might generalize this further (see the footnote for the answer!)?3
The more general a function is, the more re-useable it is. The more often we can re-use existing
code that has been tested and proven to work, rather than write new code, the less likely we are to
introduce errors into programs.
Of course, there is a limit to this. Functions that do too much or too many different things are
actually bad. Thus, generalization must be tempered by another concept called cohesion which we
discuss in the next section.
4.6 Cohesion
In software design, the term cohesion refers to the idea that code that is grouped together should
have something in common. In terms of writing functions, functions that perform one task and one
task only are said to have high cohesion. Functions with high cohesion are preferred because they
increase the reusability and maintainability of software components. Our function from Section 4.5
that computes how many users can stream Netflux on an internet connection of a certain speed has
high cohesion because it performs a single, well-defined task.
An example of low cohesion would be a function that, say, not only computed the number of
users that can simultaneously stream on a connection but also includes a parameter that changes
the user’s streaming quality (e.g. standard or high definition). These are two different tasks that are
entirely independent, and should be implemented in separate functions.
3 What if Netflux improves their video quality, and each user requires instead 5Mbps to stream? We can make our
function work in even more situations, including this one, by making the bandwidth needed to stream one movie a
parameter as well!
Objects and Encapsulation
Calling Methods in Objects
Mutable vs Immutable Objects
Defining Our Own Objects
5 — Objects
Learning Objectives
In the next section we’ll show you how to call methods defined in objects.
If you took CMPT 140...
... you may remember that the term encapsulation was used to describe procedural encapsu-
lation, where we “package” an algorithm as a function so that it can be re-used easily. Here
we are applying the same term to the “packaging” of data as well. Objects encapsulate both
data items and a group of algorithms that operate on that data.
Our method call returned 7! This is because the string ’Internet’ occurs beginning at 7 characters
to the right of the first character in the string (count it!). If the find method does not find the given
string in its object’s string, it returns -1.
So to summarize, we can call a method in an object in the same way as we call functions, we
just need to use the dot notation to specify which object we want to call the method on. We will be
calling methods in objects quite a lot. We’ll also be seeing many other kinds of objects other than
strings.
6 — Modules
Learning Objectives
• describe what a module is and why one would want to use one;
• identify and author Python code to make the functions in a module available to a
program;
• design and author programs that make use of functions from modules; and
• be able to locate, and understand the documentation for the functions of a module.
defined in a module called math. If we want to use the functions in the math module, we need to
import them from the math module into our program. For example:
>>> log10 (1000) # this won ’t work , there is no such function .
Traceback ( most recent call last ):
File " < stdin > " , line 1 , in < module >
NameError : name ’ log ’ is not defined
import x as y
x must be the name of a module, and y must be a valid variable name. This creates an object
called y that contains, as methods, the functions defined in x.
available for it, that there is probably a module to either do, or help you do almost anything you can
think of. It is also possible to obtain and use modules that do not come with Anaconda. These take
the form of Python program files (files with a .py extension) that can be placed in the same folder as
your program, and then imported. It is also possible to write your own modules.1
In this course we will be using several different modules that come with Anaconda, including
ones that can:
• read, write, modify, and display image files;
• plot line and bar graphs; and
• draw graphics to the screen.
Let’s look at one more example right now from skimage, otherwise known as scikit-image.
skimage lets us use and/or manipulate images. The following Python code reads a JPEG image file,
specified by a file name, and displays it to the screen:
import skimage . io as io
im = io . imread ( " images / parrot . jpg " )
io . imshow ( im )
io . show ()
The first line of this code reads the definition of an object called io from the module called
skimage.io. The second line calls the imread method of the io object which reads the given
image file and returns another object (of a different kind than io) containing the image data from the
parrot.jpg file2 . The third line calls the imshow method of io which adds image im to a queue of
images to be displayed on the screen. The fourth line calls the show method of the object io, which
causes the queued image to pop up in a window. It looks something like this:
This image from the public domain was download from pixabay.com.
Look at that cute parrot. He’s gorgeous! Now think about how much is actually going on behind
the scenes in those few lines of code. The file on the disk has to be opened, the data has to be
1 A module is just a .py file that contain only function and/or object definitions. You can write your own group of
chapter.
56 Modules
decompressed, decoded, and loaded into memory, and then it has to sent to the display hardware.
These are all quite complex operations with many many steps. But thanks to abstraction, we
accomplished all that with just three simple method calls to imread, imshow, and show.
We will be exploring some more of the capabilities of the skimage module in class.
Learning Objectives
7.1 Sequences
A sequence is a compound data type consisting of one or more pieces of data in a specific linear
ordering. An example is a string — it is a sequence of characters. Python defines two important
operators called indexing and slicing that can be applied to sequences, including strings.
Later, we shall find out that lists and arrays in Python are also examples of sequences, and we’ll
be able to apply the indexing and slicing operations we learn here to those data types as well.
7.2 Indexing
Each data item in a sequence has a position. We denote that position using an integer which we call
an offset. The item at the beginning of a sequence has offset 0. The second item in a sequence has
offset 1, the third has offset 2, and so on. In general, if an item is n positions to the right of the first
position, it has offset n. This is why we call it an offset. The second item of a sequence is offset by 1
position from the first position. The fifth item in a sequence has offset 4, because it is offset by 4
positions from the first position — if you start at the first position and move right 4 times, you’ll be
at the fifth item.
You can also think of it this way: if an item is the i-th item in a sequence, it has offset i − 1. The
following picture shows that the string ’Vader’ can be viewed as a sequence of characters with
58 Indexing and Slicing of Sequences
offsets 0 through 4:
Character offsets: 0 1 2 3 4
In Python, you can access an item in a sequence using its offset. This is done by putting the
offset of the desired item inside a pair of square brackets after the sequence. The square brackets are
the indexing operator. Since Python strings are sequences, we can use indexing to access specific
characters within a string. This works with both string literals and string variables:
>>> ’ Vader ’ [3] # Get 4 th character from literal
’e ’
>>> s = ’ Skywalker ’ # Make s refer to ’ Skywalker ’
>>> s [0] # Get first character from string s
’S ’
>>> s [4] # Get fifth character from string s
’a ’
>>> c = s [8] # Get 9 th character from s , give it the name c
>>> print ( c ) # print c ( the 9 th character from s ).
r
>>> print ( s [2]) # print the third character of s
y
>>> s [0]+ s [2]+ s [4] # Concatenate the 1 st , 3 rd , and 5 th characters
’ Sya ’
>>> x = 7
>>> s [ x ] # Offset 7 since x refers to 7
’e ’
>>> s [ x +1] # Offset 8 since x refers to 7
’r ’
In each example above, the indexing operator obtains the character from the string at the given offset.
Then we can do what we want with it: assign a variable name to it, pass it to a function, use it in
an expression, etc. Also note that offsets can be integer literals, variables that refer to integers, or
integer-valued expressions.
>>> t = ’ TARDIS ’ # Make t refer to ’ TARDIS ’
>>> t [ -1] # Access the last character of t
’S ’
>>> t [ -3] # Access the third - last character of t
’D ’
7.3 Slicing 59
7.3 Slicing
Slicing is the act of selecting zero or more items of a sequence and forming them into a new sequence.
Slicing is similar to indexing but it allows us to specify multiple offsets at once using a convenient
syntax. The result of slicing is a new sequence consisting of the items at the specified offsets.
We can specify a contiguous range of offsets using the : operator — this is the slicing operator.
If we write x:y, where x and y are integer expressions, this means the range of offsets between x and
y − 1. That’s right, y − 1. The range of offsets is inclusive on the lower end and exclusive on the
upper end. Thus, 0:42 actually specifies the range of offsets 0, 1, 2, . . . , 41. 42 is not included!
We can use the slicing operator to specify multiple offsets for the indexing operator to obtain
substrings of a string in Python:
>>> s = ’ Skywalker ’
>>> t = s [3:9] # get the substring of s between offsets 3 and 8
>>> print ( t )
walker
>>> print ( s [0:3]) # get the substring of s between offsets 0 and 2
Sky
The exclusion of the item at the upper offset of the slicing operator in the resulting sequence probably
seems strange now, but it’s actually quite convenient. For example, if s is a string, then s[x:len(s)]
extracts the substring beginning at offset x and ending at offset len(s)-1, which is the last valid
offset.
60 Indexing and Slicing of Sequences
8 — Control Flow
Learning Objectives
• identify and define the behaviour of relational operators, logical operators, and Boolean
expressions in Python;
• identify and author correct Python language syntax for branching statements: if, if-else,
if-elif-else, and chained statements; and
• design and author Python programs that use if, if-else, nested if, and chained-if state-
ments.
An operator that produces a result that is either True or False is called a relational operator.
Relational operators are used to ask simple "true or false" questions about how one piece of data
is related to another. Thus, relational operators always have two operands. For example, the value
of the expression 2 < 4 is True. This is because the < operator is the “less than” operator. More
generally, the expression x < y has the value True if the value of x is smaller than the value of y.
The following table lists several commonly used relational operators in Python.
62 Control Flow
Boolean Expressions
A Boolean expression is any expression whose value is either True or False. Thus, all of the
expressions in the third column of the above table are Boolean expressions.
Expression Value
1 - 1 > 0 and -2 > 0 False
False and ’x’ < ’y’ False
9 >= 9 and ’FortyTwo’.isdigit() False
5 < 10 and 20 != 42 True
len(’Skywalker’) > 0 and len(’Skywalker’) < 10 and ’Ren’ < ’Rey’ True
8.2 Logical Operators 63
Note the order of operations in the first example. The subtraction happens first, because it has
higher precedence than all relational and logical operators. Then the two greater-than operators are
evaluated because relational operators have higher precedence than logical operators. The last thing
that happens is the and operator. Since both > operators result in False, the entire expression is
False.
In the third example, we call the isdigit method on the string ’FortyTwo’. Since the string
FortyTwo doesn’t contain digits, the function returns False. Therefore, even though the relation 9
>= 9 is True, the entire expression has the value False.
In the last example, the two and operators are evaluated left-to-right. The result of the first
and is True, which becomes the first operand to the second and, then True and ’Ren’ < ’Rey’
evaluates to True, so the whole expression evaluates to True.
Expression Value
5 < 7 or 0 == 0 True
7 < 5 or 0 == 0 True
2**5 < 16 or max(7, 42) == 7 False
’Skywalker’.find(’Anakin’) > -1 or ’Skywalker’.islower() False
The last example is False because ’Anakin’ is not a substring of ’Skywalker’ so the find func-
tion returns -1. -1 is not greater than -1, so the first operand to or is False. ’Skywalker’.islower()
is also False since ’Skywalker’ does not consist only of lowercase characters. Thus, both operands
are False, so the or evaluates to False.
Expression Value
not 42 < 0 True
not 6 == 6 False
not max(17, 50) > 80 True
In the last example, the function call max has the highest precedence; it returns 50. The next highest
precedence is the > operator (relational operators have higher precedence than logical operators),
which results in False since 50 is not greater than 80, then not False results in True.
same precedence! The operator not has higher precedence than and which, in turn, has higher
precedence than or. Take a look at these expressions:
Expression Value
not 5 < 7 or 0 == 0 True
not (5 < 7 or 0 == 0) False
len(’Vader’) < 7 or len(’Maul’) < 3 and ’Vader’ < ’Maul’ True
(len(’Vader’) < 7 or len(’Maul’) < 3) and ’Vader’ < ’Maul’ False
You might expect the first expression to have a value of False, because 5 < 7 or 0 == 0 is
clearly True, and the not would change that to False. But the not operator has higher precedence
than or. In this expression, the relational operators evaluate first, giving us not True or True.
Now the not is applied to the first True, giving us False or True, which ends up as True. If
we really want to apply not to the result of the or, we have to add parentheses, like in the second
example. The relational operators still evaluate first, again giving us not (True or True). But
now, because of the parentheses, the or evaluates next, which gives us not True, and ultimately
False.
Note how in the third and fourth examples, if we want the or to evaluate before the and we have
to use parentheses around the or expression. You can see that it matters because we get different
answers depending on which of or or and evaluates first.
The if-statement is then followed by a block. Recall that a block is a series of indented lines of code.
The block of code following the if-statement is only executed if the condition in the if-statement
evaluates to True. Let’s look at an example:
guess = int ( input ( ’ Guess a number between 1 and 100 ’ ))
if guess >= 1 and guess <= 100:
print ( ’ That was a valid guess ! ’)
Listing 8.1: A program that uses a conditional statement.
The first line of this example asks the user to input a number between 1 and 100. The name guess is
assigned to the value entered. Then we have an if-statement. The condition of the if-statement is the
Boolean expression guess >= 1 and guess <= 100. The value of this expression will, of course,
depend on the value of guess. If guess is, in fact, between 1 and 100, the Boolean expression is
True, and the one-line block of code consisting of the print call is executed. Otherwise, it is not.
Here is what we see if we run the program, and enter the number 50 (green text is text entered by a
user):
Guess a number between 1 and 100: 50
That was a valid guess !
Since the Boolean expression in the if-statement is True, the indented block consisting of the call
to print is executed. If we enter a value that is not between 1 and 100, the print call will not
execute and we will not see the output That was a valid guess!. But what if we want to print
something different if the guess is not between 1 and 100? It might be natural to try this:
guess = int ( input ( ’ Guess a number between 1 and 100: ’ ))
if guess >= 1 and guess <= 100:
print ( ’ That was a valid guess ! ’)
if
condition
True False if condition:
# if block ( indented )
execute execute else :
"if" block "else" block
# else block ( indented )
Now suppose we wanted to give the user a little more information about why a guess was invalid.
If the user guessed a number that was too large, we want to print out Too high!. If they guess too
low, we want to print out Too low!. Otherwise, we want to print out That was a valid guess.
Here’s one way we could do that:
guess = int ( input ( ’ Guess a number between 1 and 100: ’ ))
if guess < 1:
print ( ’ Too low ! ’)
blocks can execute. The final else block only executes if none of the preceding conditions were True.
Once one of the blocks executes, the execution continues at the first line of code following the else
block. Multiple elif-statements and accompanying blocks are allowed as long as the first conditional
statement is an if-statement. In all cases the else statement is optional. The flow of execution in an
if-elif-else chain is described by the following flowchart and code template:
if True execute
condition: block #1
False
if condition:
elif True execute # block 1 ( indented )
condition: block #2 elif condition:
# block 2 ( indented )
False elif condition:
# block 3 ( indented )
elif True execute
condition: block #3 # ... more elif ’s as desired
else : # ( optional )
False
# else block
.. more elif’s as desired
.
# code after the else block
else block
(optional)
code after
the else
block
Notice that only one of the blocks in the if-elif-elif-...-else chain can execute no matter how many
elif-statements there are. Finally, remember that the blocks can consist of multiple lines of code, as
long as they are all indented.
# suppose smaller and larger are variables referring to
# integer values
if smaller > larger :
# swap the values referred to by the variables
temp = smaller
smaller = larger
larger = temp
Because all three lines after the if-statement in the above code are indented, they are all part of the
block, and all three only get executed if the if-statement’s condition is True . Block indentation must
be such that every line of every block is indented by the same amount, otherwise Python will not
understand your program. Moreover, the indentation must be either all spaces or all tabs, you can’t
mix them. However, most text editors that are Python-aware (e.g. PyCharm, TextWrangler) should
automatically prevent you from mixing tabs and spaces.
While-Loops
While Loops for Counting
For-Loops
Ranges and Counting For-Loops
Choosing the Right Kind of Loop
Infinite Loops
Learning Objectives
• identify and correctly author Python language syntax for repetition: while loops and
for loops;
• trace by hand the flow of program execution for programs that use while-loops and
for-loops;
• design and author Python programs that use one or more loops; and
• describe what is an infinite loop.
Very frequently in computer programming we would like to repeat certain actions. Sometimes
we want to repeat these actions a specific number of times. Other times, we want to repeat some
actions as long as some specified condition (i.e. Boolean expression) is True. Sometimes we’d like
to repeat some actions for every element of data in some collection of data elements. In Python, we
can do all of these things using loops.
9.1 While-Loops
While-loops work a lot like an if-statement in that they have very similar syntax — a condition
followed by a block — but the block can be executed multiple times as long as the condition is True.
While-loops consist of the word while, followed by a Boolean expression (the loop condition),
followed by a colon, followed by a block of code. Below you can see the general form of a while-loop,
and the corresponding flow of execution presented as a flowchart.
70 Control Flow – Repetition
code before
while-loop
# code before while - loop
while condition:
while True execute
block
# block ( indented )
condition:
When execution of code reaches a while-loop, the loop’s condition is evaluated. The condition must
be a Boolean expression yielding a result of True or False. If the condition is True, the block of
code following the while-loop’s condition is repeated until the condition becomes False. Then
the (unindented) code after the while-loop executes. Note that it is possible that the loop condition
is False the first time it is encountered. If this is the case, then the block is never executed, and
execution proceeds to the code after the while-loop.
A while-loop can help us improve our guessing game from Section 8.3. Previously, we asked the
user to input a number between 1 and 100, and reported whether the guess was too high, too low, or
valid. But we had no easy mechanism to ask the user for a new guess if their guess was too high
or too low. With while-loops, we can repeat the actions of asking for a guess, and checking it for
validity until the user enters a guess that is valid!
guess = int ( input ( ’ Guess a number between 1 and 100: ’ ))
while guess < 1 or guess > 100:
if guess < 1:
# If guess was less than one , execute this block .
print ( ’ Too low ! ’)
elif guess > 100:
# Otherwise , if guess is larger than 100 , do this block .
print ( ’ Too high ! ’)
of levels so long as all of the blocks at the same level are indented by exactly the same amount
throughout the entire program.
If we are to run our new guessing program, the output will be as follows (green text is text
entered by the user):
Guess a number between 1 and 100: 125
Too high !
Guess a number between 1 and 100: 0
Too low !
Guess a number between 1 and 100: 42
That was a valid guess !
While loops can also be used to execute a block of code a pre-determined number of times. These are
called counting loops because an integer variable is used to count the number of times the block has
executed, and the loop condition is such that the condition is True as long as the loop has executed
fewer than the required number of times. For example, we can use a counting while-loop with the
turtle graphics module to write a function that draws a row of n circles on the screen:
import turtle as turtle
def drawCircles ( n ):
circlesDrawn = 0 # number of circles drawn so far
while circlesDrawn < n : # while we haven ’t drawn n circles
turtle . goto ( circlesDrawn *50 , 0) # move the turtle
The important things to take away from this example are that the variable circlesDrawn acts as
a counter that keeps track of how many circles we’ve drawn, and that the while-loop’s condition
circlesDrawn < n causes the while-loop’s block to execute until we have drawn exactly n circles.
The last line of the block where circlesDrawn is increased by 1 is very important for this to work.
If you leave this line out, you will get what is known as an infinite loop (see Section 9.6). If we were
to call the drawCircles function with an argument of 5, like this: drawCircles(5) then we’d see
the following output consisting of five circles in a row:
72 Control Flow – Repetition
9.3 For-Loops
In Python, for-loops allow repetition of a block of code for each data item in a sequence (recall
sequences from Section 7.1). Right now we know about one kind of sequence: strings. So we can
use a for-loop to do something for every character in a string. In this example, we have a function
that counts and returns the number of capital letters in a string:
def countCaps ( s ):
count = 0
for character in s :
if character . isupper ():
count = count + 1
return count
The block following the for-loop (consisting of the if-statement and its block) is executed once for
each character in the string s; each time the block is repeated, the variable character refers to the
next character in the string.
In general, the syntax of a for-loop consists of the word for, followed by a variable name,
followed by the word in, followed by a sequence, followed by a colon, followed by a block:
9.4 Ranges and Counting For-Loops 73
for variable in sequence:
# Block of code -- each time this block is repeated ,
# variable refers to the next item in the sequence.
# Repetition stops after each item in the sequence has
# been processed .
When we do something for each element of a sequence we say that we are iterating over the sequence.
For-loops can be used to iterate over any sequence, not just strings. In the next section we will
introduce another kind of sequence called a range which is a sequence of integers. We will learn
about even more types of sequences in later chapters.
# General form :
range (start, stop, step_size)
Remember: the value stop is not part of the sequence.
Ranges can be used to write counting for-loops. Here is a for-loop that repeats its block exactly
N times:
for i in range ( N ):
# do something
In this loop, i refers to the value 0 on the first repetition, 1 on the second repetition, and so on, up to
N-1 on the last repetition. It is equivalent to the following while-loop:
i = 0
while i < N ;
# do something
i = i + 1
74 Control Flow – Repetition
# This loop is infinite because the programmer incorrectly used
# ’ or ’ instead of ’ and ’. Mathematically , the condition can
# never be False , regardless of the value referred to by x .
# Thus , the loop repeats forever .
x = -1
while x >= 0 or x <= 10:
x = input ( " Enter a number that isn ’t between 0 and 10: " )
It’s quite difficult to accidentally write infinite for-loops because sequences are of finite length and
they repeat only once for each item in the sequence.
Lists
Mutable Sequences
Creating Lists
Accessing List Items (Indexing and Slic-
ing)
Modifying List Items
Determining if a List Contains a Specific
Item (Membership)
Adding Items to a List
Removing Items from a List
Sorting the Items in a List
Copying Lists
Concatenation
Other Functions That Operate on Se-
quences
Iterating Over the Items of a List
Nested Lists
List Comprehensions
Tuples
Learning Objectives
10.1 Lists
A list is a compound data type consisting of a set of data items arranged in a specific linear ordering.
In Python, lists have the following properties:
• lists are sequences, and therefore support indexing and slicing (like strings);
• lists may contain items of different data types;
• lists are mutable sequences, meaning they can be altered after they are created (see Section
10.1.1); and
• lists are objects, and contain methods which you can call (just as strings do).
We will see that we can do things to mutable sequences that we cannot do to immutable sequences.
For example, we can add and remove items from a list, but we can’t add and remove characters from
strings (they are immutable).
Sometimes the difference between mutable and immutable sequences can be confusing. When
we do string concatenation using the + operator (remember this from Section 2.3.4?) it kind of
looks like we’re changing a string. When a and b are strings, it may seem like we’re changing a by
appending b to a, but what we are really doing is creating a new (immutable) string that is the result
of the concatenation:
a = ’ Winter is ’
b = ’ coming ’
s = a + b
Here s is a new string. The strings a and b are not changed.
But lists are mutable. We can change them without causing a new list to be created, as we will
see in subsequent sections.
compare the entire data item, and ’Diablo’ and ’Diablo 3’ are not the same strings, so it is
indeed true that y does not contain the string ’Diablo’.
You can get the index of an item in a list using the list’s index method:
>>>y = [ ’ Diablo 3 ’ , ’ Path of Exile ’ , ’ Torchlight II ’ , ’ Grim Dawn ’]
>>>y . index ( ’ Torchlight II ’)
2
>>> x = [2 , 4 , 6 , 8 , 10]
>>> y = x # y and x refer to the same list
>>> z = y . copy () # z refers to a copy of list y
>>> z [2] = -10 # change something in list z
>>> print ( x ) # change to z does not affect list x
[2 , 4 , 6 , 8 , 10]
>>> print ( y ) # or list y ( x and y are the same list )
[2 , 4 , 6 , 8 , 10]
>>> print ( z ) # only list z is changed since it was
[2 , 4 , -10 , 8 , 10] # a copy of y .
The important thing to remember is that the assignment operator = does not make a copy of data.
It only associates a new name with that data. Many mutable compound data objects, including lists,
provide methods to create copies of themselves.
10.1.10 Concatenation
We saw, back in Section 2.3.4, that the + operator concatenates two strings. The + operator can
actually be used as a concatenation operator with any type of sequence, including lists:
>>> a = [1 , 3 , 5 , 7 , 9]
>>> b = [2 , 4 , 6 , 8 , 10]
>>> c = a + b
>>> print ( c )
[1 , 3 , 5 , 7 , 9 , 2 , 4 , 6 , 8 , 10]
Earlier we saw that the extend method could add the items in one list onto the end of another
list. So it would seem that a + b does the same thing as a.extend(b). But be careful... they’re not
the same! The concatenation operator creates a new list that is the concatenation of its operands. To
put it another way, if a and b are lists, then c = a+b is equivalent to:
c = a . copy ()
c . extend ( b )
The extend method does not create a new list, it just adds the items in its argument to the existing
list.
stores the data for a magic item with the name “Sword of Fighting”, that is worth 1250 gold pieces,
and can only be used by characters of level 10 or higher.
Now imagine we want to store data about all of the magic items in our game. We could do this
using a list of lists, where each item in the list is another list consisting of the magic item’s name,
value, and minimum level. Here’s an example of a list consisting of three magic items:
How do we know that this a list of lists? Notice the positioning of the square brackets. There is a set
of square brackets enclosing the entire thing that tells us that the whole thing is a list. Within the
outer pair of square brackets, we have three more lists enclosed in pairs of square brackets, each
separated by a comma. To help you see this, each item in the list has been shown in a different colour.
Thus we have a list of three items, each of which is, itself, a list. We can build up fairly complicated
organizations of data just by using nested lists. Lists can be nested to any depth desired.
10.3 List Comprehensions 83
The general form for using list comprehensions to select items from a list is:
[ x for x in sequence if expression ]
where sequence is any sequence and expression is an expression involving x. Each item x from
sequence is selected and added to the resulting list if the expression involving x evaluates to
True. The square brackets around the list comprehension provide visual indication that the result of
the code is a new list.
Another use of list comprehensions is to apply some kind of computation to each item in a
sequence and store the results in a new list. For example, suppose we want to create a list containing
the square roots of the integers from 10 to 50. At this point, we hope you could see how to do this
with a for-loop.1 Here’s how you would do it with a list comprehension:
1 You
would use a for loop to iterate over the sequence range(10,51), take the square root of each item, and append
each square root to the end of a list which is initially empty.
84 List and Tuples
import math as m
roots = [ m . sqrt ( x ) for x in range (10 , 51)]
The general form for computing something for each item in a sequence and putting the results in
a new list is:
[expression for x in sequence ]
where expression is an expression involving x and sequence is any sequence. The result is a list
containing the value of the expression for each item x in the sequence.
List comprehensions are even more versatile than what we have seen here and can be used
to compactly code quite complex things. But for CMPT 141, we will be concerned mostly with
relatively simple list comprehensions of the forms we have seen here. We’ll look at more examples
of list comprehensions in class.
10.4 Tuples
Tuples are identical to lists, with the following exceptions:
• Tuples are immutable.
• Tuples are written with parentheses instead of square brackets.
Other than that, they are more or less the same as lists. They can contain items of different types,
they can be indexed, sliced, concatenated, and can be used with the in and not in membership
operators. However, because tuples are immutable, tuple items cannot be changed, tuples cannot be
used with del, and do not have extend, append, copy or sort methods.
So why would you use tuples instead of lists? Indeed, why would you use an immutable sequence
when you could use a mutable sequence since mutable sequences have more features? To find the
answer, we have to turn to the discipline of software engineering. Software engineering is kind
of like the “science of writing good code”. One principle of software engineering is: “don’t write
code that allows data to be modified in ways you know are not permitted”. This prevents accidental
modification of data in ways it should not be handled. If we have a sequence of data items that we
know should not be modified after creation, we should use a tuple, because tuples are immutable
which prevents modification of the data, either intentionally or accidentally. On the other hand, if
we were writing a grocery list application, we’d want to use a list because lists are mutable, and we
want people to be able to remove items from the list as they pick things off the shelf in the store.
Dictionaries
Creating a Dictionary
Looking Up Values by Key
Adding and Modifying Key-Value Pairs
Removing Key-Value Pairs from a Dictio-
nary
Checking if a Dictionary has a Key
Iterating over a Dictionary’s Keys
Obtaining all of the Keys or Values of a
Dictionary
Dictionaries vs. Lists
Common Uses of Dictionaries
Combining Lists, Tuples, Dictionaries
11 — Dictionaries
Learning Objectives
11.1 Dictionaries
A dictionary associates pairs of data items with one another. The first item in such a pair is called a
key and the second item is called the value. A dictionary stores a collection of these key-value pairs.
Dictionaries allow you to look values up by their key.
Suppose we had a dictionary called friends containing key-value pairs where the keys are people’s
names, and the value associated with each key is that person’s email address. We could then find
out someone’s email address by querying the dictionary for the value associated with a person’s
name. If there is a key-value pair in the dictionary friends whose key is ’John Smith’, the value
of friends[’John Smith’] would be the email address of John Smith. The keys of a dictionary
must be unique — the same key cannot be associated with more than one value. However, the values
need not be unique — different keys can be associated with the same value. Thus, there can only be
one ’John Smith’ key in friends, but another friend with the key’Jane Smith’ may have the
same e-mail address as John Smith.
Over the next few sections, we’ll see how to create such a dictionary and look up items in it.
86 Dictionaries
• values in a dictionary are looked up by their key, whereas items in a list are looked up by their
integer index (position in the ordering).
Dictionaries as Mappings
Dictionaries, by definition associate keys with values. Such an association can be viewed as a
mapping that translates one type of data into another. For example, we could use a dictionary to map
animal species names to their taxonomical class:
specie s_to _cla ss_ mapp ing = {
’ red squirrel ’: ’ Mammal ’ ,
’ komodo dragon ’: ’ Reptile ’ ,
’ chimpanzee ’: ’ Mammal ’ ,
’ snowy owl ’: ’ Bird ’ ,
’ green cheeked conure ’ : ’ Bird ’ ,
’ rainbow trout ’ : ’ Fish ’
}
Now we can use this mapping to look up what basic type of animal a certain species is. When a
dictionary is used to store a mapping, the dictionary is viewed as a collection of many individual
data items.
Dictionaries as Records
One common use of a Python dictionary is to represent a record. A record is a group of related
named data elements, for example, the spaces that get filled out in a form, such as name, address,
phone number, etc. Note that the term record is not specific to particular programming language but
rather is a name for this data organization paradigm. The main purpose of a record is to store, as a
group, several pieces of data that can be accessed by name.
Records are defined by the names of the data items, and the type of the data items. If we were
studying the history of pirates, we might want to define a record that has five data items: given name,
family name, pirate name, birth year, and death year. Such a record might be used to store and group
together all of the data we want to collect about one pirate. An example of such a record might be:
given_name Edward
family_name Teach
pirate_name Blackbeard
birth_year 1680
death_year 1718
Most programming languages support some way of defining and handling records. In Python,
records are stored as dictionaries. The names of the data items in a record are a dictionary’s keys,
and a dictionary’s values are the values associated with each data item. The record shown above
would be stored in Python as the following dictionary:
90 Dictionaries
pirate1 = { ’ given_name ’: ’ Edward ’ ,
’ family_name ’: ’ Teach ’ ,
’ pirate_name ’: ’ Blackbeard ’ ,
’ birth_year ’: 1680 ,
’ death_year ’: 1718 }
Now we can look up data about a particular pirate by name. Given the above dictionary, we could
compute Blackbeard’s age when he died:
pirate_age = pirate1 [ ’ death_year ’] - pirate1 [ ’ birth_year ’]
When a dictionary is used as a record, the dictionary is viewed as a single data item with several
properties.
Dictionaries as Databases
You can think of a database as a mapping that maps keys to records. When a dictionary is used as a
database, the keys are often strings or numbers, and the values are records. Consider a database of
customer information. The keys of such a database could be the customer’s name, and the value for
each key would be a record (i.e. another dictionary!) containing all of the information about that
customer. This would enable us to obtain all the information about one customer by looking up their
name in the dictionary to retrieve their record of information. Here is what such a dictionary might
look like:
cust omer_dat abase = {
’ Homer J . Simpson ’: { ’ first_name ’: ’ Homer ’ ,
’ last_name ’: ’ Simpson ’ ,
’ initial ’: ’J ’ ,
’ address ’: ’ 742 Evergreen Terrace ’ ,
’ city ’: ’ Springfield ’ ,
’ state ’: ’ Unknown ’ ,
’ country ’: ’ USA ’ ,
’ phone_number ’: ’ 555 -555 -5555 ’} ,
’ Charles M . Burns ’: { ’ first_name ’: ’ Charles ’ ,
’ last_name ’: ’ Burns ’ ,
’ initial ’: ’M ’ ,
’ address ’: ’ 1000 Mammon Ave . ’ ,
’ city ’: ’ Springfield ’ ,
’ state ’: ’ Unknown ’ ,
’ country ’: ’ USA ’ ,
’ phone_number ’: ’ 555 -000 -0001 ’} ,
# More entries ...
}
The inner pairs of curly braces tell us that the values associated with each key are dictionaries,
each of which have data items named first_name, last_name, initial, address, city, state,
country, and phone_number.
We can obtain the entire record for a given person in the database by looking up their name:
burns_record = customer_database [ ’ Charles M . Burns ’]
11.2 Combining Lists, Tuples, Dictionaries 91
Note that the variable burns_record now refers to another dictionary, specifically, the dictionary
associated with the key ’Charles M. Burns’ (which is a key in the customer_database dictio-
nary). Now we can find out more about Mr. Burns by looking up the data items within his record by
name:
print ( ’ Mr . Burns lives at ’ , burns_record [ ’ address ’ ])
This prints out
We can even access data items in a database record without storing it in an intermediate variable first.
The following produces the same result without using the variable burns_record:
print ( ’ Mr . Burns lives at ’ ,
cust omer_data base [ ’ Charles M . Burns ’ ][ ’ address ’ ])
12 — File I/O
Learning Objectives
• describe some common ways in which data may be organized in a text file;
• author Python code to open and close files;
• author Python code to read a text file one line at a time;
• apply basic string processing to read numeric data from a text file containing numbers;
• author code to read a line containing multiple data from files using split; and
• author Python code to write data to a text file.
Up to this point, the only mechanisms we have used for data input into our programs is to either
code the data right into our program as literal data (this is sometimes called hard-coding the data)
or ask the user to enter input from the console. In this chapter, we look at how to obtain input data
stored in files. Similarly, the only way we have seen our programs produce output is to print to the
console. In this chapter we will also look at how to write output to a file.
Binary file formats are generally not readable by humans because the data is binary-encoded.
Such files generally do not contain any meaningful whitespace such as spaces or newlines and appear
as gibberish when viewed in a text editor. In a binary file format, numbers are stored in binary (base
2) format, in groups of 8-bits (a byte). A number might be comprised of the bits in one, two, or four
consecutive bytes. If we stored the temperature data, above, in a binary file, it might look something
like this when we load it into a text editor:
Binary files are typically more compact, use less disk space, and are used frequently in commercial
applications and games. Since this is an introductory course, we will not be using any binary file
formats, only text file formats.
Each line of this file holds the data for one database entry, and contains exactly 8 data items, separated
by commas. The first data item on each line is the key for a database entry, and the remaining data
items are the data items in the database record associated with the key. Note how the fourth data
item of the third database entry is empty since there is nothing between the commas.
If our data items themselves do not contain spaces, we can use whitespace as a delimiter, which
makes the text file look more like a table. Here is an example of a tabular datafile that stores weather
observations taken every four hours for different weather stations on one specific day of the year
where each weather station is identified by a four-digit ID number:
1783 22 25 27 28 21 19
2214 -4 2 6 7 6 0
9934 -40 -32 -26 -21 -24 -32
5538 15 17 21 22 23 19
The first column contains the weather station ID number, and the remaining columns store tempera-
ture observations. Since observations are every four hours, there are six such columns.
Other Formats
Any format you can think of is theoretically possible, but you might have to write custom code that
can process unconventional formats.
write mode, it is possible that the data you wrote to the file will not actually be written, and that is
very bad!
whitespace at the end of the string, including spaces and newlines, removed. Revising our loop in
the previous code to this:
f = open ( ’ movietitles . txt ’ , ’r ’)
titles = []
# iterate over each line of the file
for line in f :
# append the next line ( movie title ) to the list
titles . append ( line . rstrip ())
f . close ()
results in titles referring to the list:
[ ’ The Fellowship of the Ring ’ , ’ The Two Towers ’ , ’ The Return of the King ’]
Another way to create a list of the strings from the lines in a file is to use the list function to
convert the sequence of lines from the file object f to a list. Then we can use a list comprehension to
remove the newlines:
f = open ( ’ movietitles . txt ’ , ’r ’)
titles = list ( f )
titles = [ t . rstrip () for t in titles ]
f . close ()
The result of this code is the same as the previous code listing.
What if we have a list file of numbers? This would seem to be a problem if file objects can only
return each line as a string because we would want to read in a file of numbers and store them as
numbers, not strings. We can use the built-in functions int or float to convert strings to numbers.
For example int(’42’) returns the integer 42, and float(’64.9’) returns the floating point value
62.9. If you use int or float on a string that doesn’t represent a number of the appropriate type,
Python will respond with a ValueError. We could read the list file containing temperature data at
the beginning of Section 12.1.1 and store the data as a list of floats like this:
f = open ( ’ temperatures . txt ’ , ’r ’)
temps = []
for line in f :
temps . append ( float ( line ))
f . close ()
or equivalently:
f = open ( ’ temperatures . txt ’ , ’r ’)
temps = list ( f )
temps = [ float ( t ) for t in temps ]
f . close ()
Both programs here would cause temps to refer to the list:
[ -2.7 , -1.8 , 0.3 , 2.4 , 3.5 , 5.9]
98 File I/O
This code iterates over each item in the list ingredients, and writes it to the file. Note how we
concatenate each item in the list with a newline before writing it so that each string appears on its
own line. The resulting file looks like this:
eggs
milk
flour
yeast
If the items we are writing are not strings, we have to convert them to strings because the write
method can only write strings to files. We can do this using the built-in str function which converts
its argument to a string, if possible. Here’s how we would write a list of integers to a file, one per
line:
ingredients = [99 , 88 , 77 , 66 , 55]
f = open ( ’ numbers . txt ’ , ’w ’)
for i in ingredients :
f . write ( str ( i ) + ’\ n ’)
f . close ()
Note how the integer i is converted to a string prior to concatenating it with a newline.
12.5 Pathnames
Even if you’ve never programmed a computer before, but rather, only used one, you probably already
know something about pathnames. Pathnames are strings that refer to files. When we use the open
function, we said back in Section 12.2 that we need to pass a pathname as an argument to open to tell
it which file to open. If you want to open a file in the same folder as your Python program, you only
need to specify the file’s name as a string, like ’temperatures.txt’ or ’reallycooldata.csv’.
If the file exists somewhere else you need to give a full pathname that also specifies the folder that
the file resides in. The mechanism for doing this depends on your computer’s operating system. On
Windows, folder names are separated by a backslash, and the whole pathname might be preceded by
a drive letter:
’C:\Users\Mark\My Documents\awesomedata.csv’
On Mac and Linux, folder names are separated in a pathname by a forward slash:
’/home/mark/Documents/awesomedata.csv’
These are examples of absolute paths because they specify the entire path to the file beginning at the
root folder. You can also use relative paths which specify the path to a file beginning from the folder
that your Python program is in, such as:
’../../experiment/data/specialdata.txt’
The folder name ’..’ means “parent folder”. So the above path means go "up" two folders, then go
into the experiment/data folder, and find specialdata.txt there.
102 File I/O
The different folder separator for Windows and Linux/Mac means we have to be a little careful
if we want our programs to work on all operating systems. Fortunately, Python has a module for that.
The os.path module has methods for constructing pathnames using the appropriate folder separator
for the operating system you are currently running on.
The method os.path.join() method can be used to concatenate folder and file names using
the correct separator. The variable os.sep also refers to the correct separator. Examples:
import os
# An absolute path :
filename = os . path . join ( os . sep , ’ home ’ , ’ mark ’ , ’ Documents ’ , ’ awesomedata ’)
fid = open ( filename )
fid . close ()
# a relative path :
filename = os . path . join ( ’ .. ’ , ’ experiment ’ , ’ data ’ , ’ specialdata . txt ’)
fid = open ( filename )
fid . close ()
Try this on different operating systems and you’ll notices differences in the string referred to by
filename. Experiment on your own with os.path.join() until you’re comfortable with how it
works.
There are lots of other ways of creating and manipulating paths in the os.path module that
are outside the scope of the course, but you can learn more about them here if you are interested:
https://docs.python.org/3.5/library/os.path.html.
Why do drive letters on Windows operating systems start at ’C’ and not ’A’?
Arrays
Arrays vs. Lists
Arrays in Python – The numpy Module
Programming with numpy Arrays
Creating Arrays
Important Array Attributes
Indexing and Slicing Arrays
Arithmetic with Arrays
Relational Operators with Arrays
Iterating Over Arrays
Logical Indexing
Copying Arrays
Passing Arrays to Functions
13 — Arrays
Learning Objectives
• explain how data items are organized in one- and two-dimensional arrays;
• describe the similarities and differences between arrays and lists both generally, and in
Python;
• create arrays in Python;
• access important array attributes (e.g. size, shape) in Python;
• access desired items in arrays using indexing and slicing;
• perform basic arithmetic and relational operations on each item of an array using
arithmetic and relational operators;
• iterate over each item in a one- or two-dimensional array using for-loops; and
• select items from a one- or two-dimensional array using local indexing.
13.1 Arrays
An array is a d-dimensional table of data items. The table of data items can be one-dimensional (like
a list), two-dimensional (like a grid), or of higher dimension. Each dimension of the table is indexed
by a non-negative integer. A one-dimensional (1D) array is usually visualized as a table with just
one row, for example, an array of length 5 containing floating-point data items:
0 1 2 3 4
Like lists, we can access a data item in an array by specifying its index (offset). In the picture above,
the value 5.0 is the data item at index 2. The indices for each position in the array are shown above
104 Arrays
the array.
Two-dimensional (2D) arrays allow us to organize data items in a grid. An item in a 2D array can
be accessed by specifying its row index and its column index. A two dimensional array is usually
visualized as a table with many rows, like this:
0 1 2 3 4 5
0
38 30 80 44 82 62
1
28 27 96 62 12 72
2
5 94 10 68 58 40
3
67 23 80 16 17 88
This 2D array of integers could represent grayscale values of pixels in an image where larger numbers
indicate brighter pixels. If we accessed the data item at row 2, column 0 of this array, we would find
the data item 5. Thus we can think of each data item in an array as having a row-column coordinate
(y, x) in a two-dimensional plane, much like we do when we plot data on an x-y axis in math class.
But... there are two important differences here. Firstly, in math, we always write the horizontal
or x-coordinate first, then the vertical y-coordinate. When accessing 2D arrays, it is opposite; we
write the y- or row-coordinate first, then the x- or column coordinate. Secondly, in math class when
you have axes, the y-axis gets larger as you go up. In 2D arrays, larger y-coordinates access rows
further down. There are no negative coordinates in a 2D array. Observe the difference:
y (0, 0)
x
(1, 2) (0,0) (0,1) (0,2)
in a list are not necessarily stored near each other in the computer’s memory, while arrays always
occupy a contiguous single block of the computer’s memory.2 Because of the way a computer’s
hardware is constructed, this gives arrays a speed advantage when accessing data sequentially. This
speed advantage increases when the amount of data to be stored increases. For this reason, arrays are
preferred over lists in circumstances where the data doesn’t change frequently, the amount of data
doesn’t change frequently, and fast sequential access to the data is required.
Images are an excellent example of data which is better stored in an array. Images are typically
quite large arrays of data, and are very frequently accessed sequentially, one row at a time.
Note how printing an array produces output that looks a lot like a list, or list of lists except that the
data items are not separated by commas, and 2D arrays are arranged by rows.
numpy also has some functions for creating arrays of a certain size where every data item in the
array is a specific value. For example, we can create arrays of a specified size containing all zeros
with numpy’s zeros function. For construction of 1D arrays zeros requires only one argument —
an integer specifying the length of the array to be created. For construction of 2D arrays, zeros
requires a list as an argument containing two integers that represent the number of rows and columns
to create respectively.
>>> import numpy as np
>>> # create a 1 D array of 10 zeros
>>> a = np . zeros (10)
>>> print ( a )
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
>>> # create a 2 D array of zeros with 3 rows and 8 columns
>>> b = np . zeros ([3 ,8])
>>> print ( b )
[[ 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 0. 0. 0.]]
Observe how the dots indicate that the values are floating-point. Floating-point is the default data
type for arrays in numpy. We’ll say more about array data types shortly.
There is also a ones function in numpy that behaves the same way as zeros but sets each data
item in the array to one instead of zero.
In Python, numpy arrays are objects. We know that objects contain methods. What we haven’t seen
yet is that objects can also contain variables. A variable inside of an object is called an attribute
in Python. They are accessed in the same way as methods, using the dot-notation, but without the
round brackets for calling a function.
The dtype attribute of an array indicates the type of the data stored in the array. The data types
used by numpy are different from the data types used by Python, which can be a bit confusing. The
type float64 is essentially the same as a regular Python floating-point data item; the 64 refers
to the fact that these occupy 64 bits of the computer’s memory. The type int64, however, is
different from regular Python integers. Regular Python integers are unlimited in size, but int64
values must be between −263 and 263 − 1. Sometimes, especially when we work with images,
arrays have a dtype of uint8. These are 8-bit integers that are unsigned, meaning they cannot be
negative; numbers of this data type must be between 0 and 255. If you are interested, you can find
a complete list of the data types that numpy arrays can store and the valid ranges of values of each
at http://docs.scipy.org/doc/numpy-1.10.1/user/basics.types.html. The default type
for integer arrays is either int32 or int64 depending on your computer’s operating system. The
default type for floating-point arrays is float64.
Most of time in CMPT 141 it won’t be necessary for you to care about the exact data type of
arrays because we won’t be using numbers large enough for the limits to matter, but it is important to
know that every array has a specific data type, and that the data type may limit the size of numbers
you can have in the array.
108 Arrays
[[ 0 1 2 3 4 5 6 7 8 9]
[10 11 12 13 14 15 16 17 18 19]
[20 21 22 23 24 25 26 27 28 29]
[30 31 32 33 34 35 36 37 38 39]
[40 41 42 43 44 45 46 47 48 49]
[50 51 52 53 54 55 56 57 58 59]
[60 61 62 63 64 65 66 67 68 69]
[70 71 72 73 74 75 76 77 78 79]
[80 81 82 83 84 85 86 87 88 89]
[90 91 92 93 94 95 96 97 98 99]]
The subtraction (-), multiplication (*), and division (/) operators function similarly with arrays,
operating on individual data items in the same array positions of each operand. For this reason, only
arrays of the same shape can be used with addition, subtraction, multiplication, and division. Thus,
you could subtract two arrays with five rows and five columns each, but you could not subtract an
array with two rows and five columns from one with five rows and five columns.
An exception is that you can add/subtract/multiply/divide an array with a single number. This is
the same as adding/subtracting/multiplying/dividing each data item in the array to/from/by the single
number. For example: x - 1 subtracts 1 from each data item in the array x; 4*x mulitplies each
data item in the array by 4. In each case the result is a new array and x remains unchanged. So y
= 4*x would cause y to refer to a new array that contains each item of the array referred to by x
multiplied by 4, but x itself would not be changed.
14 — Recursion
Learning Objectives
14.1 Introduction
Recursion is a form of repetition that uses function calls instead of loops. A recursive function is a
function which contains one or more calls to itself. This allows for repetition of the instructions in
the recursive function. One should not find this type of self-reference disconcerting. It’s not much
different from a self-referencing definition like “the sum of n numbers is equal to the sum of the
first n − 1 numbers added to the last number”, which defines a sum in terms of another sum. In any
case, it is this notion of self-reference that makes a function recursive. Functions that do not call
themselves are non-recursive.
A recursive function solves one and only one problem, but can solve most or all instances of that
problem. “Add up the first N positive integers” is a problem; doing so for a specific value of N is an
instance of that problem.
When given an instance of a problem, a recursive function typically solves a slightly smaller or
easier instance of the problem by calling itself and providing the slightly smaller problem instance as
input, then uses the solution to the slightly smaller problem instance to, usually quite trivially, solve
the original problem instance. Thus, a recursive function has to be able to solve any size instance of
a particular problem.
116 Recursion
Many problems lend themselves to recursive solutions. For example, consider the problem of
determining how many direct ancestors you have at the n-th generation. You are the 0-th generation,
your parents are the 1st generation, your grandparents are the 2nd generation, your great-grandparents
the 3rd, and so on. So how many ancestors do you have at the n-th generation? If you knew how
many ancestors you had at the n − 1-th generation then you could figure out the answer easily,
because each of the ancestors at the n − 1-th generation has two ancestors at the n-th generation. So
if you know that you have k ancestors at the n − 1-th generation, then you must have 2k ancestors at
the n-th generation. Thus, we could characterize the solution to the problem like this:
(
1 if n = 0;
ancestors(n) =
ancestors(n − 1) ∗ 2 otherwise.
We would read this as follows: "The number of ancestors at the n-th generation (ancestors(n)) is
equal to 1 if n is zero, otherwise, it’s twice the number of ancestors in the previous generation
(ancestors(n − 1)). Note that defining the answer to be 1 for generation 0 (i.e. yourself) is a little
bit arbitrary, but is definitionally convenenient. If it helps, you can think of it as saying "you are a
member of your own family", and when put that way, well of course you are! So we could write the
following function for calculating the number of ancestors at the n-th generation:
def ancestors ( n ):
# we want to determine number of ancestors at the n - th
# generation . If n is 0 , we know the answer immediately .
if n == 0:
return 1
else :
# otherwise , we determine how many ancestors there are at
# generation n -1.
# ( solve a slightly smaller instance of the problem !)
k = ancestors (n -1)
using recursion.
def ancestors ( n ):
k = 1
i = 0
while i < n :
k = k * 2
i = i + 1
return k
The variable k is initalized to 1 and then k is multiplied by 2 exactly n times. You should convince
yourself that, ultimately, the recursive solution does the same thing! It just does it with function calls
instead of a loop.
You may wonder why we bother with recursion, since we already know loops. The answer is that
a loop is a special case of recursion, and an introductory course in computer science is incomplete
without taking a look at recursion. As well, some algorithms can be written far more elegantly and
with much less code using recursion, and are far more difficult to write using a loop. As we learn the
basics of recursion, we won’t see many of these more difficult cases. We hope you’ll take our word
for it that they exist.
negative integer. It gives the wrong answer for negative input, but that’s OK, because there is no correct answer if N is
negative—negative N doesn’t define a valid problem instance.
118 Recursion
little problem solving. Usually, common sense tells us the answer without much thought. When
writing your own recursive functions, you might be concerned because the base case seems too easy.
Don’t be. It is supposed to be extremely easy. The recursive case of our function occurs when N
is larger than 0. The recursive case is found in the else block of the if-statement. It tells us that
we can calculate the required sum by first solving the sub-problem of finding the sum of squares
from 0 to N − 1 using a recursive call. We can then turn that into a solution for the sum of the first N
squares by adding N 2 to the solution to the sub-problem.
The simplest recursive functions for easier problems usually consist of one base case and one
recursive case. For more complicated problems, there may be more than one base case, or more than
one recursive case. You really need to understand a problem very well to decide if you need multiple
base cases or recursive cases to solve it. The number of these cases is determined by the problem
you are solving, not the recursive function you are writing.
1 + 2 + 3 + · · · + N = (1 + 2 + 3 + · · · + (N − 1)) + N
In other words, we have broken the task of adding numbers from 1 to N into 2 steps: first, add the
numbers 1 to (N − 1); second, add N to the result of the first step. Clearly, adding up the numbers
from 1 to (N − 1) is a smaller version of the same task of adding the number from 1 to N. If we had
some way to do that calculation, all we’d have to do is add N to the result. Fortunately, the function
we are writing is just such a function!
Let’s use the notation sum(N) to represent the sum of integers from 1 to N. We have to be careful
to use this notation only for N > 0, because otherwise it doesn’t make sense. With this notation,
we can also say sum(N − 1) is the sum of integers from 1 to N − 1 (as long as N − 1 > 0). By the
property we observed above, we can say that sum(N) = sum(N − 1) + N, as long as N − 1 > 0. From
here it is a short exercise to write a recursive function in Python:
def sum ( N ):
if N == 1:
# base case
return 1
else :
# recursive case
return sum (N -1) + N
The base case comes from the knowledge that sum(1) = 1; the recursive case comes from the
equation sum(N) = sum(N − 1) + N.
Notice that we were primarily engaged in understanding the task (summing a bunch of integers),
and we used a bit of basic math to describe the properties of the task. When we finished, we translated
14.3 More Examples 119
the math into Python. The recursive function, therefore, describes a mathematical truth about the
task. One only has to understand the language of Python and the nature of addition to see that the
program is correct.
Even or Not?
Here’s a slightly different example. We can write a recursive function to determine whether a given
positive integer is even or not. There are better ways to do this task, but it provides an interesting
example for us to discuss.
Suppose we are given a positive integer X, and suppose for the sake of example that X > 2 (we
ignore negative numbers). There is a property of even numbers that is extremely useful: if X is an
even number, then so is X − 2; likewise, if X is not even, then X − 2 is also not even. In other words,
we have identified a relationship between the numbers X and X − 2: they are either both even, or
both odd.
We can make this relationship work for us in the form of a recursive function. Our function will
return the boolean value true if a given X is even, and false if X is odd. We also know that two is an
even number, but one is not even:
def is_even ( X ):
# first base case
if X == 1:
return False
# second base case
elif X == 2:
return True
# recursive case
else :
return is_even (X -2)
This example has two base cases and one recursive case. Notice that X is the input to the function,
and that the recursive step transforms the task into a subtask about the value X-2. The recursive case
decides whether X-2 is even or not, and there is no combination here because the answer for X-2 is
the same as the answer for X.
Again, we motivated the function by explaining a relationship between X and X − 2. The function
describes this relationship in Python. It is a matter of understanding something about numbers, and a
little Python to see that the program is correct.
120 Recursion
We will demonstrate these ideas using the drinking song example. First we need to identify the
base case, which is when there are zero beers remaining on the wall, in which case, thankfully, no
more verses need to be sung. In our program we test whether N <= 0 and if true, print “All Done”.
This is the only base case.
To write the recursive case, we need to identify the main task. The “main task” is to display
the verses of a song; the number of verses depends on the integer N that is input to the function
drinking_song(). We can transform the “main task” into a simpler one by “drinking” one of the
beers on the wall. This transformation gives us a “sub-task” in which we have to display N − 1
verses, because now there are only N − 1 bottles of beer.2 While it may seem that N − 1 verses is
not a lot simpler than N verses, it is a step in the right direction, and it’s really all we need. We can
make a recursive call to perform the “sub-task” and we can assume this is done correctly.3 If we
can assume that the N − 1 verses will be correctly displayed, then we can solve the “main task” by
displaying exactly one verse before we display the N − 1 verses. We are combining the printing out
of one verse with the solution to the “sub-task” (the printing out of the remaining verses) to produce
a solution to the “main task”.
You can always get a start on defining a recursive program by typing the following template:
def < function name >( < parameters > ):
if < base case test >:
< return or perform solution to base case >
else :
< combine recursive call with something about
the data to return or perform solution to ‘‘ main task ’ ’ >
}
Then you fill in the blanks. Not all at once, and maybe you will make some revisions as you go.
on many occasions.
3 The word “assume" is not used here in the sense that we don’t know, or can’t prove something. We use “assume” to
mean that we will get around to making sure it is true, after we finish what we are currently doing.
122 Recursion
do their job (possibly with a long chain of their own sub-assistants) and think about how you can use
the assistant’s answer to solve your original problem.
Learning Objectives
the expected output if a negative number is entered would be an error message indicating that the
input radius needs to be positive. But if we input -4 and get an answer of -50.27 instead then our
testing detected a fault — the program didn’t respond to the invalid input as expected! We now have
to determine why the fault occurred, and repair it. Sometimes this can just be done by looking at
the program and noticing where we made a mistake. All too often, however, the reason for a fault
occurring in a program is not obvious. The larger and more complex a program is, the less obvious it
becomes what the cause of a fault is likely to be. We will look at various debugging techniques that
can help us find and repair faults.
15.2 Testing
The goal of the testing process is to detect all of the faults in some code. To achieve this goal, we
start by coming up with a set of test cases. A test case consists of a specific input to the code, or a
usage scenario performed under specific conditions. When we test code, we want to generate a set of
test cases that is:
1. as small as possible; and
2. has a very high likelihood of uncovering every fault that might exist in the code.
It is worth noting at this point that it is rare to test an entire program all at once with a single
set of test cases. More frequently, we generate test cases for an individual function we have written
to make sure it is correct before moving on and writing other code that uses that function. This
is because trying to test an entire program all at once is quite unmanageable (the set of test cases
becomes much too large) for all but the smallest programs. If we test a program one function at a
time, we can assume that previously written functions are correct when testing our most recently
written function, which speeds test case generation. That said, the process of testing is essentially
the same whether we are testing just one small function of a larger program, or an entire program.
This still leaves the question of how to actually identify test cases for a program or function.
There are two approaches we can use to generate test cases: white-box testing, and black-box testing.
may test it by feeding input into the box, and receiving output from the box and checking whether it
is correct, but you cannot see the code inside the box. Test cases are generated by considering the
different inputs that might be provided to the code including common, rare, unusual, and erroneous
inputs.
def is_divisible_by_7 ( numbers ):
"""
This function returns true if the list numbers
contains a number that is divisible by 7 ,
and returns false otherwise .
numbers : list of numbers to check
return : if list contains a number divisible by 7
"""
for i in numbers :
if i % 7 == 0:
return True
return False
As we have said, in black-box testing we are not supposed to look at the code for the algorithm when
generating test cases. So we must generate test cases using only the following knowledge:
def is_divisible_by_7 ( numbers )
"""
This function returns true if the list numbers
contains a number that is divisible by 7 ,
and returns false otherwise .
numbers : list of numbers to check
return : if list contains a number divisible by 7
"""
We now write test cases by considering typical, rare, and erroneous inputs and their expected outputs.
Below is a list of test cases we might come up with. Though it is not normally necessary, we have
tagged each test case with the label “common”, “rare”, or “erroneous” so you can better understand
our thinking.
126 Testing and Debugging
those paths at least once. If our code contains an if-statement, then we should write at least one test
case that causes the if-statement’s condition to be true, and one test case that causes it to be false. If
we have a loop in our code, we should write a test case that causes the loop to execute zero times,
another that causes it to execute one time, one that causes it to execute many times, and one that
causes it to execute the maximum number of times (if applicable).
Let’s write some test cases for the is_divisible_by_7 function from the previous section.
Here’s the code again, followed by the test cases:
def is_divisible_by_7 ( numbers ):
"""
This function returns true if the list numbers
contains a number that is divisible by 7 ,
and returns false otherwise .
numbers : list of numbers to check
return : if list contains a number divisible by 7
"""
for i in numbers :
if i % 7 == 0:
return True
return False
Notice how some test cases cover multiple testing criteria (in this case, the number of loop iterations
and whether the if-statement condition is true or false)! This is completely fine, and is encouraged,
because it amounts to less work.
Also notice that all of the test cases we identified using the white-box method were also identified
using the black-box method in terms of just the inputs and expected outputs. It is well-known that
white-box and black-box testing are complementary methods. One will often identify the same test
128 Testing and Debugging
cases using either method, however, sometimes one method will helps us discover good tests that the
other method does not. There is no one right test case generation method to use; we can use one, or
the other, or both. In any case, the goal is to generate a good set of tests that is highly likely to find
all of the faults that might be hiding in the code.
Input(s): []
Output(s): False
Reason: Cause the for-loop to be executed 0 times.
# call with empty list argument
result = is_divis ible_by_ 7 ([])
# expected output : False
if result == True :
print ( ’ Error : returned True when given empty list . ’)
print ( ’( no items divisible by 7) ’)
Input(s): [7]
Output(s): True
Reason: Cause the for-loop to be executed one time; cause the if-statement to be true.
# call with single - item list containing one element divisible by 7
result = is_divis ible_by_ 7 ([7])
# expected output : True
if result == False :
print ( ’ Error : returned False when given [7] ( divisible by 7) ’)
15.3 Debugging 129
Input(s): [1,2,7,3,5]
Output(s): True
Reason: Cause the for-loop to be executed many times; cause the if-statement to be true and
false (on different loop iterations).
# call with many - item list containing one element divisible by 7
result = is_divis ible_by_ 7 ([1 ,2 ,7 ,3 ,5])
# expected output : True
if result == False :
print ( ’ Error : returned False when given [1 ,2 ,7 ,3 ,5] ’)
print ( ’ (3 rd item divisible by 7) ’)
Notice that when faults are reported, we always indicate what the fault was and why it was wrong.
Also notice that nothing is reported when faults are not detected. The only exception is that we might
print a “test complete” message when the test driver is concluded so that we can be certain that the
test driver ran to completion and reported no faults.
15.3 Debugging
Once we have identified a fault, we have to correct it. We shall briefly discuss three debugging
strategies here. These strategies can be used regardless of how the fault was detected, whether it was
through the normal course of use of the program, or through a more formal testing process like we
have described in the previous section.
15.4 Summary
Of course, testing and debugging doesn’t end when the faults are fixed. Code that has been modified
to fix faults should be tested again to make sure no new faults were introduced in the course of fixing
the previously detected faults.1 This makes testing and debugging an iterative process (illustrated in
Figure 15.1) and is also why investing the time to write a good test driver will save you time in the
long run, since, if done right, re-testing is a simple matter of re-running the existing test driver.
One final note: make sure you come to class to see the demonstrations of hand-tracing and
integrated debuggers! These are highly interactive processes that we cannot easily show using text
and static pictures.
code
no update
tests?
yes
test case
genera-
tion/update
write/update
test driver
run test
driver
faults yes
debug
detected?
no
16 — Search Algorithms
Learning Objectives
16.1.1 Collections
The term collection refers to any data structure that stores one or more data items — this includes
lists, dictionaries, and arrays. We say that we perform a search on a collection to find a specific item.
1 Do you remember the difference between problems and algorithms from Section 1.3? If not, go back and remind
yourself.
134 Search Algorithms
Table 16.1: The search keys typically designated for different types of data.
Dead and Loving It” in our collection of movie ratings, we would use the string ’Dracula: Dead
and Loving It’ as our target key and then determine which (if any) data items in the collection
have the target key as their search key (which is the value of their ’movie_title’ dictionary key).
Membership
We might perform a search only to determine whether or not there is at least one data item in the
collection whose search key matches the target key. The result of such a search is a Boolean value —
true or false. Either the collection contains a data item whose search key matches the target key, or
it doesn’t. This type of search is called a membership search. We want to know whether there is a
member of a collection that matches the target key.
Retrieval (Look-up)
On the other hand we might perform a search because we want to retrieve the actual data item(s) in
the collection whose search key(s) match(es) the target key. This type of search is called a retrieval
search or a look-up. In this case, the result of the search is a new collection (e.g. a list) that contains
only the data items from the original collection whose search keys match the target key.
11 s = search key of i
12 if s == target_key :
13 add i to matches
14
15 return matches
This version assumes that keys are not unique. It would still work if keys were unique, but think
about how we could improve this algorithm if keys were, in fact, unique.
The following Python code implements a linear look-up search which works when the collection
being searched is a sequence (i.e. list, tuple, array) of numbers or strings, and when the data items
themselves are the search keys (i.e. when the data items are numbers or strings), again assuming that
keys are not unique.
def linear_search (C , target_key ):
"""
a linear retrieval search for all instances of target key
C : a sequence of numbers or strings
target_key : the target key for the search
Returns : a list containing items from C whose search
keys match target_key
"""
matches = []
for i in C :
if i == target_key :
matches . append ( i )
return matches
The key thing to remember (no pun intended) is that a linear search might examine every data
item in the collection, for example, in the case where the collection contains no data item whose
search key matches the target key! Linear search might stop early if it finds what it is looking for,
but that does not affect whether it is a linear search or not. If there is a loop that examines every data
item in the collection, then you’ve got a linear search.
The advantage of linear search is that it is easy to write, and will work on any collection for
which you can write a loop to look at each data item. Unfortunately, it is also one of the slowest
searches that we know of. So, unless speed is not a concern (it almost always is, in practice), then
smarter searches that perform less work are desired.
The fundamental concept that makes binary search work is that because the data items to be
searched are in sorted order, we can examine one data item, and immediately remove half of the
remaining data items from consideration without examining them. How is this possible? Consider
the following array of numbers in which we want to search for the number 42:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
-10 -4 1 2 4 7 12 23 34 37 42 45 48 62 71
In binary search, the first thing we do is examine the middle data item of the sorted sequence of
items — in this case the data item at array offset 7. We examine the data item at offset 7 and find
that it is 23. Drat... this isn’t the number we are looking for. But, because the array is sorted, we
immediately know that the data items at offsets 0 through 6 also cannot be 42, because they must all
be smaller than 23! So we need continue searching only in the right half of the array because 42 is
larger than 23, and therefore must be somewhere between offsets 8 and 14 (if it is in the array at all):
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
-10 -4 1 2 4 7 12 23 34 37 42 45 48 62 71
To continue the search, we examine the middle item of those items that are still to be searched
(those items shaded in blue, above). The middle item of those still to be searched is at offset 11.
We examine it, and find that it is the number 45. This, again, isn’t what we were looking for. But,
because the array is sorted, we immediately know that if 42 is in the array, it cannot be between
offsets 12 and 14, so we have eliminated half of the remaining data items by examining the number
at offset 11. Now the only offsets that need to be searched are offsets 8 through 10:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
-10 -4 1 2 4 7 12 23 34 37 42 45 48 62 71
From this point, the search continues in the same way — we examine the middle item of those items
that still need to be searched. At this time, that item is the one at offset 9. We examine offset 9 and
find that it is the number 37. Still not what we’re looking for. But since the array is sorted, we know
that data item 42 cannot be at offset 8, because 42 is larger than 37. So now things look like this:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
-10 -4 1 2 4 7 12 23 34 37 42 45 48 62 71
We again continue by searching the middle item of those that still need to be searched. This time
there is only one such item, the item at offset 10. We examine the item at offset 10, find that it is
equal to our target key, and the search is complete!
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
-10 -4 1 2 4 7 12 23 34 37 42 45 48 62 71
Items eliminated Items examined Items still to be searched Item examined and found!
Now think about this — something really interesting has happened here. Because the items were
sorted we were able to find an item matching the target key by examining only four out of the 15
items (the items shaded red were never looked at)! Because we are able to eliminate half of the
remaining items every time we examine one data item, binary search is extremely fast. We will
discuss this in more detail in class, but, by way of example, if you have a collection with 1,000,000
data items, then a binary search will look at no more than 20 of the data items in the collection.
Compare that to a linear search which may have to look at all 1,000,000 items!
The most natural way to write the binary search algorithm is as a recursive algorithm. Here is
the pseudocode for a binary search for membership:
1 Algorithm BinarySearch (S , target , start , end )
2
3 # a binary search for membership
4 # S : a collection of data items ordered by their search keys
5 # target : the target key
6 # start : first offset of S to be searched
7 # end : last offset of S to be searched
8 # return : true if S contains an item whose search key
9 # matches the target , false otherwise
10
11 if ( end < start ):
12 # base case #1: the range of offsets to be searched
13 # has no items in it , so we must conclude the target
14 # is not in the collection .
15 return false
16
17 # find the middle of the array offsets to be searched
18 mid = ( start + end ) // 2 # note : integer division !
19
20 if search key of S [ mid ] == target :
21 # base case #2: item was found !
22 return true
23
24 else if search key of S [ mid ] < target :
25 # if item examined is smaller than target key , it must
26 # be in the right half of the remaining items , so
27 # recursively search there .
28 return BinarySearch (S , target , mid +1 , end )
29
16.4 Comparison and Summary of Linear Search and Binary Search 139
30 else :
31 # otherwise the item examined is larger than the
32 # target key , so search the left half of the remaining items .
33 return BinarySearch (S , target , start , mid -1)
If we want to search an entire collection, we would invoke the algorithm with an argument of 0
for start, and an argument of N − 1 for end, where N is the number of items in the sequence. In
this algorithm, start and end keep track of the offset for the start of the range to be searched (the
area shaded blue in the previous example), and the end of the range to be searched, respectively. The
variable mid is always calculated to be the midpoint between start and end. Finally, there is an
if-statement to compare the search key of the item at offset mid against the target key to disqualify
half of the current offset range from consideration in the search. It is the splitting of the list in half,
and disqualifying half of the list, that makes this a binary search. Also note that the size of the
collection does not change as the algorithm proceeds, only the set of items in the collection under
consideration changes (the items between offsets start and end, inclusive).
The algorithm as presented assumes that the collection is sorted in increasing order. If the
collection were sorted in decreasing order, then the only change required is that the less-than
operators would have to be changed to greater-than operators.
Finally, here is the binary search for membership implemented in Python. It will work when C is
any sequence of numbers or strings:
def bin_search (C , target_key , start , end ):
"""
a binary search for membership
S : a collection of data items ordered by their search keys
target : the target key
start : first offset of S to be searched
end : last offset of S to be searched
return : true if S contains an item whose search key
matches the target , false otherwise
"""
if end < start :
return False
if C [ mid ] == target_key :
return True
elif C [ mid ] < target_key :
return bin_search (C , target_key , mid +1 , end )
else :
return bin_search (C , target_key , start , mid -1)
collection, in particular, in the case where the collection contains no item whose search key matches
the target key. For linear search it does not matter if the collection is sorted or not.
Binary search requires that the items in the collection be in sorted order. Binary search reduces
the number of items to be searched by half with each item examined, resulting in the examination of
only a very few items, even if there are no items matching the target key.
2 To be absolutely correct, we must say that binary search will never examine all of the items for collections with three
or more data items. For collections of only one item, obviously that one item will have to be examined. For collections of
two items, it is possible that both items are examined. For three items or more, at least one item will be eliminated without
examination.
Introduction
Insertion Sort
Divide-and-Conquer Sorts
Merge Sort
Quick Sort
17 — Sorting Algorithms
Learning Objectives
17.1 Introduction
Any sequence of data items (e.g. Python lists, arrays) where a greater-than/less-than/equal rela-
tionship can be established between each pair of items can be sorted. Sorting a sequence means
to rearrange the sequence, or create a new sequence, so that all of the data items in the original
sequence are in increasing (or decreasing) order. There are many cases when designing a program
that you may need to sort a list of things, for example, sorting a list of names into alphabetical order,
or sorting expenditures in decreasing order of cost.
142 Sorting Algorithms
Again we have to remember the important distinction between problems and algorithms. There
are dozens of different sorting algorithms, each of which solves the same problem — that of putting
the items in an input sequence in sorted order.
Sorting algorithms are widely implemented as part of most programming languages or are
available as add-ons (libraries, modules). It is rare for us to have to write a sorting algorithm by
ourselves. Instead we usually just call the appropriate existing sorting function. However, we
still study sorting algorithms because each one has different strengths and weaknesses. Thus, it
is important to understand how the different sorting algorithms work, which sorting algorithm is
implemented by the sorting function being used, and whether it is appropriate for your data.
If you only care about having a sorted list, and not the speed at which that list is sorted, then it
does not matter which sorting algorithm you choose; in the end, they will all sort the list. But if your
data has special properties, there may be sorting algorithms that work faster on it than others, or,
perhaps more importantly, that you will want to avoid because they are really inefficient for your
data. In this course, we will cover three sorting algorithms: insertion sort, merge sort, and quick sort.
Insertion sort is a basic sorting algorithm that is very fast for very short sequences. Merge sort is
a recursive algorithm that is a good general-purpose sort in that it performs well in all situations, but
is not optimal for every situation. Quick sort is another recursive algorithm that outperforms merge
sort most of the time, but is much worse in a few specific situations. Both merge sort and quick sort
are examples of algorithms that were designed using the divide and conquer approach to problem
solving, which we will introduce prior to discussing the details of the algorithms.
0 1 2 3 4 0 1 2 3 4
U: 2 5 4 1 3 S:
We now repeatedly remove the first data item in U and insert it into the appropriate spot in S,
beginning with the 2:
17.2 Insertion Sort 143
0 1 2 3 4 0 1 2 3 4
U: 5 4 1 3 S: 2
Then the 5:
0 1 2 3 4 0 1 2 3 4
U: 4 1 3 S: 2 5
Now the 4:
0 1 2 3 4 0 1 2 3 4
U: 1 3 S: 2 4 5
Note how the 5 was shifted to the right to make room for the 4 in S. This is important to note because
it takes time. It’s not a big deal here because only one data item had to be shifted, but consider a
different scenario where we insert a negative number into an S that contains one million positive
numbers — you’d have to shift all one million positive numbers over to make room for the negative
number! The sort continues by removing the 1 from U and inserting it into S:
0 1 2 3 4 0 1 2 3 4
U: 3 S: 1 2 4 5
Observe how all of the data items already in S had to be moved over to make room for the 1. Finally,
we insert the 3, and the sorting is complete:
0 1 2 3 4 0 1 2 3 4
U: S: 1 2 3 4 5
Remember we said earlier that insertion sort is good for short sequences? This is precisely because
of the work involved in shifting data items to make room for new ones. For longer sequences,
potentially many more data items need to be shifted when small data items are inserted than for short
sequences.
Here’s one way of implementing insertion sort in Python:
144 Sorting Algorithms
def insertion_sort ( U ):
"""
creates new sequence containing sorted data of U where
data is sorted using insertion sort
U : sequence to sort
return : new sequence where U is sorted
"""
return S
Insertion sort is very fast on really short sequences compared to other sorts. However, in general,
for arbitrary-length sequences, it is one of the slower sorts. Merge sort and quick sort, which we will
discuss in the following sections, are generally much faster sorts.
Most sorts are more efficient when implemented using arrays rather than lists. It is possible,
for example, to implement insertion sort where the input sequence is an array, and no second
array is used for S. This is called an in-place sort. Instead of using separate arrays for S and
U, we store S on the left side of the input array, and U on the right side of the input array.
This works because the sum of the lengths of S and U is always constant — whenever an
item is removed from U, it is added to S. Initially the entire array is U (because S is empty).
As S grows, more of the array is used for S and less for U, until finally, at the end, the entire
array is S. The other thing we would have to do is to manually shift items one-by-one by
when necessary, instead of relying on the insert method of a list to do this for us. See if
you can write an insertion sort where the input is an array without using any other arrays. Do
you accept the challenge?
17.3 Divide-and-Conquer Sorts 145
# divide
S1 = first half of S
S2 = second half of S
# conquer !!!
S = merge ( S1 , S2 )
146 Sorting Algorithms
return S
Now it should be easy to see that everything hinges on what is going on in the merge function of
the “conquer” step (the details of which we abstracted away in the above pseudocode). Intuitively,
if we have sorted sequences S1 and S2 we can “merge” them together so that we obtain a sorted
version of the original sequence S. We will now see an example of merging two sorted sequences S1
and S2 into a single sorted sequence S. Suppose S1 = [2, 3, 4, 11, 12], S2 = [0, 1, 6, 7], and S is empty:
0 1 2 3 4 0 1 2 3
S1 : 2 3 4 11 12 S2 : 0 1 6 7
S:
Since S1 and S2 are already sorted, we know that the first item of S1 is the smallest item in S1 and the
first item in S2 is the smallest item in S2 . That means that one of these two items must be the first
item in S, in particular, the one that is the smallest, which happens to be the 0 from S2 . So the first
step of the merge operation is to remove 0 from S2 and append it to the end of S:
0 1 2 3 4 0 1 2
S1 : 2 3 4 11 12 S2 : 1 6 7
S: 0
The next item of S must, again, be the smaller of the first item of S1 and the first item of S2 . This is
the 1 at the start of S2 . We remove the 1 from S2 and append it to S:
0 1 2 3 4 0 1
S1 : 2 3 4 11 12 S2 : 6 7
0 1
S: 0 1
Once again, the next item of S must be the smaller of the first item of S1 and the first item of S2 . This
is the 2 at the start of S1 . We remove the 2 from S1 and append it to S:
17.3 Divide-and-Conquer Sorts 147
0 1 2 3 0 1
S1 : 3 4 11 12 S2 : 6 7
0 1 2
S: 0 1 2
We continue in this fashion, repeatedly moving the smaller of the items at the start of S1 and S2 to the
end of S until one of S1 or S2 is empty. In this example, this occurs after moving 3, 4, 6, and 7 to S:
0 1
S1 : 11 12 S2 :
0 1 2 3 4 5 6
S: 0 1 2 3 4 6 7
At this point, we simply append the remainder of whichever of S1 or S2 is non-empty to the end of S:
S1 : S2 :
0 1 2 3 4 5 6 7 8
S: 0 1 2 3 4 6 7 11 12
Now S contains all of the data items in S1 and S2 , in sorted order! Here is how we might implement
the merge algorithm in Python:
def merge ( S1 , S2 ):
"""
combines two sorted sequences into single sorted sequence
S1 : sorted sequence to combine
S2 : other sorted sequence to combine
return : single sorted sequence of S1 , S2 combined
"""
# let S be an empty sequence
S = []
del S2 [0]
return S
Algorithm quickSort ( S )
# sorts sequence S using quick sort
# S - array of data items to be sorted
# return : sorted sequence of S
# conquer !!!
S = L + E + G // ( where + represents concatenation )
return S
We can already see that the conquer step is easy; we just need to paste together the already-sorted
sequences L, E and G because we know that everything in L is smaller than E and that everything in
E is smaller than G. Most of the work in quick sort is done in creating L, E, and G, in the first place.
Conceptually, this is very easy. We can initialize L, G, and E to be empty sequences, choose an item
of S (say, the first item) to use as the pivot p, and then just append each item of S to the appropriate
sequence. Here is how we might do that in Python1 :
# divide step of quick sort
L = [] # for items smaller than the pivot
E = [] # for items equal to the pivot
G = [] # for items greater than the pivot
p = S [0] # choose the first item as the pivot
for x in S :
if x < p :
L . append ( x )
elif x > p :
G . append ( x )
else :
E . append ( x )
Now all we need to do is recursively sort L and G, then concatenate L, E, and G to obtain a sorted
sequence.
Quick sort is usually faster than merge sort and insertion sort (except for very short sequences
where insertion sort wins). In class we will examine the type of inputs for which the performance of
quick sort becomes poor.
1 In practice, this is not a very efficient implementation of quick sort, but an efficient implementation using arrays is
beyond the scope of this course. If you decide to major in computer science, you’ll encounter the efficient implementation
in second year.
Introduction
Binary Numbers
Numbers vs Numerals
Representation of Binary Numbers
Converting from Binary to Decimal
Addition of Binary Numbers
Multiplication of Binary Numbers
Subtraction and Division
Converting from Decimal to Binary
Binary Addition and Multiplication: Con-
nections with Logic
Going Further with Number Representa-
tions
From Boolean Operators to Propositional
Logic and Beyond
Common Pitfalls
Learning Objectives
• describe what the binary number system is, using concepts of digits and bases;
• perform addition and multiplication on binary numbers;
• convert binary numbers to decimal numbers, and vice versa;
• understand the connection between binary numbers, circuits, and logic; and
• recognize the importance of logic to computer science.
18.1 Introduction
The design of electronic computers uses electronic circuitry to make the computer work. As we said
in an earlier reading, data is represented as numbers, and that is true at a certain level of abstraction.
Electronic engineers found it convenient and robust to represent data using voltages in electronic
circuits: a voltage is considered “high” if it is above a certain threshold, and “low” if it is below the
same threshold. The threshold itself depends on the design of the electronic device. This itself is an
abstraction that allows us to ignore exact voltage values.
The binary number system is built on the abstraction that the low voltage can represent the
quantity 0, and the high voltage can represent the quantity 1. This electronics design convention is
exposed to programmers in some languages by equating 0 with the boolean value false, and 1 with
the boolean value true. As a result of all these common ideas, the notions of computer programming
(data), digital circuit design (voltages) and Boolean logic are all intertwined.
The question for electronics designers is “how to design a circuit that implements data opera-
tions?” We know that computers can do things like arithmetic, but how do circuits carrying voltages
actually do that? In essence, there are circuit components (transistors) that affect voltages in circuits
in ways very similar to the kinds of operations we know as Boolean operators. So, for the electronic
152 Binary Number Systems and Logic
engineer, a knowledge of Boolean logic directly assists circuit design. Of course, logic is not the
only concern for electronics design: the physical constraints of electronic materials plays a big role
too: circuits are usually laid out on a 2D surface, and these circuits generate heat, which must be
managed. We won’t say more about this here, but that’s not because there’s nothing more to say.
The question for programmers is “How much do I need to know about Boolean logic to enhance
my programming skills?” as well as “What do I need to know about how the computer works to
make sure I am not missing anything important about my work?” Boolean logic is used in programs
in conditional statements (if-statements) and repetition constructs (loops, recursion), so it’s obvious
that we need them. Boolean logic is also the first and simplest approach to a branch of mathematics
that’s very important to Computer Science: formal logic. We need formal logic to clarify our thinking
about algorithms. In an introductory course such as this course, we don’t really need such tools, but
they become crucial as we move from introductory concepts to advanced concepts. Formal logic is
used in the design of digital circuits, the verification of software correctness, and as an approach
to formal reasoning by computers in artificial intelligence. There is also a programming language
called Prolog (“PROgramming in LOGic”) built entirely from the idea that formal logic is actually a
model of computation.
Notice that there is no numeral for the number ten. In fact, we don’t need one because we can
represent the quantity ten as a combination of more than one numeral, namely, with 1 tens plus zero
ones: 10 = 1 × 101 + 0 × 100 .
Notice that integer division by 10 is easy in decimal: we simply remove the numeral on the right.
Try it! Multiplying by ten is equally easy: we append the numeral 0 onto the right end of the number.
Try this too! Remember, this only works for integer division, but it’s a handy trick to know.
There’s no real need to represent numbers with exactly ten numerals. It is simply convenient for
us humans because we happen to have ten fingers. Had things turned out differently, humans might
have six fingers on each hand, and might have developed a number system with twelve numerals. It
would be no more difficult for us than what we have now (though it might be a bit more trouble if
we were descended from centipedes or millipedes). In fact, English words “eleven” and “twelve”
are evidence that at least some cultures used a base-twelve number system in the past. In French,
linguistic evidence points to a base-16 number system (numbers represented with 16 numerals).
The binary number system, also known as the base-2 number system, uses only two numerals,
namely the Arabic symbols 0 and 1. In computers, binary numerals are called bits, which is short for
binary digit. Because these same numerals are used in base-10, this immediately causes confusion.
How do we tell the difference between the base-10 number 111 (the quantity one hundred and eleven)
and the base-2 binary number 111 (the quantity seven)? To avoid this confusion, we will adopt the
following convention. Decimal numbers are written normally; binary numbers will be written with
“0b” as a prefix. Using this convention, 111 is the base-10 number one hundred and eleven, and
0b111 is the binary number seven, and 7 is the base-10 number seven. The “0b” prefix contributes
no quantitative information to the number; it is simply an annotation to remind us we’re dealing with
a binary number.
A binary number like 0b1101 represents a quantity in a sequence of binary digits. Literally, this
sequence means “1 eight plus 1 four plus 0 twos plus 1 one”. The words “eight, four, two, one” are
powers of two, and the bigger a number is the more powers of two we will need to represent it.2
Notice that this is no different from base-10 except that there are fewer numerals, and the base of
the powers that are associated with each numeral’s position is changed from 10 to 2. The powers of
two that we use for each position, starting with the right-most position are 20 = (one), 21 (two), 22
(four), 23 (eight), and so on. For a positive integer with k binary digits, we’ll need the powers of two
from 20 to 2k−1 .
And now for a little test. If at this point you understand the difference between numerals and
numbers, and between decimal (base-10) and binary (base-2) number representations, you will
appreciate this joke:
“There are 10 kinds of people in the world: those who know binary and those who don’t.”
Of course, we would have written “0b10 kinds of people,” but that obliterates the tiny amount of
humour in the joke.
In the same way that multiplying and dividing by ten was easy for decimal numbers, multiplying
and dividing by two is easy in binary. To divide by two, just remove the numeral on the right. For
example, if we take the number thirteen and divide by two (integer division) then the answer is six.
Let’s see how this works in binary. In binary, the number thirteen is 0b1101, that is, 1 eight, 1 four, 0
twos, and 1 one. If we remove the right-most numeral, we are left with 0b110, which is 1 four, 1 two
and 0 ones, which adds up to six! Multiplying by two is equally easy: we append the digit zero to
2 The base of the powers is 2, hence base-2!!
154 Binary Number Systems and Logic
the right hand side. For example, we can multiply the number 0b101 (five) by two by appending a
zero to get 0b1010, which is 1 eight, 0 fours, 1 two, and 0 ones, which adds up to ten.
Table 18.1 lists binary and decimal representations for a small set of integer quantities.
Table 18.1: Some 8 bit binary numbers. The space between the sets of 4 bits is used to help
readability, much the same way that commas are used in decimal numbers.
1 × 64 + 0 × 32 + 1 × 16 + 0 × 8 + 1 × 4 + 1 × 2 + 0 × 1 = 86
Note that as a shortcut, you can ignore the bits that are zero:
1 × 64 + 1 × 16 + 1 × 4 + 1 × 2 = 86
The only real difficulty here is calculating the powers of 2, but the first eight or nine powers of
two are memorized easily enough.
The first three facts are very straightforward because they involve adding something to zero.
Zeroes are zeroes in every number representation system, and adding zero leads to no change, just
like in decimal. The last fact is more interesting. We get two bits as the result, because of carrying.
In decimal 1 + 1 = 2, and does not cause a carry into the tens column like, say, 6 + 6 = 12 would.
But in binary, 0b1 + 0b1 = 0b10 and does cause a carry. There’s a carry of 1 to the next numeral
position (the twos column, in this case). Carrying in binary works exactly the same way it does in
decimal.
The algorithm for addition in binary is exactly the same as the one you learned in third grade for
addition in decimal. The only difference is that in decimal you had to memorize a lot more facts (55
of them: 1 + 1 = 2, 1 + 2 = 3,1 + 3 = 4,1 + 4 = 5, . . . ).
When we add binary numbers, we line the numbers up so that the right most digit is in the same
column. Then we do addition in columns from right to left, using the 4 binary addition facts above.
If we carry a 1 to the next column, we include it in the addition of that column, as normal.
Here’s an example binary addition, we just dropped the 0b annotations here to reduce clutter.
The columns being added in each step are shown in red, carries are shown in blue.
1 11 111 111
111 111 111 111
⇒ ⇒ ⇒
+ 11 + 11 + 11 + 11
0 10 010 1010
one’s column two’s column four’s column eight’s column
In this next example we show an example of decimal addition and an example of binary addition
where the pattern of adding and carrying is identical. This illustrates that addition in both systems of
number representation is identical, except for the particular set of addition facts that are used. As in
the previous example, the columns being added in each step are shown in red, and carries are shown
in blue.
1 1 1 1 1 1
628 628 628 628
decimal: ⇒ ⇒ ⇒
+ 507 + 507 + 507 + 507
5 35 135 1135
1 1 1 1 1 1
101 101 101 101
binary: ⇒ ⇒ ⇒
+ 101 + 101 + 101 + 101
0 10 010 1010
Algorithm DecimalToBinary ( x )
x : a decimal number to be converted to binary
Returns : binary representation of x
x r b Comments
27 – – Initial value of x before the while loop
13 1 0b1 After 1st loop iteration; 27/2 = 13, remainder 1.
6 1 0b11 After 2nd loop iteration; 13/6 = 6, remainder 1.
3 0 0b011 After 3rd loop iteration; 6/2 = 3, remainder 0.
1 1 0b1011 After 4th loop iteration; 3/2 = 1, remainder 1.
0 1 0b11011 After 5th loop iteration, 1/2 = 0, remainder 1.
At the conclusion of the loop, since b is not empty, the answer is b = 0b11011. It’s easy enough to
check our answer by converting 0b11011 back to decimal:
0b11011 = 1 × 24 + 1 × 23 + 0 × 22 + 1 × 21 + 1 × 20
= 16 + 8 + 2 + 1
= 27
This algorithm can be adapted to convert numbers in any base back to decimal. Simply change
the algorithm so that the base of the input number is used in the division and remainder operations in
place of the number 2.3
interesting implications. For one, it means that computer hardware can be engineered to perform
arithmetic using AND and OR operations, which are precisely the kinds of things that fundamental
electronic components are good at! In the following tables, we illustrate the similarities between
addition/OR and multiplication/AND, respectively (all table numbers are binary, 0b prefix omitted).
In examining the above table, note the correspondence between the binary value 0b0, and the Boolean
value false; likewise the binary value 0b1 and the value true.
variation on “signed magnitude” to represent positive and negative integers. If you are interested in
reading further about such representations, Google the “one’s complement” and “two’s complement”
representations. The two’s complement representation is what is used to represent integers in most
modern-day laptop and desktop computers. This is covered in later computer science courses, too.
But wait... you said... (optional reading, not covered on the exam)
Yes, we said here that integers are represented in computers by sequences of bits with fixed
length, which means that we cannot represent numbers larger than a certain quantity, and we
said back in Section 2.1.3 that there is no limit to the quantity that we can store as a Python
integer. Here’s the thing: both are true. Python integers are not stored with a fixed number of
bits, but instead use an entirely different implementation called arbitrary precision integers
and arithmetic is performed using arbitrary precision arithmetic.
The basic idea of arbitrary precision integers is that numbers are represented as strings of
characters ’0’ through ’9’. The advantage of this is obvious: any number can be stored
no matter how big it is because strings can be any length we want. The disadvantage is
that arithmetic cannot be performed using the computer’s built-in arithmetic hardware, and
instead has to be done in software, which results in it being a little slower. Most common
programming languages (Python being a notable exception) store integers using a fixed
number of bits.
One final note: integers stored in numpy arrays are not like standard Python integers. Because
of the nature of arrays, these are stored using a fixed number of bits, which is why numpy
arrays have dtype’s like int64 (64-bit integers), int32 (32-bit integers), etc.. This is one
(but not the only) reason why numpy arrays offer a speed advantage over lists.
with a formal, valid proof. It is worthwhile to mention that the predicate calculus, in combination
with a specific formal reasoning technique, can be used as the basis of a programming language, as
capable as Python or any other programming language.
The main point of this section is to point out that there are very deep ideas below the surface of
the material we’ve been studying. These are covered in later computer science courses.
19 — Computer Architecture
Learning Objectives
• describe the differences between a fixed function and a von Neumann architecture
computer;
• provide definitions for bit and byte;
• describe the organization of main memory in a computer;
• explain what a memory address is, and what it is used for;
• describe the basic function of the central processing unit, arithmetic logic unit, and
control unit of a computer;
• explain the purpose of the instruction pointer register and instruction register; and
• describe the steps of the machine cycle algorithm.
CPU
Main Memory
ALU
Bus
Control
Peripherals Unit
Figure 19.1: Conceptual diagram of the von Neumann architecture. We’ll talk about all of the
components shown here – main memory, CPU, and peripherals – in sections 19.2 through 19.6.
performing the task. Remember that, prior to this innovation, computers were hard-wired to perform
only one task; essentially computer programs were hardware. By treating computers programs
as data instead, and storing them in the same way and in the same place as data, a von Neumann
architecture computer is able to perform different tasks simply by storing different programs in
memory as software. It seems obvious to us now, but back in 1945, this was a seriously big deal.
Figure 19.1 shows a conceptual diagram of the von Neumann architecture. In this diagram the
lines (with and without arrows) indicate bus lines. A bus is a set of wires connecting the components
of a computer; a bus is used to send data (in the form of electrical impulses along wires) between the
components. You know all those thin lines you see on your computer’s motherboard? Some of those,
at least, are the bus.
Address Data
000 1101 1001
001 0010 0101
002 1001 0000
003 0000 0000
004 1100 0010
005 1100 1100
006 0101 0101
007 1010 1010
..
.
is the instruction register (IR). Both of these registers are key to the function of the CPU and we will
describe them in the next section.
19.5 Fetch-Decode-Execute
Programs that are running are stored in main memory. Because the program’s instructions are in
memory, the CPU’s job is very simple. The CPU just repeatedly performs the machine cycle. This is
an algorithm that has three steps: (1) Fetch; (2) Decode; and (3) Execute. A CPU performs these
three steps over and over and over again to run a program.
The instruction pointer (IP) that we mentioned in the previous section is a special CPU register
that contains the memory address of the next program instruction to be executed. In the fetch step, the
CPU will send a signal to main memory asking for whatever value is stored at the memory address
contained in the IP register. That value is sent back from main memory and stored in the CPU’s
instruction register (IR). At the same time, the value in the IP is updated to contain the memory
address of the next instruction in the program, so it can be retrieved in the next repetition of the
machine cycle. In this way, the IP always contains the memory address of the next instruction to be
fetched.
In the decode step, the CPU decodes the instruction stored in the IR. Decoding means that
the instruction is used to enable appropriate circuits in the CPU, and sometimes the ALU, that are
required to perform the instruction. Different instructions are encoded using different sequences of
bits. For example, the addition operation is represented by one sequence of bits, and the subtraction
operation by another. The sequence of bits in the IR will also contain an encoding of which registers
should be used in the operation (if any). For example, if the instruction is an addition instruction,
there will be bits in the instruction stored in the IR that indicate which CPU registers contain the
numbers to be added.2
2 You might be wondering how such numbers would get into the CPU registers in the first place. A load operation
19.6 Peripheral Devices 165
The execute step simply performs the operation indicated by the decoded instruction, by activating
the circuits enabled in the decode step. If the instruction requires that data be sent to, or retrieved
from, main memory then the instruction will be performed by the control unit of the CPU. Otherwise,
the instruction is mathematical or logical in nature and is performed by the arithmetic logic unit. In
the case of the example of an addition instruction in the previous paragraph, the control unit of the
CPU would activate circuits to send the contents of the two registers indicated in the IR to the ALU
and ask it to add them. Then the ALU would send the result back to the CPU which would store it in
a third register. A subsequent instruction might then cause the result of the addition to be moved
back to main memory, or the result might be held in the register to be used later by a subsequent
instruction.
complexity and subtlety at this point, but we want you to know that there is much more to it, which
you can and will study if you continue into second and third year computer science classes.