0% found this document useful (0 votes)
52 views173 pages

Cmpt142 Readings

Uploaded by

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

Cmpt142 Readings

Uploaded by

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

Introduction

to
Computer Science
for Engineers

Jeff Long

Course readings for


CMPT 142
Copyright © 2021 Jeff Long

P RODUCED BY THE AUTHORS FOR STUDENTS IN CMPT 142

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.

First Edition, Septmeber 2021


Acknowledgements

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

Part I Computing in Context

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

2 Hardware and Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19


2.1 Computer Architecture 19
2.1.1 Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.1.2 Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

2.2 The von Neumann Architecture 20


2.2.1 Main Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.2.2 Central Processing Unit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.2.3 Peripheral Devices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

2.3 Creating Computer Programs 23


2.3.1 Edit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.3.2 Compile/Interpret . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.3.3 Run . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Part II Computing with Python
3 Data, Expressions, Variables, and I/O . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.1 Data 28
3.1.1 Atomic Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.1.2 Compound Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.1.3 Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.2 Expressions 29
3.2.1 Literals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.3 Variables 31
3.3.1 Variable Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.3.2 Variable Assignment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.3.3 Variables as Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.3.4 Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.4 Console Input and Output 36
3.4.1 Outputting Text to the Screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.4.2 Reading Strings from the Keyboard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.4.3 Reading Numbers from the Keyboard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

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

9 Control Flow – Repetition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79


9.1 While-Loops 79
9.2 While Loops for Counting 81
9.3 For-Loops 82
9.4 Ranges and Counting For-Loops 83
9.5 Choosing the Right Kind of Loop 84
9.6 Infinite Loops 84

10 Advanced List Usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85


10.1 Lists 85
10.2 Basic List Operations 86
10.2.1 Mutable Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
10.2.2 Creating Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
10.2.3 Accessing List Items . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
10.2.4 Modifying List Items . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
10.3 List Methods 87
10.3.1 Adding Items to a List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
10.3.2 Removing Items from a List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
10.3.3 Locating an Item in a List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
10.3.4 Sorting the Items in a List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
10.3.5 Copying Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
10.4 Concatenation 91
10.5 Iterating Over the Items of a List 92
10.6 Nested Lists 93
10.7 List Comprehensions 93
10.8 Summary of List Methods 95

Part III The Practice of Computing


11 Software Design and Documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
11.1 Software Engineering 99
11.1.1 Goals of Software Engineering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
11.1.2 Software Design Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
11.2 Documentation 101
11.2.1 Function Docstrings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
11.2.2 Single-Line Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103

12 Testing and Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107


12.1 Overview 107
12.2 Verification and Validity 107
12.3 Testing and Debugging 108
12.4 Testing 108
12.4.1 Standard Form of Test Cases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
12.4.2 Test Case Generation: Black-Box Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
12.4.3 Test Case Generation: White-Box Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
12.4.4 Implementing Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
12.5 Debugging 114
12.5.1 Debugging by Inspection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
12.5.2 Debugging by Hand-Tracing Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
12.5.3 Integrated Debuggers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
12.6 Summary 115

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

14 File I/O . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125


14.1 Data File Formats 125
14.1.1 Common Text File Formats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
14.2 File Objects in Python – Open and Closing Files 127
14.3 Reading Text Files 128
14.3.1 Reading List Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
14.3.2 Reading Tabular Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
14.4 Writing Text Files 131
14.4.1 The write() method. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
14.4.2 Writing List Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
14.4.3 Writing Tabular Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
14.5 Pathnames 133

15 Binary Number Systems and Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135


15.1 Introduction 135
15.2 Binary Numbers 136
15.2.1 Numbers vs Numerals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
15.2.2 Representation of Binary Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
15.2.3 Converting from Binary to Decimal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
15.2.4 Addition of Binary Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
15.2.5 Multiplication of Binary Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
15.2.6 Subtraction and Division . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
15.2.7 Converting from Decimal to Binary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
15.2.8 Binary Addition and Multiplication: Connections with Logic . . . . . . . . . . . . . . 141
15.2.9 Going Further with Number Representations . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

15.3 From Boolean Operators to Propositional Logic and Beyond 143


15.4 Common Pitfalls 144

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

Part IV Algorithms: Searching and Sorting

17 Search Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155


17.1 Fundamentals of Searching 155
17.1.1 Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
17.1.2 Search Keys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
17.1.3 The Target Key . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
17.1.4 Search Goals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157

17.2 Linear Search 157


17.3 Binary Search 158
17.4 Comparison and Summary of Linear Search and Binary Search 161

18 Sorting Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163


18.1 Introduction 163
18.2 Divide-and-Conquer Sorts 164
18.2.1 Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
18.2.2 Quick Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
19 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
19.1 Course Summary 171

Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Part I

Computing in Context
What is Computing?
Algorithms
Computer Science
Brief History of Computing

1 — Computing

Learning Objectives

After studying this chapter, a student should be able to:

• define the term computing


• define the term algorithm
• describe in broad terms some landmarks in computing history
• describe the contributions of select famous figures in computing

1.1 What is Computing?


Broadly speaking, computing is a goal-oriented process of calculation that might benefit from some
form of computing machinery. Performing simple arithmetic by hand is an example of computing
that most people are familiar with. Humans can do arithmetic on their own if they need to. It gets
easier if you have a pencil and paper; and easier yet with a digital calculator. The key aspect of
computing is that we have a precise and well-defined goal that we want to achieve, and the goal
takes the form of information. When computing, there is something we want to know - say, the sum
of a series of numbers - and a series of steps we must take to get from the information we have to the
information we want. These steps might be described easily, but in many cases, there are so many of
them that it would be impossibly tedious to execute the steps without the aid of a machine of some
kind.

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.

1.3 Computer Science


Computer Science, at its essence, is about creating algorithms intended to be executed by a computing
machine. The difference beween creating an algorithm for humans and creating an algorithm for a
1 Especially for students who are often on a tight budget!
1.4 Brief History of Computing 17

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.

1.4 Brief History of Computing


Humans have been computing for millenia, and for much of that time, they have also used devices
or machines to assist them. More than 4000 years ago, the Babylonians developed a counting and
calculating system of placing pebbles in lines of sand, a technique that foreshadowed counting
devices like the abacus. However, for most of this history, it was assumed that humans would carry
out the computations 2 and any assistive devices were largely there as memory aids.
Devices that could perform computations on their own did emerge throughout the middle ages,
but each device was limited to a single purpose. This paradigm finally changed in the mid 1800s with
the design of the Analytical Engine by the british mathematician Charles Babbage. The Analytical
Engine is regarded as the world’s first design of a single machine that could, without the hardware
being altered or reconfigured, solve arbitrary computational problems. The machine possessed
all the fundamental theoretical components of modern computers. The Analytical Engine was
never physically constructed during Babbage’s lifetime but its theoretical properties have since been
verified.
The world’s first computer program is widely regarded as having been written by Babbage’s
collaborator, Ada Lovelace, who, in the 1840s, devised an algorithm for the Analytical Engine to
compute a mathematical sequence known as the Bernoulli numbers. Since a physical Analytical
Engine never existed, Lovelace was never able to test her algorithm on the machine, but it too was
later verified to be correct 3 . Less well-known, but arguably more important, is that Ada Lovelace
realized the potential of the Analytical Engine as a machine that manipulated not just numbers, but
rather arbitrary symbols (as represented by numbers), and in this way could be used to store and
compose music and other forms of media.
Although Babbage and Lovelace are well-recognized today for the importance and originality of
their ideas, without question the most influential figure in the history of computer science is Alan
Turing. A british mathematician and cryptographer who broke german radio codes during the second
World War, Turing is regarded as the father of both theoretical computer science and of artificial
intelligence. He created an elegant abstract model of computation now called the "Turing machine"
that is used to prove bounds on the limits of computability, and is known for the "Turing test", a
thought experiment about what it would mean for a machine to be considered intelligent.
Contemporary to Turing’s work in the 1940s, the world’s first functional computers emerged
(and indeed Turing worked on some of them). Although these computers were programmable, this
programming had to be done using machine code that was unique to each machine. This was one more
paradigm that would need to change, and the one to drive that change was american mathematician
and navy rear admiral Grace Hopper. In 1952, Hopper proposed the idea of a machine-independent
2 Infact, in the 1930s and 40s, the term "computer" was an actual job title, not a machine!
3As you begin your computing journey, you will hopefully soon be suitably awed by this achievement of writing a
correct computer program without ever being able to run it yourself!
18 Computing

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

2 — Hardware and Software

Learning Objectives

After studying this chapter, a student should be able to:

• distinguish computer hardware from software


• define in broad terms the role of the CPU and of main memory
• describe the edit-compile-run software cycle

2.1 Computer Architecture


Computer architecture is the conceptual design and operational structure of a computer system.
When discussing a computer architecture, we talk about the parts of a computer (the hardware) and
how those parts operate together to run computer programs (software).

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.

2.2 The von Neumann Architecture


The distinction between hardware and software is key to how modern computers operate.
Prior to the mid-1940s all computers were fixed function computers. That is, they were built to
perform a single specific task (adding numbers; or calculating ballistic trajectories; or calculating
the values of the logarithm function) and could perform no task other than the one they were built
for. Such computers were built using a combination of mechanical and electronic parts. On such
machines, essentially computer programs were part of the hardware.
John von Neumann changed all that in 1945. The key innovation of the von Neumann architec-
ture is the idea that computer programs can be stored in computer memory just like data! In other
words, von Neumann realized that the instructions for carrying out a task could be treated like data
in the sense that they can be stored in the computer’s memory right alongside the data to be used
while performing the task. By treating computers programs as data, and storing them in the same
way and in the same place as data, a von Neumann architecture computer is able to perform different
tasks simply by storing different programs in memory as software. It seems obvious to us now, but
back in 1945, this was a seriously big deal.
Figure 2.1 shows a conceptual diagram of the von Neumann architecture. In this diagram the
lines (with and without arrows) indicate bus lines. A bus is a set of wires connecting the components
of a computer; a bus is used to send data (in the form of electrical impulses along wires) between the
components. You know all those thin lines you see on your computer’s motherboard? Some of those,
at least, are the bus.

2.2.1 Main Memory


The main memory is the central storage centre for the computer; this is where your program and
program data are stored when the program is running. The basic unit of your computer’s main
memory is a simple circuit for storing 1 bit of data; a bit, or binary digit, is just a single 0 or 1
value. To represent a bit using an electronic circuit, engineers define a threshold voltage for the
circuit; if the voltage of the circuit is above the threshold then the circuit is holding a bit with value
1, otherwise the bit has value 0. These 1-bit circuits are organized into groups of 8 called a byte.
A byte is the smallest accessible memory unit on a computer. All of the data in your computer —
2.2 The von Neumann Architecture 21

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

Figure 2.2: Organization of main memory.

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

2.2.2 Central Processing Unit


The central processing unit (CPU), is where most of the computation happens. It’s made up of
an arithmetic logic unit (ALU) and a control unit. The ALU knows how to do simple arithmetic
operations on integers and floating point numbers, how to make comparisons to see which of two
numbers is larger, and how to perform boolean operations (which we will learn about later) like
AND, OR, and NOT. The control unit contains the circuitry for coordinating the execution of a
computer program; it uses the bus to fetch data and program instructions from main memory as
needed, feeds data to the ALU to perform computation, and obtains the results of such computations,
possibly also storing them back into main memory.
A very small amount of data can be stored right inside the CPU in extremely high-speed memories
called registers. Registers can be used as temporary storage for small amounts of data that will be
used in the very immediate future to avoid the relatively high time costs of communication with main
memory. Different CPU designs have different numbers and sizes of registers, but there are some
special-purpose registers that are common to all CPUs (though they may go by different names).
22 Hardware and Software

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.

2.2.3 Peripheral Devices


Peripheral devices include any hardware that you would attach to a computer, and that the computer
would communicate with. This includes input devices that feed data in to the computer, such as a
keyboard, mouse, and/or touchpad; output devices that output information from the computer like
your display and sound card; and combination, input/output, devices that do both, such as a network
card or modern printer (the printer might send status codes back to the computer – this counts as
input). Crucially, it also includes the computer’s hard-drive, which stores all the programs that the
computer might run once it is turned on. This includes the computer’s operating system, such as
Windows or MacOS, which are themselves simply software programs! At one time, peripheral
devices were frequently seen outside the computer case. As our technology increases, especially
with the development of notebook computers, tablets and smartphones, peripheral devices are very
often being enclosed inside the case.
For peripheral devices to be useful, data must be transferred between main memory and peripheral
devices. One way to accomplish this transfer would be to use the CPU. For example, the CPU could
fetch data from an input peripheral to be stored momentarily in a register; the CPU would then store
the data from the register to main memory. The drawback here is that the data travels across the bus
twice, existing only temporarily in a CPU register, and the CPU would not be able to do anything
else while the data was transferred across the bus. This approach was used in early computer design,
and fit quite nicely in the von Neumann architecture.
A better approach is to allow peripherals and main memory to communicate directly, leaving the
CPU free to do other things while this is occurring. This is accomplished by an innovation called
direct memory access (DMA). The idea here is that all peripherals have bus lines that connect them
to a central direct memory access controller, which has a dedicated bus that connects it to main
memory. The DMA controller is an integrated circuit like the CPU, but much simpler because it has
to do one thing and one thing only: move data between peripherals and main memory. The CPU can
initiate a DMA transfer for I/O between a peripheral and main memory by sending a signal (via the
bus) to the DMA controller, which initiates and completes the data transfer. This approach results in
2.3 Creating Computer Programs 23

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 Creating Computer Programs


The primary task of a computer programmer, and consequently of most students in a computer
science class, is the creation of brand new programs (i.e. software) for a computer to run. This
involves a three-step process that we’ll call the edit-compile-run cycle. Normal users of a computer
skip the first two steps of this cycle; they just download and install apps that have already been
written, compiled, and are ready to run.

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

Computing with Python


Data
Atomic Data
Compound Data
Data Types
Expressions
Literals
Variables
Variable Names
Variable Assignment
Variables as Expressions
Operators
Console Input and Output
Outputting Text to the Screen
Reading Strings from the Keyboard
Reading Numbers from the Keyboard

3 — Data, Expressions, Variables, and I/O

Learning Objectives

After studying this chapter, a student should be able to:

• distinguish between atomic data and compound data;


• describe what a data type is;
• describe what a literal value is in Python;
• give examples of literal values corresponding to integer, floating-point, string and
boolean data;
• list the basic arithmetic operators in Python;
• describe what an expression is in Python;
• compose valid arithmetic expressions in Python using operators and literals;
• describe what a variable is;
• explain the naming rules for variables;
• compose valid expressions in Python using variables;
• use the print syntax to display literal values and the values of variables on the console;
and
• use the input syntax to read values from the console and store the value read in a
variable.

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.1.1 Atomic Data


Atomic Data is the smallest unit of data that a computer program can define. An example of atomic
data is a single number, like 42. The word “atomic” derives from the word “atom”. At one point
in the history of chemistry, atoms were believed to be the smallest indivisible pieces of matter in
the universe. In computer science, the word “atomic” is often used to refer to something that is
indivisible or cannot be made smaller.

3.1.2 Compound Data


Compound data is data that can be subdivided into smaller pieces of data which are organized in a
particular way. An example of compound data is a list. A list consists of several pieces of data which
have a specific ordering. The data items that comprise a piece of compound data may themselves
be either compound or atomic. For example, we could imagine a list of numbers. The list itself is
compound data, while each piece of data in the list is atomic data. We could also imagine a list of
lists of numbers. In such a case, the list of lists is compound data, and each piece of data in the list is
itself an example of compound data whose individual pieces are, in turn, atomic data.
For the rest of this chapter, we’ll be dealing almost exclusively with atomic data. For now, the
only type of compound data we will be using are strings (which we will define momentarily in
3.1.3).

3.1.3 Data Types


In a computer program, every piece of data, compound or atomic, has a data type. For one last
moment, let us recall that every piece of data in a computer is, at a very low level of abstraction that
we don’t usually worry about, made up of binary 0’s and 1’s (these are referred to as bits). The data
type of a piece of data tells the computer how to interpret those bits, for instance as a number or as a
character; or as an integer or a fraction. In Python there are several built-in atomic and compound
data types.
Atomic Data Types
In this section we describe the most commonly used atomic data types in Python.
Integer: Integer data are positive or negative whole numbers, or zero. In Python, there is no limit to
the size of an integer number.
Floating-point: Floating-point data are real numbers, that is, numbers that are not necessarily whole
numbers, such as the number 42.5. Floating-point data in Python (and any other language) have
3.2 Expressions 29

a limited range, and limited


√ precision. This means that numbers with infinite representations,
such as 1/3=0.33333..., 2, or π, cannot be represented exactly. In Python, floating-point
numbers can range between 10−308 to 10308 (positive or negative) with at most 16 to 17 digits
of precision.
Boolean: Boolean data can only be one of two values: True or False. Note that capitalization
matters – true and false are not valid boolean values in Python, but True and False are.
Compound Data Types
In this section we briefly describe some of the standard compound data types that are built into
Python. However, we will save the the details of most of these until later chapters.
String: Strings are sequences of characters (e.g. letters of the alphabet) and are usually used to
store text. We’ll say more about strings and characters later in this chapter.
List: Lists are a sequence of data items. Each item in a list can be of any data type.
Tuples: Tuples are also a sequence of data items, and the items can again be of any type. The only
difference between a tuple and a list is that you cannot change the contents of a tuple once it
has been created. We’ll discuss lists and tuples further in chapter 4.
Dictionary: A dictionary consists of key-value pairs in no particular order. You can look up values
by their key. We’ll discuss dictionaries in more detail in chapter 12.

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.

String Literals in Other Languages

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.

3.3.1 Variable Names


Variable names (also called identifiers) in Python have to follow the following rules:
• may contain letters or digits, but cannot start with a digit;
• may contain underscore (_) characters and may start with an underscore; and
32 Data, Expressions, Variables, and I/O

• may not contain spaces or other special characters.


Thus, KyloRen, IG88, and luke_Skywalker are valid variable names, but these are not:
Luke+Leia_4_Evar (contains special character +), 2ManyStormTroopers (starts with a digit),
and Han Shot First (contains spaces).2

3.3.2 Variable Assignment


The equal sign (=) is used in Python to assign a variable name to a value. Unlike many other
programming languages, variables in Python do not have to be declared before they are used. You
just use them. Here are some examples of variable assignment:
 
x = 5 # assign the name x to the integer 5.
y = 42.0 # assign the name y to the floating - point number 42.0

# assign the name error_message to the string : " That didn ’t work !"
error_message = " That didn ’t work ! "
 

It may seem strange to think of the name being assigned to the value. Indeed, in most other
programming languages we tend to think of assigning values to variables. But in Python, it is safer,
and more reflective of how Python actually works, to think of assigning variable names to values. 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.3 Variables as Expressions


Just like a single literal, a single variable is an expression. The value of such an expression is the
data value that the variable refers to.
 
>>> x = 10; # x refers to the integer 10
>>> x # an expression
10
>>>
 
In the above example, Python tells us that the value of the expression x is 10.
Putting it this way might seem trite and borderline useless, but it’s actually very important to the
way Python (and most programming languages) operate. Continuing our sticky note metaphor, if
you use the name written on the sticky note in nearly any context EXCEPT as the LEFT-hand side
of a variable assignment, you’re actually referring to the value that the sticky note is stuck to, not
the physical sticky note itself. For example:
 
x = 10
y = x
x = 42
 
After executing the operations above, the variable x will refer to the value 42 and the variable y
will refer ot the value 10. This makes perfect sense if we translate the code above into the sticky
note metaphor as follows:
 
write x on a sticky note and attach it to the value 10
write y on a sticky note and attach it to whatever x is stuck to
move the x sticky note to the value 42
 
Thus, when x appeared on the right-hand side of a variable assignment, it was treated as an
expression that yielded its value, so we just assigned y to that same value, 10. Put another way,
sticky notes can ONLY be stuck to values, never to other sticky notes! As a result, moving the x
sticky note to the value 42 has no effect on the value that y is currently stuck to.

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:

Usage Description Example Expression Value


x ** y Exponentiation; x to the power of y 2 ** 5 32
-x Negation -42 -42
x * y Multiplication; x times y 6 * 7 42
x / y Division; x divided by y 6 / 4 1.5
x // y Integer division; x divided by y rounded down 6 // 4 1
x % y Modulo; remainder after integer division of x by y 6 % 4 2
x + y Addition; x plus y 3 + 6 9
x - y Subtraction; x minus y 2 - 7 -5
3.3 Variables 35

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

If you took CMPT 140 or know another programming langauge

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 ! ’
>>>
 

3.4 Console Input and Output


The console is the default location for text input and output from/to the computer’s user. Console
output goes to the screen (usually a terminal window). Console input comes from the keyboard.

3.4.1 Outputting Text to the Screen


We have seen that when we use Python in interactive mode and enter an expression, Python responds
with the value of the expression. But when we use Python in non-interactive mode, this is not the
case. Suppose we put the following Python program into a file called arithmetic.py:
 
pi = 3.14159
r = 7
pi * r **2
2 / 7 - 12
2 / (7 - 12)
 
3.4 Console Input and Output 37

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
 

If you previously took CMPT 140 or learned Python 2

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.

3.4.2 Reading Strings from the Keyboard


You can ask for input from the keyboard with the input() syntax:
 
x = input ()
 
This line will pause the program, and wait for the user to type something and press the Enter key.
Whatever the user typed will be stored as a string associated with the variable x. You can optionally
38 Data, Expressions, Variables, and I/O

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.

3.4.3 Reading Numbers from the Keyboard


Reading numbers is a bit tricker because input() will only read strings. However, we can convert
the string to a number, if the string is actually a representation of a number.
 
# read an integer
x = int ( input ( ’ Enter an integer : ’ ))

# read a floating - point number


y = float ( input ( ’ Enter a floating - point number : ’ ))
 
The text that the user typed for the first input will be converted to an integer and given the name x, if
that text contains only digits. Otherwise, Python will issue an error. The second input will convert
the typed text to a floating-point number and give it the name y, if the text is a valid representation
of a floating point number. Again, if it isn’t, Python will issue an error.
Sequences
Strings
Lists
Tuples
Sequence Operators
Slicing and indexing
Indexing
Offsets from the End
Invalid Offsets
Slicing

4 — Sequences

Learning Objectives

After studying this chapter, a student should be able to:

• define and identify strings, lists and tuples as examples of sequences


• describe the common properties of sequences
• use indexing to obtain a desired element in a sequence
• use slicing to obtain subsequences of a sequence
• use non-unit step size to select non-contiguous elements in a sequence

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.1.4 Sequence Operators


Concatenation and Duplication
We mentioned in the previous chapter that some arithmetic operators, such as + and *, can be used
on strings. In fact, those operators behave the same when used on any kind of sequence. + can be
used to combine two sequences into a new, joint sequence, while * creates a sequence where the
existing elements have been duplicated some number of times. For example:
 
L1 = [1 , 2 , 3] + [4 , 5 , 6]
L2 = [1 , 2 , 3] * 3
L3 = [0] * 10
 
After executing the statements above, L1 will consist of the list [1, 2, 3, 4, 5, 6]. L2 will
consist of the list [1, 2, 3, 1, 2, 3, 1, 2, 3]. L3 will consist of the list [0, 0, 0, 0, 0,
0, 0, 0, 0, 0]. This last form is particularly useful for creating a large list with all the elements
set to some default value, which is quite a common thing to want to do.
Membership: the in operator
The Python keyword in is an operator that can be applied to sequences. It is used to determine
whether a sequence contains a specific element. The syntax is to list first the value of the data
element, followed by the keyword in, followed by the sequence. For example:
42 Sequences
 
>>> " k " in " ash ketchum "
True
>>> 42 in [1 , 10 , 25 , 42]
True
>>> " ash " in [ " ash ketchum " , " gary oak " ]
False
 
Notice that the in operator is checking for STRICT equality. In the last example,we are checking
whether the given list contains the data value "ash" as one of its elements - which it does not, since
"ash" and "ash ketchum" are obviously different strings.

4.2 Slicing and indexing


Python defines two important operators called indexing and slicing that can be applied to sequences.
The purpose of these operations is to access sections of a sequence that may be smaller than the
entire sequence as a whole. We will generally use strings as our examples for these operations, but
they work exactly the same for lists and tuples too.

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

String (sequence of characters): V a d e r

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

>>> print ( c ) # print c ( the 9 th character from s ).


r
>>> print ( s [2]) # print the third character of s
y
>>> s [0]+ s [2]+ s [4] # Concatenate the 1 st , 3 rd , and 5 th characters
’ Sya ’
>>> x = 7
>>> s [ x ] # Offset 7 since x refers to 7
’e ’
>>> s [ x +1] # Offset 8 since x refers to 7
’r ’
 
In each example above, the indexing operator obtains the character from the string at the given offset.
Then we can do what we want with it: assign a variable name to it, pass it to a function, use it in
an expression, etc. Also note that offsets can be integer literals, variables that refer to integers, or
integer-valued expressions.

4.2.2 Offsets from the End


It is also possible to specify an offset from the end of the sequence using negative integers. Offset
−1 is the offset of the last character. Offset −2 is the offset of the second-last character, offset −3 is
the offset of the third-last character etc.. Examples:
 
>>> t = ’ TARDIS ’ # Make t refer to ’ TARDIS ’
>>> t [ -1] # Access the last character of t
’S ’
>>> t [ -3] # Access the third - last character of t
’D ’
 

4.2.3 Invalid Offsets


If you use an offset that does not exist or is of the wrong type, Python will issue an error. Remember
that offsets must be integers. Positive offsets of a string s must be between 0 and the length of s
minus 1, while negative offsets must be be between -1 and the negation of the length of s. Here are
some of the things that can go wrong if you use an offset that is out of range:
 
>>> s = ’ Ice King ’ # Make s refer to ’ Ice King ’

>>> s [9] # Offset out of range


Traceback ( most recent call last ):
File " < stdin > " , line 1 , in < module >
IndexError : string index out of range

>>> s [ -10] # Offset out of range


Traceback ( most recent call last ):
File " < stdin > " , line 1 , in < module >
IndexError : string index out of range

>>> s [5.0] # Floats cannot be offsets . Ever .


Traceback ( most recent call last ):
File " < stdin > " , line 1 , in < module >
TypeError : string indices must be integers
 
44 Sequences

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

After studying this chapter, a student should be able to:

• describe what a function is


• describe the role of a function’s return value
• describe how to call a function
• explain what the arguments of a function are
• call methods on objects
• distinguish between functions and methods

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.

5.1 Functions and Abstraction


Functions are an example of the powerful concept of abstraction. They allow us to refer to a block
of Python code by name, and ask for that code to be executed. The code within a function can be
executed without knowing what it is or how it works. Consider a function that you’ve already seen:
the print() function. We introduced this function in Section 3.4.1 and used it to output the values
of expressions to the console, but we didn’t reveal until now that it is a function!
One major purpose of functions is to break up your program into manageable parts. Each part
knows only what it needs to know in order to run its algorithm, and thus the parts can be designed
and tested independently.
Most functions require input of some kind. Input to functions in Python are called arguments.
When we used the print() function before, we provided expressions (usually involving strings)
48 Functions

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.

5.2 Calling Functions


In Python we invoke the algorithm inside of a function by calling the function. To call a function,
we write the name of the function followed immediately by a pair of parentheses. The arguments to
the function (inputs!) are given as a comma-separated list within the parentheses. We illustrate this
with one of the examples of the print function we saw in Section 3.4.1:
 
a = 8
b = 5
print(’The remainder after dividing’, a, ’by’, b, ’is:’, 8 % 5)
 
We have coloured the different parts of the function call:
Red: The function name.
Green: Parentheses enclosing the list of arguments.
Blue: The arguments (inputs) to the function. Notice how each argument is an expression.
Brown: The commas separating the arguments in the argument list.
All function calls have the same general format and look like this:
 
function_name(argument1, argument2, argument3, ... )
 
The print function is a bit special in that it can accept any number of arguments. Most functions
are defined to have a fixed number of arguments for providing specific inputs. The len function in
Python is an example; it accepts exactly one argument. The len function can be used to find out
how many characters are in a string. You provide a string that you want to know the length of as the
argument, like this:
 
S = " No , I am your father ! "
len ( S )
len ( " No . No , that ’s not true . That ’s impossible ! " )
 
Here we have two calls to the function len. The first one returns the length of the string referred to
by S, which is 21. The second call returns the length of the string literal "No. No, that’s not
true. That’s impossible!", which happens to be 45. In the next section we will discuss how
to obtain and use the value returned by a function.
5.2 Calling Functions 49

5.2.1 Functions as Expressions: Obtaining/Using a Function’s Return Value


Function calls are expressions. Like all other expressions they have a value. The value of a function
call is the return value of the function! Remember, in interactive mode, if you enter an expression,
Python prints out the value of the expression. So here’s what happens when we enter the expressions
in the listing from the previous section:
 
>>> S = " No , I am your father ! "
>>> len ( S )
21
>>> len ( " No . No , that ’s not true . That ’s impossible ! " )
45
>>>
 
Python prints out the return values of the function calls because the function calls are expressions
whose value is the return value of the function call. Since the value of a function call is its return
value, you can use a function call wherever we can use an expression! Thus, function calls can be
used...
• as operands of operators:
 
>>> len ( S ) + len ( " No . No , that ’s not true . That ’s impossible ! " )
66
 
• as values in assignment statements (give a name to the return value of a function):
 
>>> L = len ( S )
>>> print ( L )
21
 
The return value of len is 21, which gets assigned the name L. Since L now refers to the value
21, the print function call outputs 21 to the console.
• as arguments to other functions:
 
>>> print ( len ( S ) , len ( ’ Search your feelings ! ’ ))
21 21
 
The return values of the len function are the arguments to the print function. The two calls
len happen first, and their return values are used as arguments to print. Some people call
this a nested function call, because a call to one function (len) is being made as part of a call
to another function (print). When nested function calls are used, the calls are made in order
from inner-most to outer-most.

5.2.2 Calling Functions with No Arguments


It is possible for a function to be defined to have no arguments. This means that the function doesn’t
have any input. To call a function with no inputs, you simply leave the space between the parentheses
empty. There are very few functions without arguments, since necessarily, such a function would
always have to do exactly the same thing whenever it was called, and would therefore rarely be
useful.
50 Functions

5.2.3 Functions That Do Not Return a Value: Procedures


If a function has no output and does not return a meaningful value, then value of the function call is
the special value None. This means that, strictly speaking, it is not possible to have a function that
returns nothing, because None is a value!
An example is the print function. The print function doesn’t need to return a meaningful
value because it doesn’t compute any new values, it just does something, namely, print its arguments
to the console. The following example proves that print returns the value None:
 
>>> x = print (42)
42
>>> print ( x )
None
>>> x
>>> y
Traceback ( most recent call last ):
File " < stdin > " , line 1 , in < module >
NameError : name ’y ’ is not defined
 
The first line gives the name x to the return value of print(42). Then print(x) proves that the
value referred to by x is None. So print returned None. The next two lines show that there is a
difference between the value None, and no value at all. Typing just x is fine, because x refers to the
value None. But typing y results in a NameError because y refers to no value at all! The variable y
was never assigned to a value.
In mathematics, a function, by definition, always has a value. Thus, functions in a programming
language that do not return a value are sometimes referred to as a procedure since they are not, in the
strict mathematical sense, functions.

5.2.4 More Built-In Python Functions


We conclude this section with a few examples of commonly used built-in Python functions shown in
the table on the next page.
5.2 Calling Functions 51

Name Number of Description and Example


Arguments
max ≥2 Returns the maximum value of all arguments. Accepts any number of
arguments.
>>> largest = max (10 , 15 , 42 , 19)
>>> print ( largest )
42

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

type 1 Returns the data type of its argument.


>>> type (5)
< class ’ int ’ >
>>> type (2.3)
< class ’ float ’ >
>>> type ( ’ foo ’)
< class ’ str ’ >

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

5.3 Objects and Method Calls


Nearly all of the more complex data types available in Python, such as strings, tuples, and lists, are
part of a broader category of data type called objects. Nearly all objects have several methods. You
can think of a method as "an operation that the object knows how to perform upon itself." We can
call an object’s methods in much the same way as we call functions. The syntax is just very slightly
different, and a method call is always associated with a particular object.

5.3.1 Calling Methods in Objects


Let’s suppose we have a variable name s that refers to a string. Python strings have a method called
isdigit() which returns a boolean value indicating whether the string contains only digits between
0 and 9. To call the isdigit() method in the string object referred to by s, we write s.isdigit().
We call a method in an object exactly the same way we call a function, except that we have to
precede the method call with the object in which we want to call the method, followed by a period.
We sometimes call this syntax dot notation, because there’s a dot (the period) in between the object
and the method name. Here’s a more complete example:
 
>>> s = ’ This string contains 4 and 2 , but not * only * digits . ’
>>> t = ’ 421 ’ # this string contains only digits .
>>> s . isdigit ()
False
>>> t . isdigit ()
True
>>>
 
The first call of isdigit is on the string object s, which is a string containing other characters in
addition to digits, so it returns False. The second call to isdigit is on the string object t, which
contains only digits, so it returns True.
Here’s another example showing how to use the lower() method of a string to return a new
string where all of its characters are lower-case:
 
>>> mystring = ’ On the Internet CAPITAL letters mean SHOUTING ! ’
>>> mystring . lower ()
’ on the internet capital letters mean shouting ! ’
>>>
 
In general, if the variable x refers to an object which has a method named y, you can call the
method y in object x using x.y().
As you can see from the examples above, it’s quite common for object methods to require no
arguments. This is because the object itself that comes before the period can be considered an input
to the method. However, object methods can also be defined to require additional arguments. An
example is the find method defined by Python strings which is used to determine the location of a
substring within a larger string. The argument to the find function is the string we wish to find in the
object in which we are calling find. So the method call mystring.find(’Internet’) searches
for the location of the string ’Internet’ in mystring. Let’s try it and see what the result is:
5.3 Objects and Method Calls 53
 
>>> mystring = ’ On the Internet CAPITAL letters mean SHOUTING ! ’
>>> mystring . find ( ’ Internet ’)
7
 
Our method call returned 7! This is because the string ’Internet’ occurs beginning at 7 characters
to the right of the first character in the string (count it!). If the find method does not find the given
string in its object’s string, it returns -1.
So to summarize, we can call a method in an object in the same way as we call functions, we
just need to use the dot notation to specify which object we want to call the method on. We will be
calling methods in objects quite a lot. We’ll also be seeing many other kinds of objects other than
strings.

5.3.2 Mutable vs Immutable Objects


We have briefly mentioned that some Python objects are immutable, (strings and tuples), while others
are mutable (lists). But there is nothing special about immutable objects apart from the kinds of
methods that they have.
An immutable object has no methods that allow a program to change the data stored inside the
object. Immutable object methods can return new values only. Notice the behaviour of all of the
string methods relating to converting case. They return NEW versions of the original string, but they
do not change the original string itself. They can’t, because strings are immutable.
In contrast, mutable objects will have methods that allow a program to change the data inside the
object. Two of the data types that we will discuss in great detail later, lists and dictionaries, have
such methods.

5.3.3 Useful string methods


The following is a short list of commonly-used methods for strings. This list is non-exhaustive and
many more string methods exist. Just google "Python string methods" if you want to find them.
54 Functions

Name Number of Description and Example


Arguments
lower 0 Returns a new string with all letters converted to lower case
>>> " ASH " . lower ()
ash

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

isalpha 0 Returns a boolean value indicating whether a string contains ONLY


letters. Remember, spaces are not letters!
>>> " pikachu " . isalpha ()
True
>>> " Gotta catch ’ em all " . isalpha ()
False

isdigit 0 Returns a boolean value indicating whether a string contains ONLY


digits
>>> " 1234 " . isdigit ()
True
>>> " a1b2 " . isdigit ()
False

isalnum 0 Returns a boolean value indicating whether a string contains ONLY


digits and/or letters
>>> " a1b2 " . isalnum ()
True
>>> " a ? b " . isalnum ()
False

5.4 Programming Languages Are Not Toaster Ovens


The built-in functions and methods in a programming language are often very convenient. But a
common mental error for novice students is to overly rely on them. They search through the built-in
5.4 Programming Languages Are Not Toaster Ovens 55

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

After studying this chapter, a student should be able to:

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

6.1 Defining Functions and Parameters: The def Statement


In Python, functions are defined by writing the keyword def. A keyword is a name we give to words
in a programming language that have special meaning. Generally, keywords cannot be used as
variable or function names, because Python will think you mean something else! The best way to
see how this works is with some examples.
58 Creating Functions

6.1.1 Functions that Perform Simple Subtasks


The simplest form of function is one that takes no arguments as input (i.e. a procedure!) and
returns no value. In Section 3.4.2 we saw how to ask the user to enter their name, and then respond
with a greeting. Suppose this is an algorithm that we want to perform frequently. We can create
an abstraction of this algorithm it by writing a function called introductions that performs the
algorithm when we call it, allowing us the luxury of not having to remember how it works. Here’s
what that would look like:
 
def introductions ():
x = input ( ’ Please enter your name : ’)
print ( ’ Hello , ’ , x )
 
Let’s break down what’s happening here. The first line uses the keyword def to define a function
called introductions. The name of a function in a function definition must be followed by a pair
of parentheses, then a colon. Notice how the rest of the lines are indented. In Chapter ??, we called
this a block. Just like in our pseudocode, we group statements together in blocks by indenting them.
All of the Python code that is part of a function has to be in the same block, so it has to be indented,
and be indented by exactly the same amount or Python will complain thinking that some lines are
not part of your function even when you want them to be.
 
# Wrong indentation ; Python will think that the print function
# is not part of the function , and will just execute it .
def introductions ():
x = input ( ’ Please enter your name : ’)
print ( ’ Hello , ’ , x )

# Wrong indentation ; Python will issue an error here because the


# indentation is inconsistent , and it can ’t figure out
# whether the print function call is part of the function or not .
def introductions ():
x = input ( ’ Please enter your name : ’)
print ( ’ Hello , ’ , x )
 
Indentation has specific meaning to Python. Python is not like other languages where indentation is
only cosmetic. Indentation is used to indicate blocks which specify program structure.
Now, the correctly indented function definition doesn’t actually do anything other than define
the function. Like any function, the code it contains only gets executed when it is called:
 
# defines the function only :
def introductions ():
x = input ( ’ Please enter your name : ’)
print ( ’ Hello , ’ , x )

# this function call actually calls the function ,


# which executes its code .
introductions ()
 
6.1 Defining Functions and Parameters: The def Statement 59

6.1.2 Functions that Accept Arguments


We have already shown you functions that accept input using arguments. To define your own
function that accepts arguments, you can add a comma-separated list of variable names between
the parentheses. These are called the function’s parameters. Parameters are the variables that the
function uses to refer to the arguments it is given in a call to that function. Here we have modified
our introductions function to have a parameter called greeting.
 
# defines the function only :
def introductions ( greeting ):
print ( greeting )
x = input ( ’ Please enter your name : ’)
print ( ’ Hello , ’ , x )

# this function call actually calls the function ,


# which executes its code .
introductions ( ’ Welcome to my Python program ! ’)
 
This defines a function that takes one argument when you call it; the last line of the program shows
the function being called with a string as an argument. The function, internally, refers to that
argument by the variable name greeting. We have written the function so that it assumes that the
greeting is a string, which it prints out prior to asking for the user to enter their name. If we execute
the above Python program, this is what we will see:
 
Welcome to my Python program !
Please enter your name : Mark
Hello , Mark
 
The bright red text was entered by the user. The string ’Welcome to my Python program!’
was used as an argument to the function ’introductions’. The argument was then assigned the
parameter name greeting, so that within the function, the variable greeting refers to the string
’Welcome to my Python program!’, which is why this is the output we get when the function
executes print(greeting).
A function can have any number of parameters. Each parameter you add corresponds to an
argument that must be provided when the function is called. Parameters are assigned to refer to
arguments in the same order they are given. For example, if a function was defined in this way:
 
def func ti on Wit hM an yAr gu me nts (a , b , c , d ):
# code for function would go here

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.

6.1.3 Returning A 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

# this function call actually calls the function ,


# which executes its code .
username = introductions ( ’ Welcome to my Python program ! ’)
 

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.4 Returning Nothing


If a function does not need to return a value, then simply do not include a return statement. After the
execution of the last line of the function, the value None will be returned by default. As we noted
before, such a function is sometimes called a procedure.

6.1.5 Defining Before Calling


Python functions must be defined before they are called. Thus a function definition must appear in a
file prior to any calls to that function. In the following code, the first call to introductions would
fail and cause a NameError. But the second call to introductions would work fine because its
definition appears first.
 
# this fails -- function called before definition
username = introductions ( ’ Welcome to my Python program ! ’)

def introductions ( greeting ):


print ( greeting )
x = input ( ’ Please enter your name : ’)
print ( ’ Hello , ’ , x )
return x

# this is fine - function called after definition .


username = introductions ( ’ Welcome to my Python program ! ’)
 

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.

6.2 Variable Scope


The scope of a variable refers to the parts of the program in which a variable exists after it is assigned
to a value. This sounds complicated but it’s actually quite straightforward. Variables defined within
a function only exist within that function. Variables defined outside any function do not exist within
any functions. Here’s an example:
 
def fireball_damage ():
damage = 30
return damage

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

def acid_blast_damage ( bonus_damage ):


# In this line , the variable bonus_damage * is *
# the same variable as the base_damage variable
# defined outside the function , and has the value
# 10.
return base_damage + bonus_damage

acid_blast_damage (25)
 
Unlike the earlier example with the damage variable, since the usage of base_damage within the
function does not appear on the left side of an assignment operator (=), it refers to the variable that
was defined at global scope that is defined outside of any function. However, if we added a line to
the acid_blast_damage function that read base_damage = 20 this would define a new variable
that was local to the acid_blast_damage function which is different from the global scope variable
of the same name. The base_damage variable at global scope would continue to refer to the value
10, and any subsequent references to base_damage in the function would refer to the local variable
with value 20.
If this seems confusing to you, it’s because it is! This is precisely why we tell you not to
try to use variables defined at global scope within functions. It is much safer and easier to
understand if, when you need data from global scope variables, to pass them into functions as
arguments!!! In other words, don’t use global scope variables within functions at all and you
don’t have to worry about this!

If you took CMPT 140

In CMPT 140 you probably encountered the Python keyword global which allows functions
to access variables defined outside of the function. This was necessary because of the
specialized Processing programming framework in which you were working. In general, we
normally avoid the use of global variables because they can cause unexpected side effects and
bugs when we inadvertently refer to the same variable when we don’t mean to. In this class
we do not allow the use of global variables. In almost all situations where global variables
seem appropriate there exists a better way.
6.3 Console I/O vs Function I/O 63

6.3 Console I/O vs Function I/O


In computer science, we use the words input and output a lot, and we use them to mean different
things. For example, the inputs and outputs of a function are different and distinct from console
input and console output. Function inputs are the arguments of a function call assigned to function
parameters; function outputs are returned from data created within a function. Console inputs are
read from the keyboard; console outputs are printed to the screen rather than being sent to another
part of a program.
When reading instructions it is important to be able to distinguish these forms of input and output.
If you are asked to write a function that “takes” something as input, this means the function should
take an argument via a parameter. If you are asked to write a function that “reads from the console”,
or “asks the user” for some data, this means that the function should perform console input to get
this data by calling the input function. If you are asked to write a function that “outputs”, “prints”,
or ”displays” some data, then this should be done with console output by calling the print function.
If you are asked to write a function that “returns” some data, then this should be done using the
return keyword.

6.4 Documenting Function Behaviour


Python does not restrict the data type of function arguments, so you can pass an argument of any
type as the argument for any parameter of a function. But usually functions expect arguments to be
of a certain data type. How do we communicate these expectations to the programmer who wants to
call the function?
When we write a function we should document what its inputs and outputs are. We can do this
by writing docstrings in our program. Docstrings are a way of describing what the function does,
what each parameter is for, the expected data type of the argument to that parameter, and what the
function returns (if anything). Here’s how we would do this for our introductions function:
64 Creating Functions
 
def introductions ( greeting ):
"""
Greet the user and asks them for their name .

greeting : A string containing a message to greet the user


Returns : The name entered by the user .
"""
print ( greeting )
x = input ( ’ Please enter your name : ’)
print ( ’ Hello , ’ , x )
return x

# this function call actually calls the function ,


# which executes its code .
username = introductions ( ’ Welcome to my Python program ! ’)
 
The docstring is enclosed in triple double-quotes and is indented with the rest of the block of code
for the function.2 The triple double-quotes are how you specify a multi-line string literal in Python.
The docstring should contain a brief one-line description of what the function does, followed by
a list of parameters, and what they are for, followed by a description of what the function returns.
There are no particular formatting requirements for the contents of the docstring, but you should
strive for something similar to the above.
If a function has a docstring, you can view it by typing print(functionname.__doc__).
 
>>> print( introductions . __doc__ )

Greet the user and asks them for their name .

greeting : A string containing a message to greet the user


Returns : The name entered by the user .
 
It also works for built-in functions, like pow or max:
 
>>> print( pow . __doc__ )
Equivalent to x ** y ( with two arguments ) or x ** y % z ( with three arguments )

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

After studying this chapter, a student should be able to:

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

7.1 Modules: What Are They and Why Do We Need Them?


Modules are files that contain function and object definitions that add additional, and much more
powerful features to Python. The basic Python language provides only very fundamental building
blocks for programs. Modules are a way for people to share functions and objects that they have
written so that other people can use them in their own programs. In this respect, modules are similar
to the libraries that are used by other programming languages such as C++ and Java. Viewed another
way, modules contain abstractions of algorithms that we can use in our own programs without having
to understand how they work.

7.2 How to Use Modules


Modules are stored in separate files from our own programs. Thus, in order to use the functions and
objects defined by a module, we have to tell our own program to look in those files and read those
definitions. We do this using a Python keyword called import.
Perhaps you noticed that Python, by default, doesn’t seem to be able to compute common
mathematical functions like logarithms and finding the square root of a number, or trigonometry
functions such as sine, cosine, and tangent. The reason for this is that these functions are instead
68 Modules

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 math as m # read the definition of the log function


>>> m . log10 (1000) # now we can compute base -10 logarithms !
3.0
 
Our first call to log10 fails, because Python does not have a built-in function called log10. The
command import math as m reads the function definitions from the math module and creates an
object called m that contains the functions defined by the math module (i.e. the functions defined by
math become methods of m). Since the log10 function is defined in the math module, the object
m contains the log10 method, and we can call it in the same way we call any method in an object
using the dot notation we learned in Section 5.3.1. As we can see, above, m.log10(1000) returns
the correct value 3.0.
Below is a program showing some more examples of using functions from the math module.
 
import math as m

print ( m . sqrt (7.5)) # display square root of 7.5


print ( m . exp (5)) # display e to the power of 5
print ( m . log2 (256)) # display the base -2 logarithm of 256
angle = m . radians (90) # convert 90 degrees to radians .
print ( m . sin ( angle )) # display the sine of 90 degrees .
 
Run this program in Python, and you’ll see that it produces the output described. If you’re wondering
why we used the radians function to convert the angle 90 degrees to radians, it’s because the sin
function requires that its argument be an angle in radians.
If you want to see a complete list of the functions in the math module, and documentation on
how to use them, click on the following link, or copy it into your web browser: https://docs.
python.org/3/library/math.html.

Import Syntax

In general, the syntax for importing modules is:

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.

7.3 What Other Modules Are There?


In this course, we use a Python distribution called Anaconda. Anaconda comes with a lot of modules,
too many to list here. This is one of the great things about Python. There are so many modules
7.3 What Other Modules Are There? 69

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

related functions and use them as a module!


2 The image data is returned as an array object! We’ll learn more about arrays and what we can do with them in a later

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.

7.4 Finding Module Documentation


At this point, I am sure you are wondering how one finds out about modules, and how to use
them. The short answer is: internet search. For example, a search for "skimage display image"
returns us a link to the documentation for the skimage.io module. That link is here: http:
//scikit-image.org/docs/dev/api/skimage.io.html.
While it can sometimes be hard to find documentation for modules, rest assured that, for this
course, we’ll always tell you how to use a module function or object that we expect you to use, or at
least tell you exactly where the documentation is.
Relational Operators and Boolean Expres-
sions
Logical Operators
The and Operator
The or Operator
The not Operator
Mixing Logical Operators
Variables in Relational and Logical Ex-
pressions
Branching and Conditional Statements

8 — Control Flow

Learning Objectives

After studying this chapter, a student should be able to:

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

8.1 Relational Operators and Boolean Expressions

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

Operator Meaning Example Result


== are the operands equal? 42 == 42 True
!= are the operands unequal? 42 != 42 False
< is the first operand smaller than the second 10 < 42 True
operand?
> is the first operand larger than the second operand? ’Bill’ > ’Lenny’ False
<= is the first operand less than or equal to the second? 42 <= 42 True
>= is the first operand greater than or equal to the ’R’ >= ’Z’ False
second?
Notice how the operators work with non-numeric data as well, like strings and characaters. In such
cases the comparison is made lexicographically (dictionary ordering). ’Bill’ is not greater than
’Lenny’ because ’Bill’ comes before ’Lenny’ in dictionary ordering. For the same reason, the
expression ’Bill’ < ’Lenny’ has the value True.
Relational operators all have the same precedence and so, are evaluated from left-to-right. But
all relational operators also have a lower precedence than all arithmetic operators, which means
arithmetic operators get evaluated first. Thus the expression 5 + 5 < 10 is False because the
addition happens first, resulting in the value 10. Since 10 is not less than 10, the < operator evaluates
to False.

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.

8.2 Logical Operators


The operators and, or, and not are logical operators (also called Boolean operators). They are
so-called because the operands of logical operators must be Boolean values. Thus the operands of
logical operators can either be Boolean values or other Boolean expressions. We can use logical
operators to ask questions about Boolean values or Boolean expressions.
All logical operators have a lower precedence than relational operators. So that means that
relational operators always get evaluated before logical operators.

8.2.1 The and Operator


The expression x and y has a value of True only if both x and y are True. In all other cases, such an
expression has a value of False. Remember that x and y could be Boolean literals, Boolean values,
Boolean expressions, or even a function call that returns a Boolean value. Here are some examples:

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.

8.2.2 The or Operator


The expression x or y has a value of False only if both x and y are False. In all other cases, such
an expression has a value of True. Here are some examples of expressions using or:

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.

8.2.3 The not Operator


The not operator is a unary operator. It only takes one operand. The expression not x has a value
of True only if x is False; it has a value of False if x is True. So not changes the Boolean value
of its operand to the other Boolean value. Here are some examples:

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.

8.2.4 Mixing Logical Operators


We don’t want you to get the idea that you can only use one kind of logical operator per expression.
You can mix them up as much as you like, but take care — the logical operators do not have the
74 Control Flow

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.

8.2.5 Variables in Relational and Logical Expressions


We also don’t want you to get the idea that you can’t use variables with these operators. In any of
the examples above where a literal appears in an expression, we could also replace the literal with a
variable. For example, a < b and c < d. We just can’t evaluate this without knowing the values
of the variables. Here’s a complete example where we associate the variable names with values and
use them in a Boolean expression:
 
>>> a = 1
>>> b = 5
>>> c = 2
>>> d = 4
>>> a < b and c < d
True
>>>
 

8.3 Branching and Conditional Statements


Now that we know how to ask questions about data using Boolean expressions, we can use the values
of Boolean expressions to get our programs to perform different actions depending on the value of a
Boolean expression. This is called branching and it allows us to perform one block of code if an
Boolean expression is True, and a different one if it is False.
In Python, we perform branching using a conditional statement or if-statement. The syntax is the
word if, followed by a Boolean expression, followed by a colon, like this:
if condition:
8.3 Branching and Conditional Statements 75

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 ! ’)

print ( ’ That was not a valid guess . ’)


 
Listing 8.2: A program that uses a simple conditional statement.
But this won’t work because the second call to print will execute regardless of whether the Boolean
expression in the if-statement is True. What we need is a way of specifying a second block that
gets executed only if the Boolean expression in the if-statement is False. We can do this using an
else-statement. An else-statement is the word else followed by a colon:
 
guess = int ( input ( ’ Guess a number between 1 and 100: ’ ))
if guess >= 1 and guess <= 100:
# This block executes if the condition is True
print ( ’ That was a valid guess ! ’)
else :
# This block executes if the condition is False
print ( ’ That was not a valid guess . ’)
 
Listing 8.3: A program that uses an if-else statement.
Now, if we enter a number that is between 1 and 100, it will execute the first block of code. Otherwise,
it will execute the else statement’s block of code. In general, the flow of execution for conditional
statements looks like this:
76 Control Flow

if
condition
 
True False if condition:
# if block ( indented )
execute execute else :
"if" block "else" block # else block ( indented )

# code after else block


code after  
"else" block

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 ! ’)

if guess > 100:


print ( ’ Too high ! ’)

if guess >= 1 and guess <= 100:


print ( ’ That was a valid guess ! ’)
 
But Python, and most other programming languages give us a cleaner way to do this that guarantees
that only one of a series of blocks can be executed. In Python, there is an elif-statement (“elif” is
short for “else if”). An elif-statement consists of the word “elif”, followed by a Boolean expression,
followed by a colon, followed by a block of statements to execute if the Boolean expression is True.
An elif-statement can appear after the block associated with an if-statement or another elif-statement,
but is only executed if the preceding if- or elif-statement was found to be False. So here’s a different
way we could write our program which does the same thing, but is a bit easier to read: 
guess = int ( input ( ’ Guess a number between 1 and 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 ! ’)
else :
# Otherwise , execute this block .
print ( ’ That was a valid guess ! ’)
 

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 )

elif True execute # ... more elif ’s as desired


condition: block #3
else : # ( optional )
False # else block
.. more elif’s as desired
.
# code after the else block
 
else block
(optional)

code after
the else
block

Notice that only one of the blocks in the if-elif-elif-...-else chain can execute no matter how many
elif-statements there are. Finally, remember that the blocks can consist of multiple lines of code, as
long as they are all indented.
 
# suppose smaller and larger are variables referring to
# integer values
if smaller > larger :
# swap the values referred to by the variables
temp = smaller
smaller = larger
larger = temp
 
Because all three lines after the if-statement in the above code are indented, they are all part of the
block, and all three only get executed if the if-statement’s condition is True . Block indentation must
be such that every line of every block is indented by the same amount, otherwise Python will not
understand your program. Moreover, the indentation must be either all spaces or all tabs, you can’t
mix them. However, most text editors that are Python-aware (e.g. PyCharm, TextWrangler) should
automatically prevent you from mixing tabs and spaces.
While-Loops
While Loops for Counting
For-Loops
Ranges and Counting For-Loops
Choosing the Right Kind of Loop
Infinite Loops

9 — Control Flow – Repetition

Learning Objectives

After studying this chapter, a student should be able to:

• 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 ! ’)

# ask for a new guess


guess = int ( input ( ’ Guess a number between 1 and 100: ’ ))

print ( ’ That was a valid guess ! ’)


 
This program will ask the user for a guess, and then, as long as the guess is not valid, the while-loop’s
condition will be True, the reason for the guess being invalid will be printed, the user will be asked
for another guess, then the loop condition will be checked again with the new guess, and so on, until
the guess becomes valid. Once the guess is valid, the loop condition will be False, and the print
function call after the while-loop’s block will print that it was a valid guess.
Note that the block after the while-loop’s condition consists of the if-elif statement, and the line
that asks for another guess. The if-elif statement, in turn has its own blocks, which are indented
relative to the first block. This is an example of nested blocks. You can nest blocks to any number
9.2 While Loops for Counting 81

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 !
 

We will show you more examples of while-loops in class.

9.2 While Loops for Counting

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

turtle . down () # put the turtle ’s pen down


turtle . circle (20) # draw a circle of radius 20 pixels
turtle . up () # pick up the turtle ’s pen

# add 1 to the number of circles drawn


circlesDrawn = circlesDrawn + 1
 

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

The general form of a counting while-loop that does something n times


 
n = number of times you want to do something
counter = 0
while counter < n
do the thing
counter = counter + 1
 
Of course counting while-loops are not limited to those that count from 0 to n. You can count from
any integer a to any other larger integer b in a similar manner by changing the initialization of the
counter variable so it starts counting at a, and adjusting the loop condition so the loop stops repeating
when the counter’s value is b.

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.

9.4 Ranges and Counting For-Loops


We can use for-loops to create counting loops just like we did with while-loops. To do so, we first
need to learn about a new kind of sequence called a range.
A range is a sequence of integers that begins at an integer a (the start), ends before an integer b
(the stop), and in which the difference between each element in the sequence, called the step size, is
equal. Ranges are created with Python’s built-in range function. The range function requires two
arguments, start and stop, and can optionally accept a third argument for the step size which, if not
given, defaults to 1. You may also provide just a single argument to range; range(x) is equivalent
to range(0, x, 1), and is the sequence 0, 1, 2, . . . , x − 1. Here are some example ranges:
 
range (0 ,5 ,1) # the sequence 0, 1, 2, 3, 4
range (5) # the sequence 0, 1, 2, 3, 4
range ( -4 , 4) # the sequence -4 , -3 , -2 , -1 , 0 , 1 , 2 , 3
range (0 , 11 , 2) # the sequence 0 , 2 , 4 , 6 , 8 , 10
range (2 , -3 , -1) # the sequence 2 , 1 , 0 , -1 , -2
range (0 , 5 , 10) # the sequence 0

# 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

9.5 Choosing the Right Kind of Loop


Generally, for-loops are what you want to use to iterate over a sequence. Both for-loops and
while-loops are appropriate for simple counting loops. You may prefer using for-loops with ranges
for counting purposes because it requires less typing than the equivalent while-loop. For most
other non-counting loops that have complicated loop conditions and/or don’t involve iterating over
sequences, while-loops are likely the best choice.

9.6 Infinite Loops


Infinite loops are loops that repeat forever. A while-loop whose loop condition can never become
False is an infinite loop. Here are a couple of examples:
 
# This counting loop is infinite because the programmer forgot
# to add the x = x + 1 line to the end of the block . The value
# of x never changes , so the loop condition is always True .
x = 0
total = 0
while x < 10
total = total + x
average = total / 10
 
 
# This loop is infinite because the programmer incorrectly used
# ’ or ’ instead of ’ and ’. Mathematically , the condition can
# never be False , regardless of the value referred to by x .
# Thus , the loop repeats forever .
x = -1
while x >= 0 or x <= 10:
x = input ( " Enter a number that isn ’t between 0 and 10: " )
 
It’s quite difficult to accidentally write infinite for-loops because sequences are of finite length and
they repeat only once for each item in the sequence.
Lists
Basic List Operations
Mutable Sequences
Creating Lists
Accessing List Items
Modifying List Items
List Methods
Adding Items to a List
Removing Items from a List
Locating an Item in a List
Sorting the Items in a List
Copying Lists
Concatenation
Iterating Over the Items of a List
Nested Lists
List Comprehensions
Summary of List Methods

10 — Advanced List Usage

Learning Objectives

After studying this chapter, a student should be able to:

• describe what a list is


• access and modify list elements via indexing
• productively use list methods
• identify and author simple list comprehensions in Python
• use list comprehensions to select items from a list
• use list comprehensions to modify items from a list
• employ common loop strategies for dealing with lists

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

10.2 Basic List Operations


10.2.1 Mutable Sequences
In Python lists are sequences, just like strings, tuples, and ranges, which means we can use indexing
and slicing on them. But lists are the first sequence we have encountered that are mutable, which
means that you can modify the contents of the sequence. Strings, tuples, and ranges are immutable
sequences — once they are created, they cannot be changed.
We will see that we can do things to mutable sequences that we cannot do to immutable sequences.
For example, we can add and remove items from a list, but we can’t add and remove characters from
strings (they are immutable).
Sometimes the difference between mutable and immutable sequences can seem confusing. When
we do string concatenation using the + operator (remember this from Section 3.3.4?) it kind of
looks like we’re changing a string. When a and b are strings, it may seem like we’re changing a by
appending b to a, but what we are really doing is creating a new (immutable) string that is the result
of the concatenation:
 
a = ’ Winter is ’
b = ’ coming ’
s = a + b
 

Here s is a new string. The strings a and b are not changed.


But lists are mutable. We can change them without causing a new list to be created, as we will
see in the following sections.

10.2.2 Creating Lists


As a reminder, we create list literals by using square brackets. That is to say, a list literal is a
comma-separated list of expressions enclosed in a pair of square brackets:
 
L = [] # the empty list

x = [2 , 3 , 5 , 7 , 11] # a list of some prime numbers

# a list of video game titles


y = [ ’ Diablo 3 ’ , ’ Path of Exile ’ , ’ Torchlight II ’]

# a list containing different types of data


z = [ ’ Ultimate answer ’ , 42.0 , 6*9]
 

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

10.2.3 Accessing List Items


Because lists are sequences, we can use indexing and slicing on lists in exactly the same way we did
for strings. We’ve discussed this behaviour before, but include it here as a reminder. For example:
 
>>> x = [2 , 3 , 5 , 7 , 11 , 13 , 17]
>>> y = [ ’ Diablo 3 ’ , ’ Path of Exile ’ , ’ Torchlight II ’ , ’ Grim Dawn ’]
>>> x [2] # the third item in x
5
>>> x [3:6] # the fourth through sixth items of x
[7 , 11 , 13]
>>> y [ -1] # the last item in y
’ Grim Dawn ’
>>> y [0: len ( x ):2] # every other item of y
[ ’ Diablo 3 ’ , ’ Torchlight II ’]
 

10.2.4 Modifying List Items


As we know, we can access individual items in any sequence using indexing with square brackets.
But with lists, we can ASSIGN a new value to an individual item in a list using indexing as well! We
can treat an indexed portion of a list like a variable and place it on the left-hand side of a variable
assignment operation. Here is an example:
 
>>> y = [ ’ Diablo 3 ’ , ’ Path of Exile ’ , ’ Torchlight II ’ , ’ Grim Dawn ’]
>>> y [0] = ’ D3 : Deluxe Edition ’
>>> y
[ ’ D3 : Deluxe Edition ’ , ’ Path of Exile ’ , ’ Torchlight II ’ , ’ Grim Dawn ’]
 
Note how the value at offset 0 was changed, and the rest of the list remained the same. This is
possible because lists are mutable. We cannot use indexing and assignment operators this way with
immutable sequences such as strings or tuples.
In fact, we can even place a slice of a list on the left-hand side of an assignment operation as
well, like this:
 
>>> L = [1 , 2 , 3 , 4 , 5]
>>> L [0:2] = [10 , 20]
>>> L
[10 , 20 , 3 , 4 , 5]
 
The slice L[0:2] referred to a subsection of L consisting of its first two elements, and so we
replaced both of those elements in a single operation. This isn’t nearly as common as replacing just
a single element though.

10.3 List Methods


The following are some key methods that are essential for using lists in a practical manner. Most
of them depend on the mutability of lists, so therefore there aren’t similar methods for immutable
sequences such as strings and tuples.

10.3.1 Adding Items to a List


Lists have a method called append which allows you to add an item to the end of the list:
88 Advanced List Usage
 
>>> x = [2 , 3 , 5 , 7 , 11 , 13 , 17]
>>> x . append (19)
>>> print ( x )
[2 , 3 , 5 , 7 , 11 , 13 , 17 , 19]
>>> x . append (23)
>>> print ( x )
[2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23]
 
If you want to add several items to a list all at once, you can use the extend method:
 
>>> x = [2 , 3 , 5 , 7 , 11 , 13 , 17]
>>> x . extend ([19 , 23 , 27]) # Add 19 , 23 , and 27 to end of x .
>>> print ( x )
[2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 27]
 
x.extend([19,23, 27]) is equivalent to doing:
 
for i in [19 , 23 , 27]:
x . append ( i )
 
which is not the same as x.append([19,23,27])! Note the important difference between extend
and append when you try to append a list:
 
>>> x = [2 , 3 , 5 , 7 , 11 , 13 , 17]
>>> x . append ([19 , 23 , 27]) # append [19 ,23 ,27] as a single item of x
>>> print ( x )
[2 , 3 , 5 , 7 , 11 , 13 , 17 , [19 , 23 , 27]]
 
The list [19,23,27] is appended as a single item of the list x! That is, the 8-th item in the list x is
not an integer, it is another list containing the items 19, 23, and 27! This is an example of a nested
list. A nested list is when you have an entire list as a single item in another list. We’ll see more
examples of this soon.
10.3 List Methods 89

10.3.2 Removing Items from a List


There are several options for removing items from a list. That’s because there’s more than one way
to describe the item or items that you might want to remove.
The remove method of a list deletes a specified item from the list no matter what index it
occupies. We therefore can say that remove works by removing by value.
 
>>> z = [ ’ Han ’ , ’ Chewie ’ , ’ Luke ’ , ’ Leia ’ , ’ C3PO ’]
>>> z . remove ( ’ Luke ’) # delete ’ Luke ’ from the list
>>> print ( z )
[ ’ Han ’ , ’ Chewie ’ , ’ Leia ’ , ’ C3PO ’]
 
If there are multiple occurrences of the specified item in the list, the occurrence with the smallest
index is removed, but the other occurrences remain.
By contrast, the pop method deletes an item based on its index in the list. We can call this
approach removing by index. pop also returns the item that was just deleted.
 
>>> z = [ ’ Han ’ , ’ Chewie ’ , ’ Luke ’ , ’ Leia ’ , ’ C3PO ’]
>>> gone = z . pop (0)
>>> print ( gone , " is gone ! " )
Han is gone !
>>> z
[ ’ Chewie ’ , ’ Luke ’ , ’ Leia ’ , ’ C3PO ’]
 
In this example, the string ’Han’ was removed from the list because it was located at index 0, the
argument given to pop(). If you call pop() with NO arguments, by default it removes the last item
in the list.
To delete a slice from a list all at once, you can use the del operator. The slice can be of size 1:
 
>>> z = [ ’ Han ’ , ’ Chewie ’ , ’ Luke ’ , ’ Leia ’ , ’ C3PO ’]
>>> del z [1] # delete the second item in z
>>> print ( z )
[ ’ Han ’ , ’ Luke ’ , ’ Leia ’ , ’ C3PO ’]
 
...or the slice can be bigger:
 
>>> z = [ ’ Han ’ , ’ Chewie ’ , ’ Luke ’ , ’ Leia ’ , ’ C3PO ’]
>>> del z [1:4]
>>> print ( z )
[ ’ Han ’ , ’ C3PO ’]
 
Note that del is a little funny: syntactically it is an operator like + and *, not a function or method,
so round brackets, e.g. del(z[1]), should not be used.

10.3.3 Locating an Item in a List


We can determine the position (or index) of a value in a list using the index method.
 
>>> L = [ " a " , " b " , " c " , " d " ]
>>> L . index ( " c " )
2
 
90 Advanced List Usage

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.3.4 Sorting the Items in a List


If the items in a list are all comparable with one another, the list can be sorted using the sort method.
The sort method rearranges the items in the existing list and does not create a new list. Numbers
are sorted in numeric order:
 
>>> import math as m
>>> numbers = [42.0 , 7 , m . sqrt (12) , -17 , -42 , m . pow (2 ,16)]
>>> numbers . sort ()
>>> print ( numbers )
[ -42 , -17 , 3.4641016151377544 , 7 , 42.0 , 65536.0]
 
Strings are sorted in lexicographic order (dictionary order):
 
>>> words = [ ’ what ’ , ’ is ’ , ’ dead ’ , ’ may ’ , ’ never ’ , ’ die ’]
>>> words . sort ()
>>> print ( words )
[ ’ dead ’ , ’ die ’ , ’ is ’ , ’ may ’ , ’ never ’ , ’ what ’]
 
A list with both numbers and strings cannot be sorted, because strings cannot be compared to
numbers; the result is a type error:
 
>>> stuff = [6 , ’ multiplied ’ , ’ by ’ , 9 , ’ is ’ , 42]
>>> stuff . sort ()
Traceback ( most recent call last ):
File " < stdin > " , line 1 , in < module >
TypeError : unorderable types : str () < int ()
 

10.3.5 Copying Lists


Recall that the assignment operator, =, associates a variable name (also called an identifier) with a
piece of data. We have likened it to putting a sticky note on a data value. Suppose we did this:
 
x = 42 # associate the identifier x with the value 42
y = x # associate the identifier y with the value 42
 
How many copies of the value 42 are there? Only one. We did not create two copies of 42, we
simply assigned two different names to the same value. If we later associate x with a different value,
it doesn’t change the fact that y is still associated with the value 42, so in that sense, y’s value didn’t
change as a result of changing x.
But things are a bit different with mutable sequences. Firstly, if we do this:
 
x = [2 , 4 , 6 , 8 , 10]
y = x
 
Then this is no different from the previous example — we have simply assigned two variable names
to refer to the same list. But because lists are mutable sequences, this has some subtle consequences.
What if we change just the third item of x to −10?
10.4 Concatenation 91
 
x [2] = -10
 
Does this change the list referred to by y? The answer is: yes, because x and y refer to the same list.
If we now print out the value of y[2] the value −10 will be printed, and if we print x and y, we see
that they are still, indeed, the same list.
 
>>> print ( y [2])
-10
>>> print ( x )
[2 , 4 , -10 , 8 , 10]
>>> print ( y )
[2 , 4 , -10 , 8 , 10]
 
To return to our sticky note metaphor, x and y are just two sticky notes stuck to the same thing. If
you change the thing, it doesn’t matter which sticky note you use to refer to it. Lists are the first data
type we have encountered where it is possible to "change the thing", via the techniques we have
discussed in this chapter. With all previous data types (which were immutable), all we could do was
make a NEW thing, and then move one (or more) of the sticky notes to the new thing.
If you want an actual copy of a list, you can use its copy method. This will produce a second
different list, that contains the same data items as the original list, but which can be modified without
causing the original list to be changed:
 
>>> x = [2 , 4 , 6 , 8 , 10]
>>> y = x # y and x refer to the same list
>>> z = y . copy () # z refers to a copy of list y
>>> z [2] = -10 # change something in list z
>>> print ( x ) # change to z does not affect list x
[2 , 4 , 6 , 8 , 10]
>>> print ( y ) # or list y ( x and y are the same list )
[2 , 4 , 6 , 8 , 10]
>>> print ( z ) # only list z is changed since it was
[2 , 4 , -10 , 8 , 10] # a copy of y .
 
The important thing to remember is that the assignment operator = does not make a copy of data.
It only associates a new name with that data. Many mutable compound data objects, including lists,
provide methods to create copies of themselves.

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

10.5 Iterating Over the Items of a List


We often want to perform some kind of computation for every item in a list (or other sequence). This
is called iterating over the list, and requires combining lists with loops.
There are two fundamental ways of accessing each item in a list, one at a time. The first is to
use the default behaviour of for-loops to access each item in the list directly, like so:
 
L = [ ’ Tony was chased ’ , ’ Bruce was angered ’ , ’ Steven was scared ’]

# print each string in the list L


for x in L :
print ( x )
 
With this form of loop, the loop variable (x in this case), is just an ordinary variable that gets
associated with each data item in the list, one at a time. This is excellent if we just want to do
something simple with each element, like printing it out. However, we cannot use this form of loop
to modify the items in a list. For example:
 
L = [1 , 2 , 3]
for num in L :
num = num + 1
print ( L )
 
The code above will print [1, 2, 3], showing that L has not been modified. This is because the data
items in the list are integers, and integers, like all atomic data, are immutable. Adding 1 to an integer
creates a new value, which temporarily gets associated with the variable num, and then disappears
when num is automatically assigned the next value in the sequence by the for-loop iteration.
The alternative approach is to iterate over a sequence of numbers, and use those numbers as an
index to access the items in the list using the square bracket notation.
 
L = [ ’ Tony was chased ’ , ’ Bruce was angered ’ , ’ Steven was scared ’]

# Append the phrase ’ by zombies ’ to each item of L .


for i in range ( len ( L )):
L [ i ] = L [ i ] + ’ by zombies . ’
 
After executing the above code, the list L will have been changed to:
 
[ ’ Tony was chased by zombies . ’ , ’ Bruce was angered by zombies . ’ ,
’ Steven was scared by zombies . ’]
 
10.6 Nested Lists 93

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.

10.6 Nested Lists


In Section 10.3.1 we encountered the concept of nested lists. The idea is that a data item in a list
could be another list. Suppose we are designing a video game and want to store data about all the
different magic items that a player might find. Further suppose that the data we need to store for each
such magic item is its name, its value (in gold pieces, of course!), and the minimum level a character
needs to achieve before they can use it. We could represent these three pieces of information for a
single magic item as a list. For example, the list:
[’Sword of Fighting’, 1250, 10]
stores the data for a magic item with the name “Sword of Fighting”, that is worth 1250 gold pieces,
and can only be used by characters of level 10 or higher.
Now imagine we want to store data about all of the magic items in our game. We could do this
using a list of lists, where each item in the list is another list consisting of the magic item’s name,
value, and minimum level. Here’s an example of a list consisting of three magic items:
[ [’Sword of Fighting’, 1250, 10], [’Scroll of Conjure Milk’, 20, 5],
[’Yellow Wizard Robe’, 100, 3] ]
How do we know that this a list of lists? Notice the positioning of the square brackets. There is a set
of square brackets enclosing the entire thing that tells us that the whole thing is a list. Within the
outer pair of square brackets, we have three more lists enclosed in pairs of square brackets, each
separated by a comma. To help you see this, each item in the list has been shown in a different colour.
Thus we have a list of three items, each of which is, itself, a list. We can build up fairly complicated
organizations of data just by using nested lists. Lists can be nested to any depth desired.

10.7 List Comprehensions


List comprehensions offer a convenient syntax for creating more complex lists. One way we can use
list comprehensions is to select some items of a list to put in a new list. This is best illustrated with
an example. Suppose we had a list of magic items, like in the previous section, and we wanted to
construct a new list consisting of only the magic items whose value is greater than 500 gold pieces.
We could use a for-loop, like this:
 
# 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] ]

# an empty list for storing pricey items


expensive_loot = []

# for each magic item in the list ’ loot ’, if it has


# a value > 500 , put it in the list of expensive loot
94 Advanced List Usage

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] ]

expensive_loot = [ x for x in loot if x [1] > 500 ]


 
Both of these programs result in expensive_loot referring to the following list:

[[’Sword of Fighting’, 1250, 10], [’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.

10.8 Summary of List Methods


We include a summary of all the list methods we have discussed in this chapter in a table format.
As usual, this summary is non-exhaustive; more methods can be found if you google the Python
language documentation.

Name Number of Description and Example


Arguments
append 1 Appends a data item to the end of the list
extend 1 Appends all items in a given list to the end of the list
remove 1 Removes a given value from the list
pop 0 or 1 Removes and returns the value at a given index from the list
index 1 Returns the index of the first occurrence of an item in a list
sort 0 Sorts the list in ascending order
copy 0 Creates a copy of the list
Part III

The Practice of Computing


Software Engineering
Goals of Software Engineering
Software Design Workflow
Documentation
Function Docstrings
Single-Line Comments

11 — Software Design and Documentation

Learning Objectives

After studying this chapter, a student should be able to:

• distinguish software engineering from computer science


• define and distinguish correctness and robustness
• design programs using an algorithm-pseudocode-code workflow
• write complete docstrings for functions
• write good in-line comments

11.1 Software Engineering


Software Engineering is the study and creation of techniques that lead to best practices for building
software in real settings. Although it is typically studied by computer scientists and is often interlaced
throughout an undergraduate computer science program, it can be considered distinct from computer
science. Pure computer science is, at its heart, highly mathematical in nature, while software
engineering is necessarily empirical, relying on wisdom and experience from others with regard
to "what has worked in the past"1 . We can mathematically prove bounds on the efficiency of an
algorithm (a computer science task), but we cannot mathematically prove that a single-line comment
is useful (a software engineering task). How best to organize your code, how to organize teams of
people to work productively on code, how to write software such that it is easily extensible in future
and won’t quickly become obsolete; these are all the domain of the software engineer.
Since this is the textbook for "Computer Science for Engineers", one final note: in spite of the
name, software engineering has nothing to do with general engineering and you won’t learn it at all
in your engineering classes!
1 If
you play table-top role-playing games, perhaps we could say that computer science is an INT skill, while software
engineering is a WIS skill...?
100 Software Design and Documentation

11.1.1 Goals of Software Engineering


Software Engineering is concerned not only with just "getting the work done", but also with various
desirable properties of the software that we write. Two such important properties are correctness and
robustness.
We say that a program (or a function, or even just a code snippet) is correct if it does everything
it is supposed to do and nothing that it ISN’T supposed to do. The latter point is the more subtle one,
and is especially key to computational thinking. If you write a function to return the largest value in
a list, and the function returns the correct value but in so doing also deletes all the data in the list,
then the function is not correct.
Correctness is largely about programs producing correct output when supplied with valid inputs.
By contrast, we say a program (or a function) is robust if it perforrms reasonably even when supplied
with invalid inputs. For example, say you are filling out a web form that is asking you to create a
password that consists of only letters and numbers. If you include some illegal characters in your
password and the browser reloads the form, flagging the password field and asking you to try again,
that is robust behaviour. If instead your web browser had crashed as a result of the illegal password,
that is not robust behaviour.
For introductory programming in general, making programs that are correct is hard enough for
beginners, so that’s largely what we focus on! For now, it’s sufficient for robustness to be an issue
that you keep in the back of your mind as you encounter programming problems. If you take more
computer science, you will also encounter further software engineering design goals beyond just
correctness and robustness as well.

11.1.2 Software Design Workflow


As the problems that we are writing software to solve become more complex, a good workflow
becomes essential. Not only will a good workflow save time in the long run, it can make you more
likely to achieve various software engineering goals without having to explicitly worry about them.
It can be tempting to start any software project by leaping in and starting to write code right away,
but for more complex problems, this is a recipe for disaster. Instead, it is better to first describe, at a
high level, the algorithm that you plan to use to solve the problem. By a ’high level’, we mean that
the algorithm should not make reference to specific programming language constructs or functions,
but should be written such that even a non-programmer could understand it.
The next step is to refine your algorithm into something we call pseudocode. Pseudocode looks
a lot like computer code and can make reference to variables, loops, and so on, but it is not specific
to any one programming language. A non-programmer may not understand the pseudocode, but any
programmer should, no matter if they know the actual programming language in which you will
ultimately implement the software.
Finally, you then go on to write the actual code in your programming language of choice (in our
case, Python). We’ll call this approach the algorithm-pseudocode-code workflow.
For example, consider the problem of computing the average of a list of numbers. Here’s an
appropriate algorithm, which we would compose as the first step in the algorithm-pseudocode-code
workflow.
 
sum all the values in the list
divide the sum by the number of values
 
11.2 Documentation 101

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

divide the sum by the length of L


return the sum
 
Although pseudocode is meant to look a lot like code, there are no formal rules for it (indeed,
if there were, then it would itself be a programming language!). The important thing is to lay out
the steps in the high-level algorithm in a way that matches what most computers are able to do in a
single step, but without yet committing to a particular language.
Finally, we can then write a Python implementation of this pseudocode.
 
def average ( L ):
s = 0
for val in L :
s = s + val

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

11.2.1 Function Docstrings


We already mentioned much earlier, back in chapter 6, the importance of describing functions that
you write with docstrings, but now we’ll revisit that topic in slightly more detail.
As a reminder, a docstring is a multi-line comment enclosed in triple-quotes that comes immedi-
ately after a function’s header (i.e. the line that contains def). It should contain information about
all of the following:
• the function’s purpose
• the function’s expectations about its parameters
• the meaning of the function’s return value (if any)
The purpose should describe, as concisely as possible, what the function does. It does NOT
need to describe how the function does it. The following is a bad example of a purpose for our
average() function.
 
def average ( L ):
" " " adds up numbers using a for - loop and a variable
called s for computing the average
"""
s = 0
for val in L :
s = s + val

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

returns : the average of all values in L as a float


"""
 
For other sorts of functions, the semantics are the most crucial information. Consider a function
named getDirections() for a GPS device in your car. Such a function might expect four parame-
ters: the latitude and longitude of the car’s current location, and the latitude and longitude of the
destination. Knowing the expected data types for these parameters is not enough; we need to know
which values are which in order to call the function productively. If we swap the longitude with
the latitude, or the location with the destination, the directions we get back are not going to be very
good! A complete docstring for such a function could look like this:
 
def getDirections ( long_source , lat_source , long_dest , lat_dest ):
" " " computes driving instructions for how to reach
the destination from the source

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

returns : a string consisting of the next upcoming


direction to present to the vehicle ’s driver
"""
 

11.2.2 Single-Line Comments


Recall that single-line comments are denoted with the # symbol and are used to improve the
readability of your code. Code without comments can be difficult for a third party to read, or even for
yourself if you come back to it after a few days or weeks. However, too many comments, especially
if they are trivial and unhelpful, can be as bad as none at all. It’s extremely common for novice
students to write comments of this latter nature, especially if they are told "your code must have
comments or else it will lose marks". Here is an example of very bad comments for our average()
function.
104 Software Design and Documentation
 
def average ( L ):
# initialize sum to 0
s = 0
# for loop over all numbers in L
for val in L :
# add each number to s
s = s + val

# divide sum by length of L


avg = s / len ( L )
return avg
 
These comments are terrible, because they are simply stating in English what each line of code
does. This is redundant, making the code twice as long to read without adding new information
of any sort. Instead, single-line comments should aim to describe why you wrote certain lines of
code. What were you trying to achieve? Comments of that nature not only give insight into the
programmer’s thought process, but also make it possible to improve the code in future. Since you
know the purpose of each segment of code, you could conceivably come back and replace that
segment with code that does the same thing, but more efficiently.
Another extremely common novice behaviour is to write their comments as the very last thing
that they do while composing a program.2 One likely reason for this is that novices are often not
especially confident that the code they write is going to work. They think if they have to change the
code a lot, they’ll have to change the comments too, and this sounds like double the work.
Following the algorithm-pseudocode-code workflow helps you avoid this trap! Typically, the
algorithm that you write as the first step of this workflow actually makes for the perfect set of
comments for your code. The algorithm should be written at a high enough level that you are
confident it is correct even if you struggle to implement the details and get the code to ’work’. And
if you are NOT confident the algorithm is correct, you have no business starting to code it yet!
Using this approach, here’s an example of good comments for our average() function:

2 Be honest. You’ve done it. You totally have.


11.2 Documentation 105
 
def average ( L ):
# sum all the values in the list
sum = 0
for val in L :
sum = sum + val

# divide the sum by the number of values


avg = sum / len ( L )
return avg
 
Notice that the comments are exactly the same as the algorithm we first wrote in section 11.1.2,
and each one comes immediately before the lines of code that implement that step of the algorithm.
Overview
Verification and Validity
Testing and Debugging
Testing
Standard Form of Test Cases
Test Case Generation: Black-Box Testing
Test Case Generation: White-Box Testing
Implementing Tests
Debugging
Debugging by Inspection
Debugging by Hand-Tracing Code
Integrated Debuggers
Summary

12 — Testing and Debugging

Learning Objectives

After studying this chapter, a student should be able to:

• define and distinguish verification and validity


• define and distinguish testing and debugging
• explain what a fault is
• explain what a test case is and list its components
• generate test cases using the black-box method
• generate test cases using the white-box method
• write test drivers that implement test cases and report faults
• identify three methods for debugging

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.

12.2 Verification and Validity


Verification and validity are two separate concepts relating to program correctness.
Verification is asking the question "Did I build the correct thing?" To verify a piece of software,
you are ensuring that what the software actually does is a good match to what the eventual end user(s)
wanted it to do. This is relatively easy to do in an introductory class, as instructors tend only to give
108 Testing and Debugging

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.3 Testing and Debugging


Testing and debugging are processes to validate a program. If a program does not behave as expected,
this is called a fault. Faults are caused by errors (sometimes called bugs, both for historical reasons
and to make them sound less serious). Faults and errors are distinct, though there is a strict causal
relation between the two. A fault is the erroneous behaviour that you actually observe. For example,
the program crashing when the user types in a negative number might be a fault. The matching error
is the line(s) of code that need to be corrected in order to prevent the fault from occurring.
Testing is a proactive process where one specifically chooses inputs or designs usage scenarios
meant to find faults. For example, if we have a function that is supposed to find the largest value in a
list, we might test it with the list [1, 2, 3, 4, 5], and we would expect the function to return the
value 5. Any other value is the wrong answer for this list, so if it returns a value other than 5, then
we have detected a fault! We use testing to detect faults, hopefully before the software is released
and faults are detected by customers in the course of normal usage!
Debugging is a reactive process where, having detected a fault, we attempt to find and fix the
error that caused the fault. For example, in our function to find the largest value in a list, suppose we
tested with the list [1, 2, -10, 3, 4] and received a return value of -10. -10 is not the largest
value (in fact it’s the smallest!), so we have found a fault. We now have to determine why the fault
occurred, and repair it. Sometimes this can just be done by looking at the program and noticing
where we made a mistake. For our largest value function, it could easily be the case we used a <
sign when we meant to use > instead. All too often, however, the reason for a fault is not obvious.
The larger and more complex a program is, the less obvious it becomes what the cause of a fault is
likely to be. We will look at various debugging techniques that can help us find errors and thus repair
faults.

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.

12.4.1 Standard Form of Test Cases


An individual test case is defined by determining the following items:
1. the input(s) for the test case. These are the specific data values that we plan to try when testing
the code.
2. the expected output(s) for the given input(s). This is the specific output that you should get
for the given input. You must determine what this output should be WITHOUT using the
program you are testing, usually by hand!
3. the reason for the test case. This explains why you selected the inputs that you did, and
which property(ies) of those inputs are most important. Basically, what are you looking for by
selecting these inputs?
When writing test cases, we will use the following standard form:

Input(s): Description of program or function required inputs.


Output(s): Description of expected program or function outputs.
Reason: Description of reason for test case.

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.

12.4.2 Test Case Generation: Black-Box Testing


If we generate test cases for an algorithm/program/function using knowledge of only the expected
behaviour of the program or function, and without knowledge of the actual code, this is known as
black-box testing. The name is a metaphor: you imagine the code is in an opaque black box. You
may test it by feeding input into the box, and receiving output from the box and checking whether
it is correct, but you cannot see the code inside the box. Test cases are generated by considering
the different inputs that might be provided to the code including both common and rare cases. We
sometimes call the rare cases "special cases" or "edge cases".
110 Testing and Debugging

Let’s consider the following function:

 
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): [1,2,14,3,5] Input(s): [13,2,4,3,6]


Output(s): True Output(s): False
Reason: Test when there is a multiple of 7 Reason: Test when there is no element di-
in the middle of a list with many visible by 7 in a list with many
elements. [Common] elements. [Common]

Input(s): [1,2,4,3,7] Input(s): [7]


Output(s): True Output(s): True
Reason: Test when there is one element Reason: Test when the list has only one
divisible by 7 at the end of a list element that is divisible by 7.
with many elements. [Rare] [Rare]

Input(s): [14,2,4,3,6] Input(s): [9]


Output(s): True Output(s): False
Reason: Test when there is one element Reason: Test when the list has only one
divisible by 7 at the beginning of element that is not divisible by 7.
a list with many elements. [Rare] [Rare]

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!

12.4.3 Test Case Generation: White-Box Testing


When we generate test cases by looking at the code we are testing, we call this white-box testing. It is
a metaphor similar to black-box testing, but in this case we imagine that the code is in a transparent
box, and we can see everything inside.
In white-box testing we examine the code and try to think about all the different paths of
execution through the code. A path of execution simply means the individual code statements
that are actually executed during a specific run. For example, if a segment of code involves an
if/else statement, then at a bare minimum, there is one possible path for when the condition for the
112 Testing and Debugging

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
 

Input(s): [] Input(s): [1,2,7,3,5]


Output(s): False Output(s): True
Reason: Cause the for-loop to be exe- Reason: Cause the for-loop to be exe-
cuted 0 times. cuted many times; cause the if-
statement to be true and false (on
Input(s): [7] different loop iterations).
Output(s): True
Input(s): [1,2,4,3,7]
Reason: Cause the for-loop to be exe-
cuted one time; cause the if- Output(s): True
statement to be true. Reason: Cause the for-loop to be exe-
cuted the maximum number of
times (for the given input); cause
the if-statement to be true and
false (on different loop itera-
tions).

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.

12.4.4 Implementing Tests


Once we have a suitable set of test cases, we have to write (i.e. implement) code that actually runs the
tests so that we can see if the tests detect any faults. This takes the form of a program that contains
the implementation of the test cases. Such a program is called a test driver.
Recalling that each test case lists an input and an expected output, we implement each test case
by invoking the code to be tested with the listed input, and checking whether the received output is
the expected output. Recall also that the expected output is the answer that SHOULD be produced
by the given inputs according to your calculations by hand, not according to the code as it currently
stands (which may contain errors!). A fault is detected when a test case does not produce its expected
output. If a test case implementation detects a fault, the fault must be reported by printing a message
to the console. If a test case implementation does not detect a fault, it should output nothing. In this
way, a test driver only reports on failed test cases. Results of successful test cases are not reported
— this makes it easy to see if any faults were detected after running the test driver. If you see output,
there’s a problem!
Example Test Case Implementations
Implementation of test cases proceeds in the same way regardless of which method (white-box or
black-box) was used to identify the test cases. Here we will demonstrate the implementations of some
test cases that we identified with white-box testing in Section 12.4.3 for the is_divisible_by_7
function. Each test case is shown, and then followed by its implementation.

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.

12.5.1 Debugging by Inspection


Sometimes the cause of a fault is obvious once the existence of the fault is detected. In such cases,
once can just inspect the code and make the necessary adjustments to repair the fault.
Quite often, it is useful to output to the console (using print) the values of variables/data that
are used in the code causing the fault. All too often a fault occurs at one line of code because some
data was computed or stored incorrectly at an earlier point and it is necessary to trace the cause of
the fault back to that point by inspecting the data along the way.

12.5.2 Debugging by Hand-Tracing Code


If inspection of the code and printing out data relevant to the fault does not help you fix it, then
we can resort to more formal debugging techniques. For small pieces of code, hand-tracing the
execution of the code will usually uncover the cause of a fault.
In hand-tracing we simulate the execution of a program or function on paper. We manually step
through the code one line at a time and record the value of each program variable after every line
of code, essentially executing the program “on paper”. This helps us identify the exact point in the
12.6 Summary 115

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.5.3 Integrated Debuggers


For programs/functions that are larger and/or manipulate a very large amount of data, hand-tracing
can become impractical. In such case, we can turn to an integrated debugger for help. An integrated
debugger is a feature of a code editor (like Pycharm) which allows you to have the computer
step through a program one line at a time. The computer still performs all of the execution of the
program as normal, but you get to watch it happen one line at a time. Again, this is very difficult to
demonstrate in a reading, so we’ll do a demonstration during class. For now we will briefly describe
three main features of integrated debuggers that we will demonstrate in class:
Stepping
Integrated debuggers display the program’s code, the current value of all variables defined at that
point in time, as well as which line of code is about to be executed next. At your own speed you
can repeatedly tell the debugger to execute the next single line of code. You can also choose to step
inside of function calls, or to execute an entire function call without stopping to look inside.
Inspection
Inspection allows you to dig deeper into the data associated with a particular variable. For example,
the variable inspection window allows you to look inside sequences and see, for example, what data
items are stored inside of them.
Breakpoints
Breakpoints are useful for larger programs where it is impractical to step through every single line of
the program. If you tag a line of code as a breakpoint, then normal program execution will pause
when execution reaches that line, and allow you to then inspect variables and/or begin stepping a
line at a time. You can also tell a debugger to resume uninterrupted execution of a program until
the next breakpoint is encountered. This allows you to quickly run parts of a program you aren’t
interested in (because you know the fault occurs elsewhere).

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.

1You’d be surprised how often this happens!


116 Testing and Debugging

code

no update
tests?

yes

test case
genera-
tion/update

write/update
test driver

run test
driver

faults yes
debug
detected?

no

done (for now!)

Figure 12.1: The testing and debugging process.


Dictionaries
Creating a Dictionary
Looking Up Values by Key
Adding and Modifying Key-Value Pairs
Removing Key-Value Pairs from a Dictio-
nary
Checking if a Dictionary has a Key
Iterating over a Dictionary’s Keys
Obtaining all of the Keys or Values of a
Dictionary
Dictionaries vs. Lists
Common Uses of Dictionaries
Combining Lists, Tuples, Dictionaries

13 — Dictionaries

Learning Objectives

After studying this chapter, a student should be able to:

• describe what a dictionary is;


• distinguish dictionaries from lists/tuples;
• become familiar with the various ways in which we can access and manipulate data
stored in dictionaries; and
• appreciate how lists, tuples, dictionaries may be combined to create more complex data
structures.

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

13.1.1 Creating a Dictionary


Dictionaries can be created in a few different ways. They can be literally written out like a list,
except dictionaries are enclosed in curly braces rather than square brackets. We can construct an
empty dictionary using an empty pair of curly braces:
 
# associate the variable name ’ friends ’ with an empty dictionary
friends = {}
 
We can create a non-empty dictionary by writing a comma-separated listing of key-value pairs within
a pair of curly braces. Each key-value must consist of the key, followed by a colon, followed by the
value. The following defines a dictionary with four key-item pairs; each pair is a name and an email
address:
 
# associate ’ friends ’ with some known key - value pairs
friends = { ’ Bilbo Baggins ’ : ’ burglar1@theshire . net ’ ,
’ Sauron the Great ’ : ’ greateye@mordor . gov ’ ,
’ Gandalf the White ’: ’ whitewizard@valinor . org ’ ,
’ Saruman ’ : ’ entkiller@isengard . gov ’ }
 
We can have line breaks and line indentations between key-value pairings within the curly braces
because Python ignores whitespace within curly braces (the same applies to square brackets enclosing
lists as well!).
Dictionary keys may be any immutable type. Thus, numbers and strings can be dictionary keys.
Even tuples can be dictionary keys so long as the tuple itself doesn’t contain a mutable data type or
a data item that directly or indirectly refers to a mutable type.1 Lists and dictionaries may not be
dictionary keys because they are mutable.
Dictionary values may be any type, including lists, tuples, or even another dictionary.

13.1.2 Looking Up Values by Key


Looking up values by key in a dictionary works very much like indexing a list. You write the variable
name that refers to the dictionary, then a pair of square brackets enclosing the key whose value you
want to look up.
 
>>> print ( friends [ ’ Bilbo Baggins ’ ]) # get Bilbo ’s email address
burg ler1@the shire . net
>>> print ( friends [ ’ Sauron the Great ’ ]) # get Sauron ’s email address
greateye@mordor . gov
 
If you try to look up a key that is not in the dictionary, you get a KeyError:
 
>>> print ( friends [ ’ Tom Bombadil ’ ])
Traceback ( most recent call last ):
File " < stdin > " , line 1 , in < module >
KeyError : ’ Tom Bombadil ’
 
Just as well — your friend Tom Bombadil talks a lot and doesn’t seem to serve any useful purpose.
1 Thus
(1,2,’buckle_my_shoe’) could be a dictionary key because none of the items in the tuple are mutable, but
(1,2,[3,4]) could not because the third item of the tuple is a list, and lists are mutable.
13.1 Dictionaries 119

13.1.3 Adding and Modifying Key-Value Pairs


You can add a key-value pair, or modify the value associated with a key using the same syntax as a
lookup in conjunction with the assignment operator.
 
# add Haldir ’s email address to the dictionary
friends [ ’ Haldir ’] = ’ s mug_elf_531 @lothlorien . net ’

# update Saruman ’s email address


# ( This is an update since key ’ Saruman ’ is already in
# the dictionary )
friends [ ’ Saruman ’] = ’ b ag_ en d_ squ at te r@t he sh ire . net ’
 
Adding and modifying keys look very much the same. If the key already exists in the dictionary,
the existing key becomes associated with the new value on the right of the assignment operator.
Otherwise, the key is added to the dictionary and becomes associated with the value to the right of
the assignment operator.

13.1.4 Removing Key-Value Pairs from a Dictionary


The del operator, which we previously used to delete items from lists, can be used to remove a
key-value pair from the dictionary.
 
# remove the pair with key ’ Saruman ’ from the dictionary
del friends [ ’ Saruman ’]
 
If you want to remove all of the keys from the dictionary, call the dictionary’s clear() method:
 
# remove everyone ’s email address from the dictionary
friends . clear ()
 

13.1.5 Checking if a Dictionary has a Key


The in operator can be used to determine if a dictionary has a particular key.
 
if ’ Sauron the Great ’ in friends :
print ( ’ Yeah , I am friends with Sauron ’)
else :
print ( ’ Sauron is not my friend . I hope his tower collapses . ’)
 

13.1.6 Iterating over a Dictionary’s Keys


You can iterate over all keys in a dictionary, and do something with each key’s corresponding value
using a for-loop.
 
spam_addresses = []
for k in friends :
# Add all my friends email addresses to list of spam recipients .
spam_addresses . append ( friends [ k ])
 
It is important to note that there is no guarantee on the order in which each key of friends is
processed in such a loop.
120 Dictionaries

13.1.7 Obtaining all of the Keys or Values of a Dictionary


All dictionaries have a keys() method which returns a special type of sequence (not a list!) contain-
ing all of the keys from all of a dictionary’s key-value pairs.
 
>>> friend_names = friends . keys ()
>>> print ( friend_names )
dict_keys ([ ’ Bilbo Baggins ’ , ’ Sauron the Great ’ , ’ Gandalf the White ’ ])
 
There is no guarantee of the order in which the keys appear in the returned sequence. For example,
if someone else were to call friends.keys() on their own Python installation, ’Gandalf the
White’ may occur prior to ’Bilbo Baggins’ in the resulting sequence!
Similarly all dictionaries have a values method that returns a special type of sequence (again,
not a list!) containing all of the values from all of a dictionary’s key-value pairs.
 
>>> friend_values = friends . values ()
>>> print ( friend_values )
dict_values ([ ’ bu rgler1@t heshire . net ’ , ’ greateye@mordor . gov ’ ,
’ wh i te w i za r d@ v a li n or . org ’ ])
 
Why would we want to use these functions? We’d want to use them to iterate over the keys or
values of a dictionary using a for-loop. In order to do this, we’d need a sequence containing all
of the keys or values in a dictionary. In this example, we print friends’ key-value pairs in sorted
key-order:
 
friend_names = friends . keys ()
for k in sorted ( friend_names ):
print (k , ’: ’ , friends [ k ])
 
The output of this code is:
 
Bilbo Baggins : burgler1@theshire . net
Gandalf the White : whitewizard@valinor . org
Sauron the Great : greateye@mordor . gov
 
The sorted function is a built-in Python function that can sort both immutable and mutable
sequences. It takes a sequence as an argument and returns a new list2 containing the items from its
argument in sorted order. This behaviour is slightly different from the sort method of a list which
modifies the existing list so that it is sorted.

13.1.8 Dictionaries vs. Lists


Dictionaries are similar to lists in the following ways:
• both are containers that hold a collection of data items;
• both allow storage of data items of different types; and
• both allow you to look up individual data items.
Dictionaries are different from lists in the following ways:
• there is no ordering of the key-value pairs stored in a dictionary, whereas items in a list are in
a specific order; and
2 It’s always returns a list, regardless of the type of sequence being sorted.
13.1 Dictionaries 121

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

13.1.9 Common Uses of Dictionaries


In this section, we will discuss some common data storage patterns that can be realized with
dictionaries.

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

Mr. Burns lives at 1000 Mammon Ave.

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 ’ ])
 

13.2 Combining Lists, Tuples, Dictionaries


We hope, at this point, that you can appreciate how lists, tuples, and dictionaries can be used to build
up complex organizations of data. For example, in section 13.1.9 we saw how to make a database by
making the values of a dictionary another dictionary. Indeed any of these data types can contain data
items/values that are themselves lists, tuples, or dictionaries. As we proceed through this course (and
subsequent courses should you continue in computer science), we’ll repeatedly encounter this idea
of combining data types to organize data in interesting and useful ways.
Data File Formats
Common Text File Formats
File Objects in Python – Open and Closing
Files
Reading Text Files
Reading List Files
Reading Tabular Files
Writing Text Files
The write() method.
Writing List Files
Writing Tabular Files
Pathnames

14 — File I/O

Learning Objectives

After studying this chapter, a student should be able to:

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

14.1 Data File Formats


The term file format refers to the way in which data is organized in a file. There are two main types
of file formats: text file formats and binary file formats.
Text file formats are readable by humans. You can open them in any text editor and see the
data inside and how it is organized. In text files, numbers are stored as strings of digits. A text file
containing data about cities and their average annual high temperatures might look like this:
 
Saskatoon 9
Vancouver 14
Winnipeg 9
Toronto 13
 
126 File I/O

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.

14.1.1 Common Text File Formats


In this section, we review two typical ways in which we might organize data in a text file.

List Files: One Data item Per Line


A list file consists of one data item per line, and usually each line contains the same type of data. List
files are very simple to read into a program since each line of the file contains one data item, and
most programming languages have built-in functions for reading one line from a file. An example of
a list file might be observations of temperature recorded over a single day:
 
-2.7
-1.8
0.3
2.4
3.5
5.9
 

Tabular Files: One Group of Related Data Items Per Line


A tabular file format is one where there is a fixed number of data items per line. We can think of
such a file as a table, because it will have a certain number of rows (lines) and a certain number of
columns (data items per line).
The data items on a line may be different types, but typically each column of data is all of the
same type, that is, the n-th piece of data on each line is of the same type. Data items on a line might
be separated by spaces, or another character, such as a comma. Whatever character is used to indicate
separation of data items on a line of the file is called the file’s delimiter. It delimits (separates) one
data item from the next. Here is an example of a tabular text file, delimited by commas, where each
line holds data from an entry in the database from Section 13.1.9:
 
Homer J . Simpson , Homer , Simpson ,J ,742 Evergreen Terrace , Springfield , Unknown , USA
Charles M . Burns , Charles , Burns ,M ,1000 Mammon Ave . , Springfield , Unknown , USA
Ned Flanders , Ned , Flanders , ,744 Evergreen Terrace , Springfield , Unknown , USA
 
14.2 File Objects in Python – Open and Closing Files 127

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.

14.2 File Objects in Python – Open and Closing Files


In Python we interact with files on the disk via an abstraction. We can ask Python to return an object
that allows us to interact with a data file on disk. This is called opening a file. We can open a file and
obtain an object for that file using Python’s built-in open function. The open function returns an
object that contains methods that allow us to read and write data to or from a file. Suppose the table
of temperature data, above, is stored in a file called temperatures.txt. We can open it like this:
 
f = open ( ’ temperatures . txt ’ , ’r ’)
 
The first argument to open is a string containing the name of the file to be opened — this can be any
valid pathname (see Section 14.5 for more details on pathnames). The second argument string is the
mode. Here we are using the file mode ’r’, to indicate that we want to read from the file. Later
we’ll see how to write to files using the ’w’ mode. Now f is an object that contains methods that
allow us to manipulate the file. This is a nice abstraction because we can work with the file just by
calling methods of f and we don’t have to have any idea how disks and filesystems work. One other
interesting thing about file objects is that they behave as sequences, which means we can use them in
places where we could use sequences! We’ll see how this works in the next few sections.
Before we move on, we must note that once a file is opened, it must be closed again when you
are done with it. If f refers to a file object created with open, it is closed by calling the close
method of f:
 
f . close ()
 
Once you call f.close(), f can no longer be used to manipulate the file; trying to do so will result
in an error message. If you forget to close a file that was opened in read mode, usually nothing bad
will happen, although you really should always do it. If you forget to close a file that was opened in
128 File I/O

write mode, it is possible that the data you wrote to the file will not actually be written, and that is
very bad!

14.3 Reading Text Files


In the previous section, we mentioned that file objects returned by the open function behave like
sequences. In particular, they behave like sequences of strings, where each string is a line of the file.
This means that we can iterate over the lines of a file just like we can iterate over the elements of a
list!

14.3.1 Reading List Files


List files are pretty easy to deal with since each line of a file contains a single data item and, as we
have already mentioned, we can access each line of a file as a string easily.
Suppose we have a file called movietitles.txt which contains one movie title per line. We
can read the movie titles from the file and store them in a Python list like this:
 
# Open the file for reading
f = open ( ’ movietitles . txt ’ , ’r ’)

# create an empty list


titles = []

# iterate over each line of the file


for line in f :
# append the next line ( movie title ) to the list
titles . append ( line )

# close the file


f . close ()
 
If movietitles.txt contains the following data:
 
The Fellowship of the Ring
The Two Towers
The Return of the King
 
Then the above code will result in titles referring to the list:
 
[ ’ The Fellowship of the Ring \ n ’ , ’ The Two Towers \ n ’ , ’ The Return of the King \ n ’]
 
Hey, wait, that’s weird. What are those \n’s at the end of each string in the list? Those are
newline characters; they are invisible characters that mark the end of each line in a text file, and
therefore are included in the string that comprises a line of the file. In Python, \n represents the
newline character. Even though it is represented by two characters, \ and n, it is actually a single
character. It is represented this way so that we can see it because normally it is invisible since it is
not associated with any symbol.
Usually we don’t want newline characters in our strings. We can remove them by calling the
string method rstrip. If s refers to a string, then s.rstrip() returns a copy of s that has all of
14.3 Reading Text Files 129

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

14.3.2 Reading Tabular Files


Reading tabular files is almost the same as reading list files. The main difference is that we have
to separate the data items on each line. Remember that the data items on each line are separated
by a delimiter. String objects have a split method which returns a list of strings consisting of the
individual strings that occur between a specific delimiter character. For example, the string ’The
king in the north.’ can be separated into individual words like this:
 
my_string = ’ The king in the north . ’
words = my_string . split ()
 
This results in words referring to the list:
 
[ ’ The ’ , ’ king ’ , ’ in ’ , ’ the ’ , ’ north . ’]
 
If we want to split a string based on a delimiter other than whitespace, we just pass the desired
delimiter to split as an argument. Here’s how we can obtain a list of strings from a string delimited
by commas:
 
my_string = ’ 42 ,38 ,27 ,99 ,55 ’
numbers = my_string . split ( ’ , ’)
 
This results in numbers referring to the list
 
[ ’ 42 ’ , ’ 38 ’ , ’ 27 ’ , ’ 99 ’ , ’ 55 ’]
 
They’re still strings, but we’ve already seen how we can use a list comprehension to convert this to a
list of integers or floats.
We can obtain the lines of a tabular data file in the same way that we obtained lines for list files,
but then we have to use split to divide up each line into its individual data items. A common way
to store the data from a tabular file in Python is a list in which each data item is another list that
contains the data items from one line of the file, i.e. a list of lists. Recall the temperature data in the
tabular file we saw in Section 14.1.1:
 
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 following code reads this data and stores it as a list of lists of integers:
 
f = open ( ’ temptable . txt ’)
stations = []
for line in f :
stations . append ([ int ( n ) for n in line . split ()])
f . close ()
 
Observe how we read each line, split it (using whitespace as a delimiter), then convert the resulting
list of strings into a list of integers, then append that list to the list stations. This causes stations
to refer to the list:
14.4 Writing Text Files 131
 
[
[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] ,
]
 
Observe that stations[i] refers to the data in the i-th line of the file, and stations[i][j] refers
to the item in the j-th column of the i-th line of the file. Thus, stations[1][4] refers to the file
data found at the fifth column of the second line, which is 7.

14.4 Writing Text Files


To write to a file, you have to open it in write mode:
 
f = open ( ’ file_to_write . txt ’ , ’w ’)
 
If a file is opened in write mode, and a file of the same name already exists, then the existing file is
destroyed, and a new file of the same name replaces it. If the file opened for writing does not exist
yet, it is created.
It is possible to write data at the end of an existing file without destroying it. To do so, open the
file in append mode:
 
f = open ( ’ file_to_write . txt ’ , ’a ’)
 

14.4.1 The write() method.


Writing data to text files is very similar to printing to the console. First you have to open a file in
write or append mode. Then, instead of using the print function, you use the write method of the
resulting file object. If the variable f refers to a file object, and the file was opened in write mode,
then the code
 
f . write (string )
 
writes the the string string to the file. The write method does not write a newline character to the
file unless the string given as an argument includes one. Note that this behaviour is different from
the print function which, by default, always outputs a newline after printing its argument.

14.4.2 Writing List Files


List files can be written by writing each data item followed by a new line. If we have a list of strings,
we can write those strings, one per line, to a file called shoppinglist.txt like this:
 
ingredients = [ ’ eggs ’ , ’ milk ’ , ’ flour ’ , ’ yeast ’]
f = open ( ’ shoppinglist . txt ’ , ’w ’)
for i in ingredients :
f . write ( i + ’\ n ’)
f . close ()
 
132 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.4.3 Writing Tabular Files


To write a tabular file, a typical strategy is to construct a string consisting of one line of the tabular
file to be written, and then write it. This is done by combining the data items to appear on that line
into a single string, separated by the appropriate delimiter. Just as we had a method, split, that
could separate a delimited string, we have one that can construct a delimited string from a list of
individual data items. String objects have a method called join. This method takes a list as an
argument and returns a new string that consists of the items in the list separated by the original
string. Remember: the string on which we call the join is the separator, and the list provided as an
argument to join contains the data items to combine.
Suppose we have a list of numbers numbers which should all appear on one line of a tabular file,
separated by commas. We can construct the appropriate string to write to the file like this:
 
numbers = [42 , 24 , 87 , 21 , 76]
line = ’ , ’. join ([ str ( x ) for x in numbers ])
print ( line )
 
This produces the following output string:
 
42 ,24 ,87 ,21 ,76
 
Look what’s happening here. The list comprehension [str(x) for x in numbers] converts the
list of integers numbers into a list of strings. This list is then passed to the join method of the
string object ’,’. This causes the elements of the list to be concatenated, separated by the string ’,’.
The result is that line refers to the string ’42,24,87,21,76’ which is then output by the print
statement.
Putting all of this together, suppose we had a list of lists. We could write all the data items of
each list to a tabular file like this:
14.5 Pathnames 133
 
# a list of lists . We ’ ve seen this temperature data before .
data = [
[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] ,
]
f = open ( ’ temperaturedata . txt ’ , ’w ’)
for station in data :
f . write ( ’ , ’. join ([ str ( i ) for i in station ])+ ’\ n ’)
f . close ()
 
For each list of integers station in data, we use a list comprehension to convert the items in
station to strings and put them into a new list, then join this list of strings into a single string
with a comma as a separator, then add a newline to the end of the resulting string, and write it to the
file. This results in a tabular text file that looks like this:
 
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
 

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.

Optional Trivia Challenge

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

15 — Binary Number Systems and Logic

Learning Objectives

After studying this chapter, a student should be able to:

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

15.2 Binary Numbers


In this section we introduce the binary representation of numbers.

15.2.1 Numbers vs Numerals


First, we have to make a comment about the difference between a number and a numeral. This
is a subtle distinction, but it will help us a bit to talk about binary numbers unambiguously. A
number is quantity, an amount. A numeral is a symbol used in the representation of a number. The
number associated with the English word “three” is a quantity which can be expressed in a variety of
ways, that is, through various representations. For example we could represent the number “three”
in base 10 (decimial) with the Arabic numeral 3. The ancient Romans used a system of number
representation with numerals such as I, V, X, in which the number “three” is represented by the
string of three numerals III. The important thing in the following discussion is to not confuse the
representation of a number with its quantity. We’ll use English words for smallish numbers, and
use Arabic digits for numerals, until we think the point is clear.

15.2.2 Representation of Binary Numbers


We’ll build our understanding of binary numbers from the existing understanding we have of decimal
numbers and we’ll focus solely on positive integer numbers. Decimal numbers are expressed using
ten different numerals: 0,1,2,3,4,5,6,7,8, and 9. The fact that there are 10 numerals is the reason why
another name for decimal numbers is base 10 numbers. This is beacuse each numeral in a decimal
number represents a multiple of a power of 10. The right-most numeral denotes a multiple of 100 or
one, the second-right most numeral denotes a multiple of 101 or ten, the third right-most numeral
denotes a multiple of a power of 102 or one hundred.1 That’s why these numeral positions are
respectively called the ones, tens and hundreds columns. Thus, the decimal number 8209 represents
9 ones, plus 0 tens, plus 2 hundreds plus 8 thousands. In other words:

8209 = 8 × 103 + 2 × 102 + 0 × 101 + 9 × 100 .


1 See how the number ten is the base for the exponent in each case? That’s base-10!!!
15.2 Binary Numbers 137

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.

Decimal Binary Decimal Binary Decimal Binary Decimal Binary


0 0000 0000 1 0000 0001 2 0000 0010 3 0000 0011
4 0000 0100 5 0000 0101 6 0000 0110 7 0000 0111
8 0000 1000 9 0000 1001 10 0000 1010 11 0000 1011
12 0000 1100 13 0000 1101 14 0000 1110 15 0000 1111
16 0001 0000 17 0001 0001 18 0001 0010 19 0001 0011
20 0001 0100 21 0001 0101 22 0001 0110 23 0001 0111
24 0001 1000 25 0001 1001 26 0001 1010 27 0001 1011
28 0001 1100 29 0001 1101 30 0001 1110 31 0001 1111

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.

15.2.3 Converting from Binary to Decimal


Computers represent numbers in binary. Almost always, when we ask the computer to display a
number by printing it on the console or other output device, it automatically converts its internal
binary representation into decimal. This is very easy to do, and, in fact, we already did it a number
of times in the previous section when we added up the multiples of the powers of two that the binary
number represents.
Example: The binary number 0b1010110 has 7 bits. To convert this to decimal, we need to add
up the corresponding multiples of the powers of two from 20 = 1 to 26 = 64. Reading the binary
number from left to right, we have 1 sixty-four plus 0 thirty-twos plus 1 sixteen plus 0 eights plus 1
four plus 1 two plus 0 ones. Or, more simply:

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.

15.2.4 Addition of Binary Numbers


For addition of binary numbers, there are four basic addition facts that must be memorized:
1. 0b0 + 0b0 = 0b0
2. 0b0 + 0b1 = 0b1
3. 0b1 + 0b0 = 0b1
4. 0b1 + 0b1 = 0b10
15.2 Binary Numbers 139

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

carry no carry carry no carry

15.2.5 Multiplication of Binary Numbers


The algorithm for multiplication in binary is the same as the one you learned for multiplication in
decimal. You need to know binary addition (see previous section) and you need the following 4 facts
about multiplication of single-numeral binary numbers:
140 Binary Number Systems and Logic

1. 0b0 × 0b0 = 0b0


2. 0b0 × 0b1 = 0b0
3. 0b1 × 0b0 = 0b0
4. 0b1 × 0b1 = 0b1
Here’s an example (again, we drop the 0b flag to reduce clutter). The numerals being multiplied
at each step are shown in red. The final addition is shown in blue.

110 110 110


× 11 × 11 × 11
⇒ ⇒ ⇒
0 10 110
+ + +

110 110 110 110


× 11 × 11 × 11 × 11
⇒ ⇒ ⇒
110 110 110 110
+ 0 + 10 + 110 + 110
10010

15.2.6 Subtraction and Division


We won’t cover this directly, but you already know enough to work these out. They are not
significantly different from the decimal versions. Long division in binary is significantly easier than
in decimal. Seriously. Try it!

15.2.7 Converting from Decimal to Binary


Converting from decimal to binary is likely the only thing in this chapter that you will find that is
truly new in the sense that we need an algorithm that you probably don’t already know. Before we
begin, we want to remind you that we are only changing the representation of the number when
we convert from binary to decimal. The quantity of the number does not change when we change
representations.
Suppose we want to convert a number, let’s call it x, from decimal to binary. The conversion
algorithm repeatedly divides x by 2 using integer division, and the remainders from the integer
division at each step are collected into a sequence that forms the binary representation of the original
decimal number. The result of each division is used in the next division. The algorithm finishes
when the result of the division is zero. Here is the algorithm presented as pseudocode:
15.2 Binary Numbers 141
 
Algorithm DecimalToBinary ( x )
x : a decimal number to be converted to binary
Returns : binary representation of x

Let b be an empty string


while x > 0:
r = x % 2 # r is a remainder , either 0 or 1
add r to the left side of the string b
x = x // 2 # integer division !

if b is still an empty string : # ( i . e . , x was 0 to begin with )


return " 0 "
else :
return b
 
The sequence b is built-up from right to left from the remainders resulting from each division. The
variable r holds the remainder for the current division. The following table shows the values of x, r,
and b at the end of each iteration of the while loop in the algorithm when it is given an initial value
of x = 27.

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

15.2.8 Binary Addition and Multiplication: Connections with Logic


You may have noticed that the facts for adding and multiplying single-numeral binary numbers look
suspiciously similar to Boolean AND and OR operations. This is no coincidence and it has some
3 This
is a generalization of our algorithm! We changed it from working for only one specific base to working for any
base. Remember we talked about generalization way back in Section 6.5?
142 Binary Number Systems and Logic

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

x y x+y x×y x y x OR y x AND y


0 0 0 0 false false false false
0 1 1 0 false true true false
1 0 1 0 true false true false
1 1 10 1 true true true true

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.

15.2.9 Going Further with Number Representations


Positive integers are easy. If you want to represent negative integers and positive integers, it takes a
bit more work. In normal arithmetic using decimal numbers, we represent a negative by a dash in
front of the number. We could do the same in binary, giving us simple extensions to all the familiar
concepts.
But computers don’t do that. Ultimately, if a computer is to do arithmetic, we have to think about
how to represent the dash as a circuit. So far, we’ve been writing down binary numbers using only as
many digits as are needed. But you may remember that computers have finite memory, and that bits
(binary digits or numerals) are grouped together into bytes, words, and so forth. So let’s think about
numbers that are represented by a fixed number of bits, say 8 bits (one byte)4 . With 8 bits, every
number will always have exactly 8 numerals, using leading zeros on the left to fill the spots that
aren’t really needed to represent the number. For example, the number 0b101 would be 0b00000101
as an 8-bit number. The leading zeros have no effect on the quantity of the number. With 8 bits, we
can represent all of the positive integers from 0 to 255. It is not possible to represent a larger positive
number using only 8 bits.
To represent negative numbers, computers could use one bit of a number to indicate whether
it is positive or negative, and the remaining bits to represent the magnitude of the number. This
approach is called the signed magnitude representation. It’s a good place to start learning, but it’s
not used in modern systems, for reasons we’ll mention later. Using a bit inside the number itself
means we don’t have to add circuitry, which is good, but it does mean we have to be careful about
how to interpret numbers for use in calculations. Suppose we decide to let the left-most bit represent
whether the number is negative. If the left-most bit is 1, this will mean that the number is negative.
Under this scheme, 0b10000101 is a negative integer, and 0b00000101 is a positive integer. Notice
that we have only 7 bits to represent the magnitude of the number, from 0 to 127, so we can represent
numbers between −127 and +127 (the 7-bit magnitude can be a quantity between 0 and 127, and
the left-most sign bit determines the sign).
The signed magnitude representation has a little problem: there are two representations for zero!
Specifically there are 0b00000000 and 0b10000000 which are +0 and −0, respectively. This causes
a whole bunch of special cases to arise in the circuitry when you want to check whether a number is
equal to zero, which happens really really often! To avoid this, modern computer design uses a clever
4 Modern computers typically use 32 bits, 64 bits, or even 128 bits to represent integers.
15.3 From Boolean Operators to Propositional Logic and Beyond 143

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.

15.3 From Boolean Operators to Propositional Logic and Beyond


We are familiar with Boolean values, and Boolean operators, because they help us express conditions
in if-statements and repetition constructions like loops and recursion. They are essential for express-
ing contextual information within an algorithm. And now that we’ve looked at how numbers are
represented in binary, you should almost be able to see that numerical operations can be expressed
by electronic circuits; you have enough information to work this out yourself, but we won’t say any
more about it in this book. There are other courses for that!
The next step from Boolean logic is called propositional logic. It adds two fundamental ideas
to the mix. First: knowledge can be represented in a form that’s not too far from what we have
learned from programming with Boolean operations. Second: proofs (that is, a sequences of verbal
or symbolic reasoning steps in support of a claim) can be constructed for some statements in this
form that are unequivocally valid and correct. A statement that can be supported with a proof cannot
be doubted, and that’s a very powerful concept.
Propositional logic is one step removed from a more expressive language loftily called “the
predicate calculus”. It’s used to formalize a wider variety of arguments, for wider purposes. The
predicate calculus is powerful enough to express useful properties of interest to philosophers,
scientists, engineers, and software developers, much the same way that a programming language lets
us express computations. Formal logics are enhanced by formal reasoning techniques, sometimes
expressed as strategies, and sometimes as algorithms, with which the properties can be established
144 Binary Number Systems and Logic

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.

15.4 Common Pitfalls


When you see a binary number, like 0b1101011, you might not see it as a recognizable quantity,
and that could be confusing. But try not to think of it as a word that you don’t understand. Think
of it only as a quantity whose magnitude you have not yet determined. Large decimal numbers are
accessible to us only because of the amount of time we have already spent using them.
Introduction
Recursion Terminology
More Examples
How to Design a Recursive Function
The Delegation Metaphor
Common Pitfalls
Confusion About Self-Reference
Infinite Recursion
Incorrect Answers

16 — Recursion

Learning Objectives

After studying this chapter, a student should be able to:

• explain the difference between a recursive and a non-recursive function;


• explain the purpose of the base and recursive cases of a recursive function;
• identify the base and recursive cases of a recursive function;
• understand how recursive formulations for algorithms are derived; and
• explain recursion in terms of the delegation metaphor.

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)

# Now we know how many ancestors there are at generation


# n -1 , if we double that , we have the correct number of
# ancestors at generation n .
return 2 * k
 
Notice how this algorithm delegates the problem of determining how many ancestors there are at
generation n − 1 to a function call. The fact that we are calling the same function should not alarm
you. We just assume that the function call does what it is supposed to, solves the problem of how
many ancestors are at generation n − 1, and returns the right answer. That function call, in turn, will
solve the problem of how many ancestors are at generation n − 1 by first solving the problem of
how many ancestors are at generation n − 2 (by making another recursive call!), then doubling that
answer, and so on. So a whole sequence of n recursive calls will be made, each trying to solve a
smaller version of the problem, but this has to stop at some point. When the problem instance is the
smallest possible, we can just return the solution immediately without delegating the computation
of part of the solution to another function call. Thus, when n is zero, we can immediately return 1,
because there is no smaller version of the problem.
Anything that can be done with a loop can also be done with recursion, and vice versa. The
following function computes the solution to the same number of ancestors problem, but without
using recursion.
16.2 Recursion Terminology 147
 
def ancestors ( n ):
k = 1
i = 0
while i < n :
k = k * 2
i = i + 1
return k
 
The variable k is initalized to 1 and then k is multiplied by 2 exactly n times. You should convince
yourself that, ultimately, the recursive solution does the same thing! It just does it with function calls
instead of a loop.
You may wonder why we bother with recursion, since we already know loops. The answer is that
a loop is a special case of recursion, and an introductory course in computer science is incomplete
without taking a look at recursion. As well, some algorithms can be written far more elegantly and
with much less code using recursion, and are far more difficult to write using a loop. As we learn the
basics of recursion, we won’t see many of these more difficult cases. We hope you’ll take our word
for it that they exist.

16.2 Recursion Terminology


All recursive functions have the same general structure. At first glance, they are functions containing
a conditional (if-else statement, or a variation). At least one of the branches of the if-statement gives
a simple answer to a very simple task. This is called the base case. The base case is the smallest or
simplest possible instance of the problem. The other branch of the if-statement solves the problem by
transforming it into one or more sub-problems, and then combines the solutions of the subproblem(s)
to form the result of the main problem. This is called the recursive case. The recursive case always
makes a recursive function call (usually just one, but sometimes more) to the function being defined.
Here is another example. Here, we are adding up all the squares of integers from 0 to N, that is,
it calculates 02 + 12 + 22 + 32 + · · · + N 2 .
 
def sum_squares ( N ):
if N <= 0:
# base case
return 0
else :
# recursive case
return ( N * N ) + sum_squares (N -1)
 
The base case occurs when N = 0; this is the simplest instance of the problem. When N = 0, the
sum of squares of integers from 0 to 0 is equal to 0, so if N is zero or less, the function immediately
returns 0.1 The base case for a recursive function is almost always so simple that it requires very
little problem solving. Usually, common sense tells us the answer without much thought. When
writing your own recursive functions, you might be concerned because the base case seems too easy.
1 In our base case, we also included N < 0, which is a robust design, in case someone calls the function with a

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.

16.3 More Examples


Simple Sum
Consider the task of adding up all the integers from 1 to N, where N is any positive number. In other
words, we want to calculate 1 + 2 + 3 + · · · + N. This could easily be done with a loop, but let’s
derive a recursive solution. The key idea for this task is to recognize that it does not matter which
order you add the numbers. One way to transform the task is to group all but the last number into a
sum, and then add the last N to it:

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

100 Bottles of Beer on the Wall


Consider the task of singing about some number of bottles of beer that happen to be on the wall:
 
def drinking_song ( N ):
# base case
if N <= 0:
print ( " All gone ! " )
# recursive case
else :
# display one verse on the console
print (N , ’ bottles of beer on the wall , ’)
print (N , ’ bottles of beer . ’)
print ( ’ Take one down , pass it around , ’)
print (N -1 , ’ bottles of beer on the wall ! ’)
print ()

# sing the rest of the song


drinking_song (N -1)
 
The base case when there are zero or fewer bottles of beer on the wall (N <= 0) is very simple: there
is no beer left, so we simply display All gone! and do nothing more.
In the recursive case we do several things, in sequence. First, display one verse about the current
number of beers on the wall; this is done with some print statements. Then decrease the number
of beers on the wall by one, and recursively call drinking_song to display the rest of the song,
starting with number of bottles that still remain. The recursive call performs a simpler task, in the
sense that it displays the verses for a smaller number of beers.

16.4 How to Design a Recursive Function


There are two steps to designing a recursive function.
1. Determine the base case(s):
• Determine the smallest instance(s) of the problem.
• Write one or more base cases to return solutions to those specific instances. These take
the form of if-statements, checking whether the problem instance is a base case and, if it
is, returning the appropriate answer.
2. Write the recursive case – this usually appears in an else-block following the base cases.
Establish the mathematical relationship between the main task and the smaller sub-task(s)
before you start coding.
• Transform the problem instance that you start with (we will call it the “main task” of
the function) into a smaller or simpler instance of the same problem (we will call it the
“sub-task” implied by the transformation).
• Obtain a solution to the subtask using a recursive call.
• Combine the solution of the “sub-task” with some information you have about the “main
task” to create a solution for the “main task.”
16.5 The Delegation Metaphor 151

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.

16.5 The Delegation Metaphor


It is helpful to use a metaphor to remove confusion. Think of a function call as delegation. That is,
no matter what function is involved, a function call is like calling in an assistant, giving them a task,
and some room to work, and waiting for them to come back with a result (or the task complete). You
should imagine that your assistant is extremely trustworthy and will always perfectly complete the
task that you give them. You can even call in multiple assistants and split the task between them.
Calling in multiple assistants corresponds to making multiple recursive calls in the code for your
recursive function.
With this metaphor, it doesn’t matter if you give your assistant the instructions for the same
function you are working on, or if you give your assistant instructions for a different function. It’s all
the same. The assistant takes only the data you give them, and works completely independently. They
may create variables with the same name as the ones you created, but they are different variables.
The mental model to use is "assume the assistant is doing the same thing that you are." In other
words, the assistant will call sub-assistants of their own to help with the problem (because that’s
what you did!), but you don’t need to micro-manage that process. Just assume that the assistant will
2 This is not a suggestion that every task can be made simpler by drinking a bottle of beer. But the idea has been tried

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.

16.6 Common Pitfalls


Some believe that recursive functions are easier to write than repetition using loops. The reason is that
every part of a recursive function describes a property of the task being defined. If you understand
the task well enough, you can inspect the recursive function and verify each part independently: the
base case test, the base case task, and the recursive case task. Correctness comes from the task, not
from Python.

16.6.1 Confusion About Self-Reference


The most common pitfall is to focus on recursion as something confusing. Some books actually want
you to see recursion as mystical or mysterious. Don’t fall for that sophistry. Focus on the delegation
metaphor. Think about how you can represent two similar tasks, like sum(N) and sum(N − 1), with
the same sort of notation. This seems like self-reference, defining something in terms of itself. But
this particular kind of self-reference is not problematic. A recursive call is just saying “To do this
task, use a copy of the instructions you are reading now to solve a slightly smaller version of the
problem.”

16.6.2 Infinite Recursion


Infinite recursion is like an infinite loop: the function keeps calling itself without ever reaching the
base case. Sometimes, it’s because the test to identify the base case is incorrect. Sometimes, it’s
because the recursive call was incorrectly written to solve the main task, not a smaller or simpler
sub-task. Sometimes, especially in examples like is_even(), the subtasks might not be correct.

16.6.3 Incorrect Answers


Suppose your recursive function stops, and gives an answer, but it’s the wrong answer. The most
obvious place to look is the combination of the subtask with the information from the main task.
However, in this case, any of the components could be incorrect.
Part IV

Algorithms: Searching and Sorting


Fundamentals of Searching
Collections
Search Keys
The Target Key
Search Goals
Linear Search
Binary Search
Comparison and Summary of Linear Search
and Binary Search

17 — Search Algorithms

Learning Objectives

After studying this chapter, a student should be able to:

• list and describe the possible goals of a searching algorithm;


• describe, in plain English, the linear search algorithm;
• demonstrate the linear search algorithm by hand, identifying which data items are
inspected by the algorithm for a given search key;
• describe, in plain English, the binary search algorithm; and
• demonstrate the binary search algorithm by hand, identifying which data items are
inspected by the algorithm for a given search key.

17.1 Fundamentals of Searching


One of the most fundamental problems in computing is that of searching a collection of data items
for a particular desired data item. This problem is solved by search algorithms. In this section we
will introduce some basic concepts and terminology surrounding searching. In the following sections
we will present two basic search algorithms — linear search, and binary search — which are two
very different algorithms for solving the same problem.1

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

Type Search Key Example


integer itself the search key for data item 42 is 42
float itself the search key for data item 42.0 is 42.0
string itself the search key for ’Marvin’ is ’Marvin’
dictionary value corresponding to a the search key for the dictionary {’name’:’Marvin’,
predetermined dictionary ’description’:’The paranoid android.’} could
key be chosen to be the value of either the name or
description fields, i.e. either ’Marvin’ or ’The
paranoid android’.
list N/A We do not normally search for a specific list in a collec-
tion of lists.
array N/A We do not normally search for a specific array in a col-
lection of arrays.

Table 17.1: The search keys typically designated for different types of data.

17.1.2 Search Keys


When searching for an item, we must be able to identify it. Therefore, in a collection, every data
item is identified by a search key. The type of the data items in the collection being searched plays a
role in determining the search key of each data item, as shown in Table 17.1.
The search key of an atomic data item is itself. The search key of a compound data item is
usually a specific data item that forms part of the compound data item, for example, a specific
field of a record. In the case of Python, records are implemented using dictionaries. The search
key of a dictionary can be the value of the key-value pair for any of that dictionary’s keys. For
example, if we have a collection of dictionaries with keys ’movie_title’, ’release_year’, and
’average_viewer_rating’, we would have to select which of these dictionary keys’ corresponding
values would be used as the search keys for the dictionaries in that collection. Normally, when
searching a collection of records, all of the records have the same set of field names and the search
key for each dictionary is the value of the same field (i.e. the value of the same key-value pair for
each dictionary).
Note the distinct difference between the terms search key and dictionary key. Any data item of
any type, including dictionaries, can have a search key. Only dictionaries have dictionary keys. The
search key of a dictionary is a value associated with one of the dictionary’s dictionary keys.
Search keys need not be unique, but we often want them to be. If search keys are not unique, then
a search for a particular search key could yield multiple matching data items or only the first data
item that matches. If we want a unique search key for our collection of movie rating dictionaries, we
would have to use the values of the movie_title dictionary keys as the search keys since ratings
and release year of movies are not unique. The implementation of a particular search algorithm for a
particular collection is influenced by whether or not the search keys of a collection are unique.

17.1.3 The Target Key


We search a collection by choosing a target key and then look for the item in the collection whose
search key matches the target key. For example, if we wanted to search for the movie “Dracula:
17.2 Linear Search 157

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

17.1.4 Search Goals


A search can have different goals. We’ll discuss two such possible goals here.

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.

17.2 Linear Search


The linear search algorithm is the simplest, but often least efficient search algorithm. Linear search
simply examines each data item in the collection, one by one, comparing each data item’s search
key to the target key. When you find an item whose search key matches the target key, whether or
not you can stop depends on whether the search keys are unique, and whether it is a membership
search or a look-up search. If it is a membership search, you can always stop as soon as you find a
matching data item, and return true. If it is a lookup search, you can only stop if the search keys are
unique; if they are not, then there could be more matching items that have not yet been examined.
The name linear search comes from the fact that to find something in the collection, the bigger
the collection is, the more items you have to look at to find it, and that this relationship between
collection size, and number of items examined is a linear relationship. If you were to plot, on a
graph, the number of items in the collection against the number of items looked at by the search,
then you would see a trend that could be summarized by a straight line.
A linear look-up search can be described in a language-independent way by the following
pseudocode:
 
1 Algorithm LinearSearch ( C , target_key )
2
3 # a linear retrieval search for all instances of target key
4 # C - a collection of data items
5 # target_key - the target key for the search
6 # Returns : a collection containing items from C whose search
7 # keys match target_key
8
9 matches = an empty collection
10 for each item i in C :
158 Search Algorithms

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.

17.3 Binary Search


Binary search is one of the most powerful search algorithms. However, binary searches can only be
performed on collections where the data items are sorted in order by their search keys. So if we want
to use binary search on our list of movie rating records from the previous section, then the records
need to be sorted in order by movie title (alphabetic ordering). If we want to use binary search on a
sequence (array, list, tuple) of numbers, then the numbers have to be sorted in increasing order.
The fundamental concept that makes binary search work is that because the data items to be
searched are in sorted order, we can examine one data item, and immediately remove half of the
17.3 Binary Search 159

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

Items eliminated without examination Items examined Items still to be searched

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

Items eliminated without examination Items examined Items still to be searched

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

Items eliminated without examination Items examined Items still to be searched

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

mid = ( start + end ) // 2

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)
 

17.4 Comparison and Summary of Linear Search and Binary Search


Linear search steps through every data item in the collection one at a time looking for items whose
search key matches the target key. Linear search might need to look at every data item in the
collection, in particular, in the case where the collection contains no item whose search key matches
the target key. For linear search it does not matter if the collection is sorted or not.
Binary search requires that the items in the collection be in sorted order. Binary search reduces
the number of items to be searched by half with each item examined, resulting in the examination of
162 Search Algorithms

only a very few items, even if there are no items matching the target key.

Comparison of Linear and Binary Search


Algorithm Items Examined Sorted Collection Effect of examining one item
Linear Search Possibly All Optional Remaining items to be searched
reduced by one.
Binary Search Very Few, Never All2 Required Remaining items to be searched
reduced by half.

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

After studying this chapter, a student should be able to:

• name two different sorting algorithms;


• explain the divide and conquer approach to problem solving;
• describe, in plain English, the algorithm for merging two sorted sequences into a single
sorted sequence;
• describe, in plain English, the merge sort algorithm, and identify the divide and conquer
portions of the algorithm;
• describe, in plain English, the partition algorithm used by quick sort;
• describe, in plain English, the quick sort algorithm, and identify the divide and conquer
portions of the algorithm;
• describe, in general terms, the relative strengths and weaknesses of merge sort and
quick sort.

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.

18.2 Divide-and-Conquer Sorts


Divide-and-conquer is a very powerful, general-purpose recursive framework for solving certain
kinds of problems. Its key elements are these:
• Divide: If the problem size is so small that the answer is trivial, just return the answer (this is
the base case of the recursion). Otherwise, divide the problem to be solved into two (or more)
smaller sub-problems whose solutions will help you solve the original problem.
• Recurse: Recursively solve the sub-problems (possibly by dividing them into even smaller
problems in the process).
• Conquer: Take the solutions of the sub-problems and combine them into a solution of the
original problem.
We will look at two divide-and-conquer sorting algorithms: merge sort and quick sort.

18.2.1 Merge Sort


Suppose we are sorting a sequence of data items S containing n items. The merge sort algorithm
uses the following divide-and-conquer approach:
• Divide: If S has zero or one items, return S immediately, because it is already sorted (these
are the base cases!). Otherwise, divide S into S1 and S2 such that S1 contains the first n2 items
of S and S2 contains the remaining n2 (rounding up and down respectively).
• Recurse: Recursively sort S1 and S2 using merge sort.
• Conquer: Obtain a sorted version of S by merging the sorted sequences S1 and S2 .
Observe that the “divide” step is trivial — we just chop the array in half and recursively sort each
half. All of the work is done in the “conquer” step. The mathematical truth behind this recursive
algorithm is that:
sorted(S) = merge(sorted(S1 ), sorted(S2 ))
Let’s turn this into pseudocode:
18.2 Divide-and-Conquer Sorts 165
 
Algorithm mergeSort ( S )
# sorts sequence S using merge sort
# S - a sequence of data items
# return : new sorted sequence of S
if S contains 0 or 1 data items :
return S

# divide
S1 = first half of S
S2 = second half of S

# recursively sort S1 and S2


S1 = mergeSort ( S1 )
S2 = mergeSort ( S2 )

# 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 = []

# repeatedly move the smallest item to S


while len ( S1 ) > 0 and len ( S2 ) > 0:
if S1 [0] < S2 [0]:
S . append ( S1 [0])
del S1 [0]
else :
S . append ( S2 [0])
del S2 [0]

# once one of S1 or S2 is empty , append the remaining


# non - empty sequence to S .
if len ( S1 ) > 0:
S . extend ( S1 )
else :
S . extend ( S2 )

return S
 

18.2.2 Quick Sort


Quick sort is another divide-and-conquer approach to sorting in which all of the hard work is done
in the “divide” step. Contrast this with merge sort where all of the hard work is done in the conquer
step. The approach used by quick sort to sort a sequence of data items S is:
• Divide: If S has zero or one items, it is already sorted, so just return S (these are the base
cases!). Otherwise, select some item p from S, called the pivot. Remove each item from S and
place them in one of three sequences:
– L (for items less than p)
– E (for items equal to p)
168 Sorting Algorithms

– G (for items greater than p)


• Recurse: Recursively sort L and G using quick sort (E is already sorted by virtue of all of its
items being equal).
• Conquer: Concatenate the (already sorted) items of L, E, and G. This results in a sorted
version of the original S since everything in L is smaller than everything in E, which, in turn,
are smaller than everything in G, due to the divide step. The mathematical truth implemented
by the recursive quick sort algorithm is:

sorted(S) = sorted(L) + E + sorted(G)

where + stands for concatenation. Let’s express these ideas in pseudocode:


 
Algorithm quickSort ( S )
# sorts sequence S using quick sort
# S - array of data items to be sorted
# return : sorted sequence of S

# divide into sub - problems


pivot = any item of S # e . g . the last item of S
L = items in S smaller than pivot # these items may be in any order
G = items in S larger than pivot # these items may be in any order
E = items in S equal to the pivot

# recursively solve the sub - problems of sorting L and G


quickSort ( L )
quickSort ( G )

# L and G are now sorted

# 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

After studying this chapter, a student should be able to:

• summarize what they learned from this course

19.1 Course Summary


We view this courses, at its heart, as a fluency course in reading and writing. When you were
very young, you first learned to read and write in English (and perhaps other natural languages) by
learning the alphabet and reading very simple children’s books. You have now done the same, but in
a programming class. The programs that you have read and written during this class are like those
children books. They are purposefully short and simple, but if you can now read them fluently, you
are well on your way to being able to read and write larger and more complicated programs. It will
take practice, and like with writing books, it will help to study famous works created by other people
along the way, but what you have done now is a necessary pre-requisite. If you do more comptuer
science, you will use different programming languages, but the basic computational concepts that
you have learned - variables, data types, control flow, data structures, and recursion - are present in
all of them.
Like all forms of literacy, the ability to read and write in a programming language is a liberating
and transformative skill. It gives you the power to sit down with nothing more than a blank page in
front of you and to create. Your creations might be useful only to you, or they could be useful to
people in your immediate workplace, or they could be useful to millions of people all around the
world. Few, if any, other disciplines today give individual creators such a dramatic and wide-reaching
potential to change the world for the better.
Index

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

decimal numbers, 136

search
binary, 158
linear, 157
sorting, 163

You might also like