Cmpt142 Readings
Cmpt142 Readings
to
Computer Science
for Engineers
Jeff Long
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.
Much of this work was written primarily by Mark Eramian as course readings for CMPT 141. To
produce this document, I have pulled heavily from that material, with some reorganization, editing,
and additional content as needed. I would also like to thank Brittany Chan and Michael Horsch who
were also deeply involved with producing materials for our first-year computer science program.
Contents
1 Computing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.1 What is Computing? 15
1.2 Algorithms 15
1.3 Computer Science 16
1.4 Brief History of Computing 17
4 Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.1 Sequences 39
4.1.1 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.1.2 Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.1.3 Tuples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.1.4 Sequence Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.2 Slicing and indexing 42
4.2.1 Indexing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.2.2 Offsets from the End . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.2.3 Invalid Offsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.2.4 Slicing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
5 Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.1 Functions and Abstraction 47
5.2 Calling Functions 48
5.2.1 Functions as Expressions: Obtaining/Using a Function’s Return Value . . . . . . . . . 49
5.2.2 Calling Functions with No Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.2.3 Functions That Do Not Return a Value: Procedures . . . . . . . . . . . . . . . . . . . . . . . 50
5.2.4 More Built-In Python Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.3 Objects and Method Calls 52
5.3.1 Calling Methods in Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.3.2 Mutable vs Immutable Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.3.3 Useful string methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.4 Programming Languages Are Not Toaster Ovens 54
6 Creating Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.1 Defining Functions and Parameters: The def Statement 57
6.1.1 Functions that Perform Simple Subtasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
6.1.2 Functions that Accept Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
6.1.3 Returning A Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
6.1.4 Returning Nothing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
6.1.5 Defining Before Calling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
6.1.6 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
6.2 Variable Scope 61
6.3 Console I/O vs Function I/O 63
6.4 Documenting Function Behaviour 63
6.5 Generalization 64
6.6 Cohesion 65
7 Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
7.1 Modules: What Are They and Why Do We Need Them? 67
7.2 How to Use Modules 67
7.3 What Other Modules Are There? 68
7.4 Finding Module Documentation 70
8 Control Flow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
8.1 Relational Operators and Boolean Expressions 71
8.2 Logical Operators 72
8.2.1 The and Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
8.2.2 The or Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.2.3 The not Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.2.4 Mixing Logical Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.2.5 Variables in Relational and Logical Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . 74
8.3 Branching and Conditional Statements 74
13 Dictionaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
13.1 Dictionaries 117
13.1.1 Creating a Dictionary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
13.1.2 Looking Up Values by Key . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
13.1.3 Adding and Modifying Key-Value Pairs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
13.1.4 Removing Key-Value Pairs from a Dictionary . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
13.1.5 Checking if a Dictionary has a Key . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
13.1.6 Iterating over a Dictionary’s Keys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
13.1.7 Obtaining all of the Keys or Values of a Dictionary . . . . . . . . . . . . . . . . . . . . . . 120
13.1.8 Dictionaries vs. Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
13.1.9 Common Uses of Dictionaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
13.2 Combining Lists, Tuples, Dictionaries 123
16 Recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
16.1 Introduction 145
16.2 Recursion Terminology 147
16.3 More Examples 148
16.4 How to Design a Recursive Function 150
16.5 The Delegation Metaphor 151
16.6 Common Pitfalls 152
16.6.1 Confusion About Self-Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
16.6.2 Infinite Recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
16.6.3 Incorrect Answers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Part I
Computing in Context
What is Computing?
Algorithms
Computer Science
Brief History of Computing
1 — Computing
Learning Objectives
1.2 Algorithms
Algorithms are central to, but distinct from, the concept of computing. An algorithm is an ordered
list of actions that describe how to perform a task or solve a problem. Defined this way, algorithms
are an extremely general concept that are not limited simply to computing machines in any way. A
recipe for making bread is an algorithm. The recipe describes what actions you must take, and the
16 Computing
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.
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, depending on
the problem, likely will not get the desired output.
Making ramen is a useful task1 , but we would probably not call it a computation because the
output doesn’t take the form of information. We can, of course, have algorithms for performing
computations. Euclid’s GCD algorithm is a 2400-year-old algorithm for finding the greatest
common denominator of two numbers, A and B. It looks likes this:
Algorithm Euclid GCD :
If A is 0 , the answer is B
If B is 0 , the answer is A
Let M be the larger of A and B
Let N be the smaller of A and B
Let R be the remainder of M divided by N
Find the GCD for N and R
When this algorithm was created, humans would have carried out all the steps by hand. But
hopefully, you don’t need to understand the algorithm perfectly to realize that for large numbers, it
would be tedious for humans to carry out! It would be much nicer if we could get a machine to carry
out this algorithm for us.
machine is that with humans, it’s hard to be precise about what exactly they can and cannot do. For
a machine, we can precisely enumerate the fundamental basic steps that the machine is capable of
carrying out. Our algorithms, therefore, must then be written in terms of those steps. Along with
their creation, we also have to be able to evaluate our algorithms, and understand the theoretical
limits of the kinds of algorithms that are even possible to write. You’ll study these latter issues if
you go on to take more computer science. Our primary goal in this course is to develop fluency as
readers and writers of basic algorithms in a programming language.
programming language where programs could be written to "look like English", and could then be
automatically translated to machine code by a type of program known today as a compiler. This
revolutionary insight by Hopper laid the groundwork for the modern software industry and for the
high-level languages like Python that we use today.
On the hardware side, one more development that led to computers coming to look they do in
the 21st century was the development of the metal-oxide-silicon (MOS) transistor in 1959. Prior to
this time, physical computers were enormous. For example, one famous early computer, the ENIAC,
took up an entire room and weighed 30 tons. The MOS transistor made miniaturization possible,
moving computers out of the realm of specialized government and company research labs and into
people’s homes and, eventually, their pockets. Thanks to this development, a single 21st century
smartphone has more — VASTLY more — computing power than every single computer in the
world combined in the 1950s.
The reach of the miniaturization of computers is hard to understate. Today, computers can be
found in cars, fridges, power sockets, and doorbells — basically, in everything. Every single one of
these computers runs software that had to be written, at some point, by human programmers. We can
even say at this point that computing, and computer science with it, has come to form the foundation
of modern civilization.
Computer Architecture
Hardware
Software
The von Neumann Architecture
Main Memory
Central Processing Unit
Peripheral Devices
Creating Computer Programs
Edit
Compile/Interpret
Run
Learning Objectives
2.1.1 Hardware
Computer hardware consists of the physical pieces that make up a computer. Today, those pieces
are entirely electronic, and consist largely of circuit boards that are wired together. The way
the components are wired implicitly defines an algorithm that controls the basic operation of the
computer. Modern computers all have a very similar design in terms of how the hardware works
together, which we will discuss shortly.
2.1.2 Software
Computer software consists of sets of instructions telling the computer how to perform a particular
task. These instructions are nearly always originally written in a high-level programming language
(such as Python), but are usually stored on the computer in a form that has already been translated to
executable machine code. Crucially, these instructions are NOT part of the computer’s hardware;
they are simply stored by the computer as data. While the computer is turned off, these instructions
20 Hardware and Software
CPU
Main Memory
ALU
Bus
Control
Peripherals Unit
Figure 2.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 2.2.1 through 2.2.3.
(which most normal users will call ‘programs’ or ‘apps’) are stored on the computer’s hard drive (a
peripheral device), and the instructions are loaded into the computer’s main memory when they need
to be executed.
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
..
.
program instructions, numbers, letters, pictures, etc. — are stored as one or more bytes.
Memory, as a whole, is organized as a very large sequence of bytes (see Figure 2.2). Each byte
in the sequence has a numeric address that is used by the computer to access the data stored in the
byte very quickly. This address is not actually stored in memory; it is implicit in the design of the
memory circuitry. The address of the first byte in the sequence is 0, the second is 1, the third is 2,
and so on; think of the address of a byte in memory as its offset from the start of the sequence (the
first byte in the sequence is offset 0 from the start of the sequence). Viewed in this way, the main
memory of a computer is a very large one-dimensional sequence of bytes.
The maximum amount of memory that can be used in a computer is dictated by the number of
bits used by one of its addresses; an N-bit computer (ex: 8-bit, 32-bit, 64-bit) uses N bits for its
memory addresses, and can have an absolute maximum of 2N bytes of memory installed in it (in
practice this number is slightly lower due to other contraints, but not by much).
Data is divided into byte-sized pieces in part because it allows hardware designers to move data
in parallel. Typically, an N-bit computer can move N-bits (N/8 bytes) at the same time (rather than
one at a time, which would be N times slower, at least).
One of these common registers is the instruction pointer (IP); also called the program counter (PC).
Another is the instruction register (IR).
The CPU is commonly called the brain of a computer, but it might be better to call it the engine:
all the components of a computer operate like electronic clockwork, and the CPU drives it all. The
CPU performs a very simple algorithm called the machine cycle consisting of three steps:
1. The CPU fetches an instruction, by sending a signal to main memory asking for the instruction
stored at the memory address contained in the IP register. This instruction is sent along the
data bus from memory and stored in the CPU’s instruction register (IR). During the time that
the data was travelling over the bus, the CPU updates the instruction pointer, to contain the
address of the next instruction.
2. Once the instruction register contains the instruction, the CPU decodes it, which means that
the CPU uses the bit-patterns in the instruction to activate the appropriate circuits in the ALU,
or sometimes, in the control unit itself.
3. When the correct circuits are ready to go, the CPU calls for the execution of the circuit:
electronic signals pass through the circuit.
The CPU’s only work is to repeat these three steps, which it can perform at extremely high rates,
because they are encoded by the circuitry of the control unit.
a faster transfer of the data between the peripheral and main memory because it doesn’t have to go
through the CPU. Also, the CPU isn’t forced to be idle while the data is transferred.
2.3.1 Edit
A computer program is nothing more than a set of instructions that are intended to be executed
together and in sequence, and so the first step in programming is to write down those instructions.
The instructions need to be written in a programming language, and in this class, we will use the
Python language. Typically, we will use an editor of some kind to do the writing — our default
editing tool is called Pycharm — but the choice of editor is not essential. We can even write our
instructions just using pencil and paper and it’s still a valid computer program, although most likely
we’ll need to type it up before the computer can run it. Typically on a computer we will save
these instructions in a file called something like myprogram.py, but the .py file extension is just a
convention. The file itself is simply a collection of plain text statements, and can be easily opened,
read, and edited in any plain text editor (for example, Notepad on Windows).
The key thing to realize is that composing the instructions is entirely about planning what we
want the computer to do1 . Just as writing down a grocery list doesn’t magically cause food to appear
in your fridge, typing up a computer program doesn’t make the computer do anything. It’s simply a
list of instructions. Once we are happy with the instructions that we have, we can proceed to the next
step.
2.3.2 Compile/Interpret
The next step in the programming cycle is to automatically convert the instructions, written in a
language like Python, to machine code that the computer can understand. This is done using tools
that you’ll usually have downloaded when you set up your programming environment. The process
of changing high-level, human readable code into executable machine code is often called compiling.
For a language like Python, strictly speaking this term is inaccurate, as Python is what we call an
interpreted language, not a compiled one, but for our purposes, the basic idea is the same. As a result
of the automatic translation, we find out whether the computer is able to understand the instructions
that we’ve written. To continue our grocery list metaphor, this is akin to handing our shopping list to
a third party and asking them if they are familiar with all the ingredients on the list. If they are, then
good! Off to the store they go. If not, then the list will need to be corrected, changed, or expanded
upon.
1 The legendary Grace Hopper once said that programming is "just like planning a dinner" in an interview explaining
why women make excellent programmers. Our modern female students can decide for themselves if they find this comment
inspiring, condescending, sardonic, or some combination of the three.
24 Hardware and Software
2.3.3 Run
The final step is for the computer to actually execute the instructions we have written, causing the
computer to produce some data as a result, and possibly do something with that data (such as display
it to the computer’s monitor, so that we, the human user, can see it). In the Pycharm environment
that we use for this class, it will appear as if this step is combined with step 2, as there is just a
single button (or menu option if you prefer) to ’run’ your program. However, if the computer cannot
complete step 2 (i.e. it cannot fully interpret your program), then it will raise an error and your
instructions will not be executed all the way to the end. Otherwise, the program WILL run to the
end, and it will be up to you, the human, to decide whether the instructions that you wrote, and
that the computer executed, actually did what you wanted. Again, continuing with our grocery
list, once our partner has returned with the groceries, perhaps we’ll find that there was something
we needed for a recipe that we forgot to put on the list, or perhaps one of the ingredients doesn’t
work out for whatever we planned it for. If that happens, it’s not the fault of the shopper (who
purchased everything that was asked for), but rather a failure in sufficient planning while writing the
instructions. Luckily for us, going back and editing a computer program and then running it again is
a lot faster than sending our poor partner back to the store for a second trip!
Part II
Learning Objectives
The vast majority of instructions in a computer program are about manipulating data, so in
this chapter, we’re going to learn about what data is and how we perform the basic manipulation.
We’ll cover this material fairly quickly, because these concepts are common across nearly every
programming language, including MATLAB that you have already studied. If you did some
programming in high school, you will have encountered these concepts as well, possibly in a
language like Java. Every time you change programming languages, you will find there are small
details that differ, but the underlying concepts remain very similar.
28 Data, Expressions, Variables, and I/O
3.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
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.
3.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.
3.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.
30 Data, Expressions, Variables, and I/O
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
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!
1 Other, stranger things can be characters too, but we’ll avoid that discussion for now.
3.3 Variables 31
>>> ’ The card says " Moops ". ’
’ The card says " Moops ". ’
>>>
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.
3.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.
# 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. A
good metaphor is to think of variables as sticky notes. You write the variable’s name on the sticky
note, and then stick the note onto a data value. If you re-assign the variable (using the = operator) to
a new value, it’s like moving the sticky note and sticking it on something else.
In Python, more than one variable can refer to a given value. This is like saying you can stick
more than one sticky note onto the same thing. You can then later move one of the sticky notes, but
that doesn’t affect the other(s). For example:
x = 10
y = 10
x = 42
After executing the operations above, the variable x will refer to the value 42, and the variable y
will refer to the value 10. It doesn’t matter that x briefly also referred to 10.
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 Does not change the fact that Han did shoot first.
3.3 Variables 33
3.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:
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.
34 Data, Expressions, Variables, and I/O
>>> 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:
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.
36 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 ! ’
>>>
Then suppose we run this program. We will see that nothing happened! Or at least it appears
that nothing happened because Python didn’t output any responses. All of the computations in the
program did occur, but nothing was printed out in response. In a non-interactive program we have to
explicitly ask Python to print values to the console. We do this using the print() syntax. Here we
have modified the program to print out the values of the three expressions:
pi = 3.14159
r = 7
print ( pi * r **2 )
print ( 2 / 7 - 12 )
print ( 2 / (7 - 12) )
To use the print() syntax we type the word print, followed by whatever expression whose value
we want printed enclosed in a pair of parentheses. When we run the modified program, we now get
some results:
153.93791
-11.714285714285714
-0.4
You can print the values of more than one expression at once by providing a comma-separated list
of expressions within the parentheses. Each value printed in this manner is separated by a space
character. This allows you to combine data from several literals or variables to produce a single
message. Here’s a program stored in printStuff.py:
print ( ’ Two to the power of six is : ’ , 2 ** 6)
a = 8
b = 5
print ( ’ The remainder after dividing ’ , a , ’ by ’ , b , ’ is : ’ , 8 % 5)
And here is its output:
Two to the power of six is : 64
The remainder after dividing 8 by 5 is : 3
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.
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:
iroh : CMPT141 mark$ python hello . py
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.
4 — Sequences
Learning Objectives
4.1 Sequences
A sequence is a compound data type consisting of one or more pieces of data in a specific linear
ordering. Sequences are compound data types because they consist of multiple, independent values
all stored together, and as needed we can either talk about the sequence as a whole, or else talk about
its individual elements. Sequences are ordered because their elements are considered to be arranged
in a left-to-right order and that order matters. Two sequences with the same contents but different
orderings, such as the strings "ash" and "sha", are considered different sequences.
The three types of sequences we will study in this chapter are strings, lists, and tuples.
4.1.1 Strings
We have discussed strings to some degree in the previous chapter. Strings are sequences that consist
entirely of characters, such as letters, digits, or punctuation. In many other programming languages,
characters are in fact their own (atomic) data type that are distinct from strings, but this is not the
case in Python. We denote strings in Python by enclosing the characters that make up the string in
either double quotes (") or single quotes (’). Here are some examples of creating strings in Python
and assigning them to a variable.
40 Sequences
s1 = ""
s2 = "a"
s3 = ’ pikachu ’
s4 = ’ ’
s1 in particular is an example of a very important string: the empty string, which consists of
no characters. It’s perfectly fine for a sequence, such as a string, to be empty. It may help to think
of compound data types like strings as being like a container or backpack. Your backpack might
be empty of contents, but it’s still a backpack. s4 is a string consisting of a single space character
(which is the character you get when you press the spacebar). Spaces can be tricky, because they are
visualized as simply blank space on the computer’s monitor, but as far as Python is concerned, a
space is simply a character like any other. Having unintended invisible spaces in your strings (or text
files, a concept we will discuss later) is a frequent source of error!
4.1.2 Lists
Lists are sequences where the elements can consist of any data type. In fact, an individual list can
contain elements of multiple different data types (this is not true in many other popular programming
languages). This makes them very flexible, and indeed solutions to nearly all practical Python
problems will involve using lists in some way. We will spend much more time on lists in a later
chapter; for now, we focus only on what they have in common with all sequences.
We denote lists in Python by enclosing the elements that make up the list in square brackets, [
and ]. Here are some examples of creating lists in Python and assigning them to a variable.
L1 = []
L2 = [10 , 20 , 30 , 40]
L3 = [ " aaa " , 1 , " bbb " , 2]
L4 = [ [1 , 1 , 1] , [2 , 2 , 2] ]
Notice that when specifying the elements in a list, we separate the elements from each other with
commas. L1 is the empty list, analogous to the empty string; it is a list that contains no elements. L4
is an example of a list-of-lists; this is allowed because the elements of a list can be of any data type
and lists are, themselves, a data type! Again, we will say more about this very powerful usage of
lists in a later chapter.
4.1.3 Tuples
Tuples are sequences where once again the elements can consist of any data type. We denote tuples
in Python by enclosing the elements that make up the tuple in round brackets, ( and ). This makes
writing tuple literals a little bit tricky in some cases, because the round brackets already have another
meaning in Python; they can be used in expressions (like the arithmetic expressions discussed in the
previous chapter) to signify precedence. Here are some examples of creating tuples in Python and
assigning them to a variable.
4.1 Sequences 41
t1 = ()
t2 = (1 , 2 , 3)
t3 = (10 , " aaa " , 20)
t4 = (42 , )
Once again, t1 consists of the empty tuple. As with lists, the elements of a tuple are separated
from each other with commas. Finally, note that to create a tuple witih just one element for t4, we
need to include a trailing comma. This is a slightly tricky exception to the syntax for tuple creation
and is a consequence of the double meaning of the round brackets mentioned above.
From what we’ve said here, it might seem like tuples are identical to lists, and with regard to
their properties as sequences, they are. The true difference is that lists are an example of what we
call a mutable data type, which means that their contents can be changed and updated after their
creation, whereas tuples (and strings) are immutable. We’ll revisit this distinction later.
In practice, compared to strings and lists, it’s relatively rare that you’ll make an explicit choice
to create tuples in your programs. However, there are circumstances where Python automatically
creates tuples for you, so it’s important to know that tuples exist so you’ll be able to understand and
handle these situations.
4.2.1 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
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
4.2 Slicing and indexing 43
4.2.4 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.
Slicing with a Non-Unit Step Size
You can select every second, third, or n-th item between the start and end indices by specifying a
second colon and a third integer:
s = ’ Skywalker ’
>>> s [0: len ( s ):2] # every other character in s
’ Syakr ’
>>> s [2:7:3] # every third character between offsets
# 2 and 6 in s .
’ yl ’
The third integer is called the step size for the slicing operation.
Slicing with Invalid Offsets
Providing an invalid offset when indexing results in an error, as we have seen. However, providing
an invalid offset as the starting or ending offset of a slicing operation does not result in an error. The
slicing operator includes in the resulting sequence all of the original sequence items that occupy valid
offsets within the specified range. Invalid offsets within the specified range are ignored. Moreover,
nonsensical slicing where the starting offset is to the right of the ending offset results in an empty
sequence.
s = ’ Skywalker ’
>>> s [5:25] # valid offsets between 5 and 24 ( i . e . 5 through 8)
’ lker ’
>>> s [ -55: -5] # valid offsets between 55 th last and 6 th last offset .
’ Skyw ’
>>> s [5:3] # nonsense results in an empty sequence
4.2 Slicing and indexing 45
’’
Note that in the second example, offset -5 is excluded because the ending offset is always excluded
when slicing.
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
Objects and Method Calls
Calling Methods in Objects
Mutable vs Immutable Objects
Useful string methods
Programming Languages Are Not Toaster
Ovens
5 — Functions
Learning Objectives
Functions allow us to give names to small, self-contained algorithms. These functions receive
data as input, and generate new data as output. 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.
as arguments, and their values were displayed to the console as a result of the print() function’s
behaviour.
Many functions also produce data values as output. When a Python function produces output,
we say that the function returns a value. We call this output the return value of the function. Be
careful! Displaying text to the console, like the print() function does, is NOT an example of a
return value. In fact, the print() function does not HAVE a return value at all (or at least, not an
informative one). Return values instead are a piece of data produced by the function, that can (most
likely) be used later in the program as the input to other function calls.
In this way, we can use functions by providing input values (in the form of Python expressions),
and receiving back output (in the form of return values). This is a nice abstraction because we can
send data to the function, the function executes, and produces its output, and we don’t need to know
how that output is arrived at. All we need to know is what a function does, what inputs it requires,
and what it returns as a result.
min ≥2 Works like max but returns the minimum value of all arguments. Accepts
any number of arguments.
len 1 Returns the length of a sequence, i.e. the number of letters in a string, or
the number of data elements in a list
>>> len ( " abc " )
3
>>> len ([ " aaa " , " bbb " ])
2
sum 1 sequence Returns the sum of the values in a sequence that contains numbers 3.4.2!
>>> sum ( [10 , 30 , 50] ) 90
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.
>>> int (42.0)
42
>>> int ( ’ 42 ’)
42
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 3.4.2!
x = input ( ’ Please enter your name : ’ )
52 Functions
upper 0 Returns a new string with all letters converted to upper case
>>> " i choose you " . upper ()
I CHOOSE YOU
find 1 Returns the index of the first occurrence of the given substring, or -1 if
not found
>>> " Bulba Ivy Venus " . find ( " Ivy " )
6
rstrip ≥0 Strips unwanted characters from the end of the string. By default,
removes whitespace
>>> " Ash " . rstrip ()
Ash
functions they know about and if they can’t find one that does the thing they want, they assume that
thing cannot be done — that, effectively, it is "not allowed" by the programming language. This
is because the novice is treating a programming language like a toaster oven. A toaster oven is a
tool built to perform a specific task. It may have dials and buttons of various sorts, but if it doesn’t
have a button that does the particular thing you want, then the toaster oven can’t do that thing. A
programming language like Python is not a specific tool like a toaster oven, but rather a workshop
that can be used to build nearly any kind of tool. Most high-level programming languages like
Python are what we call Turing complete; this means they can compute anything that is possible
to be computed, using only their fundamental components. The fundamental components of a
programming language are its keywords, its operators, and its data types and the syntax for creating
them. Function calls, even to built-in functions, are typically optional and indeed those functions
were simply written by the people who created the language in terms of the language’s fundamental
components. They are certainly convenient - sometimes VERY convenient! - but we can nearly
always do things the long way if we need to. So if we can’t find a function that does what we want,
that just means we need to build it ourselves. The only limits are our imagination and our skill, not
the programming language itself.
Defining Functions and Parameters: The def
Statement
Functions that Perform Simple Subtasks
Functions that Accept Arguments
Returning A Value
Returning Nothing
Defining Before Calling
Summary
Variable Scope
Console I/O vs Function I/O
Documenting Function Behaviour
Generalization
Cohesion
6 — 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.
60 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.
6.2 Variable Scope 61
6.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 )
62 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!
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.
6.3 Console I/O vs Function I/O 63
Some types , such as ints , are able to use a more efficient algorithm when
invoked using the three argument form .
6.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.
Suppose we want to compute how many users can simultaneously stream a movie on Netflux on
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.
6.6 Cohesion 65
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.
6.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 6.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.
3What 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!
Modules: What Are They and Why Do We
Need Them?
How to Use Modules
What Other Modules Are There?
Finding Module Documentation
7 — 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 Syntax
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
1A module is just a .py file that contain only function and/or object definitions. You can write your own group of
chapter.
70 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.
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.
72 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 73
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 ! ’)
Note that only one of the three blocks is executed. As soon as an if- or elif- statement is True,
its block is executed and no more if- or elif- statement conditions are tested, and no more of the
8.3 Branching and Conditional Statements 77
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 )
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.
80 Control Flow – Repetition
code before
while-loop
# code before while - loop
while condition:
while True execute # block ( indented )
condition: block
# code after the while - loop
False
code
after the
while-loop
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:
82 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 4.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 83
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
84 Control Flow – Repetition
Learning Objectives
10.1 Lists
We have already mentioned lists 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 and tuples);
• lists may contain items of different data types;
• lists are mutable sequences, meaning they can be altered after they are created (see Section
10.2.1); and
• lists are objects, and contain methods which you can call
In many ways, lists are the central data type in Python and it is almost impossible to write a
practically useful program without them. In this chapter, we will discuss in more detail how to wield
the full functionality of lists in useful ways.
86 Advanced List Usage
The most common list to create this way is the empty list; creating lists with just a small number
of starting values is generally most useful for creating toy examples and for your own testing
purposes.
Because lists are so central to Python, there are also many functions in many modules that obtain
or generate data in some way and return the data items as a list. For example there are modules
that contain functions for reading data from a file and returning that data in a list. We’ll look at an
example of this in a later chapter.
10.3 List Methods 87
If the given value is in the list more than once, only the index of the first occurrence will be
returned. If the item doesn’t exist at all, Python will raise an error.
10.4 Concatenation
We saw, back in Section 3.3.4, that the + operator concatenates two sequences, and it can be used on
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]
92 Advanced List Usage
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.
This is because using indexing on the left-hand side of an assignment operator does allow us to
modify a list. Therefore, if modifying a list is required, then this form of loop is preferred.
for x in loot :
if x [1] > 500:
expensive_loot . append ( x )
But we can do it even more easily with a list comprehension:
# A list of magic items .
loot = [ [ ’ Sword of Fighting ’ , 1250 , 10] ,
[ ’ Scroll of Conjure Milk ’ , 20 , 5] ,
[ ’ Yellow Wizard Robe ’ , 100 , 3] ,
[ ’ Orcish Rhyming Dictionary ’ , 550 , 1] ]
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:
import math as m
roots = [ m . sqrt ( x ) for x in range (10 , 51)]
A particular useful form of this type of list comprehension is to change the data type of all the
items in a list, like so:
data = [ " 1 " , " 2 " , " 3 " , " 4 " , " 5 " ]
data = [ int ( x ) for x in data ]
If our original list is a nested list, we can construct a new nested list but omitting some of the
original nested items like so:
loot = [ [ ’ Sword of Fighting ’ , 1250 , 10] ,
[ ’ Scroll of Conjure Milk ’ , 20 , 5] ,
[ ’ Yellow Wizard Robe ’ , 100 , 3] ,
[ ’ Orcish Rhyming Dictionary ’ , 550 , 1] ]
inventory = [ [ x [0] , x [2] ] for x in loot ]
1You
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.
10.8 Summary of List Methods 95
The above list comprehension will produce a new nested loop, but the inner sublists will contain
only item names and their quantity; the price (which was at index 1 of each sublist) was left out for
the new sublists.
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 we will be concerned mostly with relatively simple list
comprehensions of the forms we have seen here.
Learning Objectives
This algorithm might seem simple, but that’s because this is a simple task. Furthermore, we
generally want our algorithms to be as simple as possible so that we can be completely confident
that each step is undeniably both necessary and correct. Once we’re happy with our algorithm, we
refine it into pseudocode.
function average
input : a list L of numeric values
output : the average of all the values
initialize sum to 0
for each number in the list L :
add the number to the sum
avg = s / len ( L )
return avg
In this case, there’s nearly a one-to-one correspondence between the lines of the pseudocode and
the lines of our Python code. That will often be the case, but not always. For instance, it would have
been perfectly fine to use Python’s built-in sum() function to add up the numbers instead of a loop.
But not all languages have such a function, so we probably shouldn’t put it in the pseudocode.
One of the biggest challenges that novices have with using this workflow is convincing themselves
that it is necessary. Indeed, what we’ve done here will seem like overkill for such a simple problem -
and in this case, it probably is. But we practice this technique, and others like it, when problems
are small, because otherwise we will not be able to tell if we’re using the techniques correctly once
problems get big.
11.2 Documentation
Documentation is an integral part of good program design. It is most important in professional
settings where software development is almost always done in teams, but it is useful even for personal
projects. You might be surprised when you look back at code that you yourself wrote months, weeks
or even just days ago and struggle to remember what you were thinking when you wrote it! In this
section, we will expand on how to write effective and useful documentation.
102 Software Design and Documentation
avg = s / len ( L )
return avg
There is no need at all for this docstring to mention what kind of loop is used to iterate through
the numbers, nor is there any need to mention the specific name of variables used in the function’s
body. This information is irrelevant to anyone wanting to call the function. A good example of a
purpose is as follows:
def average ( L ):
" " " computes the average from a list of numbers
"""
For very simple functions, it may even seem like the purpose is redundant given the function
name. That’s a good thing when it happens; wherever possible function names should be both
intuitive and descriptive. But we can’t count on all behaviour being so simple that a single name can
summarize it perfectly.
Finally, a function’s purpose should also clearly describe any effects of the function not captured
by its return value. For example, if the function prints information to the console, this should be
mentioned in the purpose.
The parameter description should, for each parameter, include all of the following information:
the parameter’s expected data type, any constraints on its value, and if appropriate, its semantic
meaning.
A parameter’s expected data type is likely straight-forward, but it is ok to mention categories of
types if needed. For example, for many functions, integers and floats are both acceptable input, and
in such a case we could simply say the function requires "any numeric value".
11.2 Documentation 103
Value constraints can limit the expected value of a parameter within a given data type: for
example, requiring integers to be positive, or expecting a list to contain only numbers.
Lastly, the semantic meaning of a parameter helps in choosing productive values with which
to call the function. Some functions are extremely general in this regard, such as our average()
function, and so there isn’t anything to say on this point. A good, complete docstring for that function
might look like this:
def average ( L ):
" " " computes the average from a list of numbers
params
L : a list containing entirely numeric values
params
long_source : float . current longitude of a vehicle
lat_source : float . current latitude of a vehicle
long_dest : float . longitude of the destination
lat_source : float . latitude of the destination
Learning Objectives
12.1 Overview
Programmers make errors. This is an inevitable fact of life. This fact doesn’t change even as you
progress from novice to expert in your skills. It is rare that any sizable piece of code will work
perfectly on the first run. What will change is your ability to detect and find errors, and the speed
with which you can fix them. Although you have almost certainly experienced the process of finding
and fixing errors already on your own, in this chapter we present some more principled techniques
for doing so.
out problems that they understand and can describe very well. Any time you ask an instructor "is
this what you want?", you are engaging in verification. In professional settings, verification is vastly
more difficult. End users, who are typically not computer experts, often do not know exactly what
they want until you show it to them. Getting this right will often involve a lot of rapid proto-typing
and back-and-forth with the end users. This is an important skill, but we won’t be focusing on it
further in this class.
Validity is asking the question "Did I build the thing correctly?" To validate a piece of software,
you are ensuring that what the software actually does is a good match to what YOU wanted it to
do, based on your design for the software. If it does not, that means you (the programmer) made an
error communicating what you wanted to the computer - in other words, an error in your code. Any
time your program has crashed or given you the wrong output and you’ve tried to fix it, you’ve been
engaging in the process of validation.
12.4 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.
12.4 Testing 109
Note that these two goals are contradictory! The more test cases we have, the more likely we are
to find faults. The only way to reconcile the two is to be smart and careful about which test cases we
select.
It is also worth noting 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.
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 knowledge that you can read
from the function’s docstring. In practice, black-box tests are in fact usually created before the code
is even written, which definitely means you won’t be able to see it when creating the tests!
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” and "rare” so you can better understand our
thinking.
12.4 Testing 111
Input(s): []
Output(s): False
Reason: Test when the list is empty
[Rare]
Notice that none of the test cases rely on knowing the implementation of the function, only its header,
and its docstring description.
We could probably come up with more test cases, but we’ll stop here. Writing test cases requires
effort, and that effort has diminishing returns. That is, after a certain point, more test cases are less
and less likely to uncover new faults. It is often more of a priority to make sure one tests all of the
rare cases than to exhaustively test the more common cases since the rare cases are more likely to
require special cases in the code, and special cases in the code are more likely to harbour faults.
The goal of test case generation is not to make enough of them to guarantee that the software is
error-free, but to do just enough testing that the probability of a bug remaining is extremely low.
Believe it or not, it requires vastly more work to provide a guarantee of correctness than it does to
provide a very low probability!
if-statement is true, and another path (which will enter the else-block) when the condition is false.
We then identify test cases that cause the execution of each of 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
exactly 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
12.4 Testing 113
using the black-box method in terms of just the inputs and expected outputs. The only thing that is
different is the reason for the test case. Many novices over-estimate the difference between black-box
and white-box testing. Both are simply slightly different mental models that help programmers to
answer the question: "Of the infinitely many test cases that I could try, how do I pick just a small
number of them and still be reasonably confident that my code is error-free?"
It is well-known that white-box and black-box testing are complementary methods. We’ll often
identify the same test cases using either method. But sometimes, one method will help 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 produced by errors 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 = False
if result != expected :
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.
114 Testing and Debugging
# call with single - item list containing one element divisible by 7
result = is_divis ible_by_ 7 ([7])
expected = True
if result != expected :
print ( ’ Error : returned False when given [7] ( divisible by 7) ’)
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 = True
if result != expected :
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 single “test complete” message when the entire test driver is concluded so that we can be
certain that the test driver ran to completion and reported no faults.
12.5 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.
execution at which the fault occurs and incorrect data is generated, and usually gives us insight into
where the error might be that is causing the fault.
It is very hard to demonstrate this process in a reading, but you will see a demonstration during
class time.
12.6 Summary
Of course, testing and debugging doesn’t end when the errors are fixed. Code that has been modified
to fix errors should be tested again to make sure no new faults were introduced in the course of fixing
the previously detected errors.1 This makes testing and debugging an iterative process (illustrated in
Figure 12.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
13 — Dictionaries
Learning Objectives
13.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.
118 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:
122 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 ’]
13.2 Combining Lists, Tuples, Dictionaries 123
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 ’ ])
14 — 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
64.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 14.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]
130 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.
14.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 14.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.
134 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’?
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.
15.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
engineer, a knowledge of Boolean logic directly assists circuit design. Of course, logic is not the
136 Binary Number Systems and Logic
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!!
138 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 15.1 lists binary and decimal representations for a small set of integer quantities.
Table 15.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
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 3.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.
16 — Recursion
Learning Objectives
16.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.
Many problems lend themselves to recursive solutions. For example, consider the problem of
146 Recursion
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)
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.
148 Recursion
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
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.
16.3 More Examples 149
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.
150 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.
152 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.
17 — Search Algorithms
Learning Objectives
17.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 ??? If not, go back and remind
yourself.
156 Search Algorithms
Table 17.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.
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!
160 Search Algorithms
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
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)
17.4 Comparison and Summary of Linear Search and Binary Search 161
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)
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
Divide-and-Conquer Sorts
Merge Sort
Quick Sort
18 — Sorting Algorithms
Learning Objectives
18.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.
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
164 Sorting Algorithms
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 just two sorting algorithms: merge sort and quick sort.
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.
# divide
S1 = first half of S
S2 = second half of S
# conquer !!!
S = merge ( S1 , S2 )
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. Remember that in this framing, we know nothing about the
relationship between S1 and S2. Everything in S1 might be smaller than everything in S2; or the
other way around; or maybe S1 and S2 will need to be interleaved in some way. The only thing we
know is that S1 and S2 are each, individually, already sorted.
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:
166 Sorting Algorithms
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:
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:
18.2 Divide-and-Conquer Sorts 167
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 = []
return 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
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.
18.2 Divide-and-Conquer Sorts 169
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. In class we will examine the type of inputs for which
the performance of quick sort becomes poor.
Course Summary
19 — Conclusion
Learning Objectives
base conversion
binary to decimal, 138
decimal to binary, 140
binary numbers
addition, 138
meaning, 138
multiplication, 139
negative values, 142
relation to logic, 143
search
binary, 158
linear, 157
sorting, 163