CIS552 Course Transcript
CIS552 Course Transcript
Functions in Python
What you'll do
Course Description
You already know how to call functions. In this course, Cornell University
Professor Walker White helps you build the skills to not only create your
own functions but also develop a full understanding of functions in a
business context. You will write specifications and examine what makes
a good or bad spec. You will perform testing, debug code, and look at
error messages. Has a program ever given you a confusing error
message? Before you've completed this course, you will identify and
write helpful error messages.
This course continues the skill and knowledge-building that are essential
for Python programming in a business setting.
Walker M. White
Senior Lecturer and Stephen H. Weiss
Provost's Teaching Fellow
Cornell Computing and Information Science,
Cornell University
Walker White is also the Director of the Game Design Initiative at Cornell.
In this role, he directs the computer game minor at Cornell and teaches
the primary game design courses. He is a strong proponent of project-
based education incorporating interdisciplinary teams, and he is the
faculty sponsor of the CU-AppDev engineering project team.
Table of Contents
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Define a Fruitful Function
Watch: Visualizing a Function Call
Read: Procedures vs. Fruitful Functions
Watch: Visualizing Frames in the Python Tutor
Activity: Explore the Python Tutor
Visualizing a Function Call
Watch: Accessing Global Variables
Activity: Access Global Variables
Watch: Calling Functions from Functions
Activity: Break Up Functions
Visualizing the Call Stack
Watch: Defining Optional Arguments
Activity: Create Optional Arguments
Tool: Fundamental Python Concepts 1
Project Submission for Executing Functions
Module Wrap-up: Executing Functions
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Designing Test Cases
Designing Tests with Multiple Arguments
Watch: Scripting Tests
Activity: Script Tests
Read: IntroCS Documentation
Watch: Organizing Test Cases
Activity: Organize Test Cases
Watch: Debugging Discovered Errors
Activity: Debug Discovered Errors
Tool: Fundamental Python Concepts 3
Project Submission for Testing Functions
Module Wrap-up: Testing Functions
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Assert Preconditions
Watch: Designing Error Messages
Activity: Design Error Messages
Watch: Identifying Trade-offs
Watch: Enforcing with Helper Functions
Activity: Enforce with Helper Functions
Tool: Fundamental Python Concepts 5
Project Submission for Enforcing Specifications
Module Wrap-up: Enforcing Specifications
Course Project
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module 1: Executing Functions
Module Introduction: Executing Functions
Activity: Explore the Codio Project
Watch: Reviewing Basic Terminology
Checking Vocabulary
Watch: Defining a Procedure
Watch: Defining a Procedure in a Module
Activity: Define a Procedure
Watch: Defining a Fruitful Function
Activity: Define a Fruitful Function
Watch: Visualizing a Function Call
Read: Procedures vs. Fruitful Functions
Watch: Visualizing Frames in the Python Tutor
Activity: Explore the Python Tutor
Visualizing a Function Call
Watch: Accessing Global Variables
Activity: Access Global Variables
Watch: Calling Functions from Functions
Activity: Break Up Functions
Visualizing the Call Stack
Watch: Defining Optional Arguments
Activity: Create Optional Arguments
Tool: Fundamental Python Concepts 1
Project Submission for Executing Functions
Module Wrap-up: Executing Functions
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Introduction: Executing Functions
You've explored how to call functions in Python. Now
we're going to look more closely at the details that go into
coding functions. You'll review important Python
terminology and examine how to write both a procedure
and a fruitful function. In addition, you'll be introduced to
the call frame and practice your visualization skills.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Explore the Codio Project
Many of the assessments in this course module will take place in a Codio
project. Furthermore, many of these assessments build on each other,
requiring you to copy over files. To simplify this process, we have created
a single Codio project for the entire course project. It contains all the
sample code for this course module as well as many of the exercises.
Read the text below to familiarize yourself with the structure of this
project. Once you are finished, feel free to explore some more. However,
do not work on the exercises until you are directed to.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Reviewing Basic Terminology
As you learn to code and then design functions in this course, there will
be many terms you'll need to understand.
Video Transcript
We're going to introduce the basic terminology that you're going to need
to know in order to start coding functions. Now, when we talk about
coding functions, we mean this in a very narrow context. We're not
talking about designing functions, which is really the larger goal of this
entire course. In designing functions, you would be writing your own code
with very little guidance. In this case, we're just going to really be focused
on the technical details of how do you write functions in Python in such a
way that you don't get error messages? Even to do that little, we're going
to have to introduce a lot of terminology. And if you don't know this
terminology, you're going to have a very hard time following these
lectures. For that reason, we are going to have a glossary available on
the Canvas course web page so that you can look up these terms at any
given time. But in this video, it's going to help for you to see a lot of these
things in context to help you understand what these words are. In
addition to introducing new terminology, we're also going to try to
standardize some other terminology. One of the problems with
programming is that sometimes people use very similar words in very,
very different ways, and we're going to have to be very, very precise with
our language. So right away, we're going to assume that you are familiar
with what is known as a function call, even if you don't know what that
term is.
A function call is what it's known in Python when you use a function in
your code. To call a function, you first type its name. So in this case, what
I'm going to do is I'm going to use the function round(), and I'm going to
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
type its name, "round". That by itself is not a function call; I also have to
give the function something to operate on. So I'm going to give it some
parentheses, and then inside of the parentheses, I'm going to give it a
value; "(26.54)". This is the function call: the name, plus the parentheses,
plus the expression inside the parentheses that it can use in order to do
its work. I'm going to hit Return, and Python has rounded this number,
and it's given me the value 27 back. Those things that are inside of the
parentheses, they also have a very formal name; they are known as
arguments. So in this case, 26.54 is the argument for my function call
round(). One of the interesting things about the function round() is that it
can actually take multiple arguments. If I take this function call round(),
and now I'm going to add a second argument, separating it by a comma
before I close parentheses. I hit Return, and now you'll notice that instead
of rounding to the nearest integer, Python has rounded to the nearest
tens place. And that's because I've given it the second number, telling it
the place after the decimal point for it to round.
Most of the functions that we work with are known as expressions. What
is an expression is just something in Python that when I type it into
Python, Python turns it into a value. That's what we saw right away with
round(). This is an expression; it's an expression that happens to be a
function call. And I hit Return, and it gives me back the value 27. The
nice things about expressions is that I can use them in assignment
statements; I can assign them to variables. So I can say, "x =
round(26.54)". Now, it didn't look like anything happened; that's because
assignment statements happen sort of silently or invisibly. But if I look at
the variable x, you'll notice that there is now a value 27 inside of x. We
bring this up because some function calls are not expressions; they are
instead statements. Let's look at another interesting function, print(). So
I'm going to print out the word 'Hello'. This looks a lot like what happened
with round(). It's a function call again; I had the name "print", and then I
have some parentheses, and then I've given it an expression inside the
parentheses. My argument in this case is the string 'Hello'. It looks like it's
given me a value back, but that's not true, because if you notice, that text
"Hello" doesn't have quotes around it; it's not a string. So it's unsure what
happened there. And in fact, let's try assigning this to a variable. I still see
the "Hello" again; it didn't actually run silently. And if I look inside of the
variable x, you'll notice that there's nothing inside of that variable right
now. That's because print() doesn't produce a value; it displays text on
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
screen.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Checking Vocabulary
&KHFNLQJ9RFDEXODU\FRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Defining a Procedure
Now that you've been introduced to procedures, the next step is learning
how to write a procedure.
Video Transcript
Our goal throughout this entire module is to teach you how to write a
function definition. You know how to call a function. And Python does
something when you call a function. But how does Python know what to
do? Well, the built-in functions, they have definitions that tell Python what
to do. They're hidden; you'd have to go search for them if you wanted to.
But in this video, what we're trying to do is to teach you how to write your
own functions, so you're going to have to write your own definitions. Right
now we're just going to focus on procedures. That's because procedures
are the easier of the two function types. With that said, most of what we
say in this video applies to all functions. So right away, let's just look at a
function definition. In this file I have open in front of me, I have a
definition for a function called greet(). Right away you'll notice that it
starts with the word "def", which stands for "definition." This is a keyword,
and when Python sees this, it knows that I'm beginning a definition. The
rest of this line is what is known as the function header. And one of the
key important things about the function header is that it has to end in a
colon; if you don't have it ending in a colon, you're going to get an error
message.
After the header, we have several lines that compose what are called the
function body. The most important thing that you notice about the
function body is that it doesn't quite line up with the "def" keyword;
actually, all of these lines are indented. That's because that's how Python
tells where a function begins and ends. If I were to type another line, like,
say, another print statement, in this case you'll notice that the print
statement is now aligned with the keyword "def"; it's no longer indented,
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
so Python knows that it's not part of the body. The only thing that is in the
body is what is indented right now. Let's look a little deeper at this
function body. So, the function body breaks up into two parts right here.
One of the things you'll notice that I have several lines that are between
two sets of triple quotes. This is a docstring, or a multi-line comment. It's
very, very similar to the docstring that I have up at the top of the file.
Docstrings are usually a way of writing notes to myself. This particular
docstring, one that is attached to a function, has a very specific name. It's
what's known as a specification. And specifications will be an important
topic of another module.
Let's say I want to greet myself. In this case, I've given it a string as an
argument. I gave it exactly one argument, and the reason why I gave it
exactly one argument is that I have exactly one parameter here. So I
must have enough arguments to match the parameters in my function
definitions. So let's see how we use this function. I'm not actually going to
work with a file right now; instead I'm going to work directly into Python,
and I'm just going to cut and paste this definition into Python. You'll notice
that actually when I type this definition into Python, I'm not seeing the
three greater-thans anymore; it's seeing these dot-dot-dots. It's because
Python knows that I'm in the middle of a function definition and it's waiting
for me to finish the definition before it can continue. Well, the way you've
finish a function definition is you just type in a line that isn't indented. I hit
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Return, and so now I have defined the function greet(). So I can write
away; I can now call it. And notice that it greeted me.
What happened in this case? Well, what Python did was it took the
argument — in this case, 'Walker' — and it plugged it inside of the
variable n. Then it executed each line, so, in fact, what we have here is
an assignment statement that adds my name to the word "Hello" and
puts it inside of the variable text. And then finally what it did was it printed
out that text, and indeed that's what you saw happen on the screen. One
of the important things to understand, however, is that you have to type in
a function definition before you can call it. If we were to quit Python, so
that we forget everything. And now I'm going to try to greet myself. Now
Python doesn't know what to do because it doesn't actually have a
function definition. Once again, I'm going to have to paste into the
function definition. And now I can call the function, and it works normally.
As one last bit of terminology, I just want to look at this variable text right
here. We call the variable n a parameter. This is also a variable, but since
it's not inside of parentheses we give it a different name. This is what is
known as a local variable. And in fact, when we start going deeper into
function definitions, keeping parameters distinct from local variables is
going to be an important topic.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Defining a Procedure in a Module
While this course contains modules, the term used by Professor White
refers to a file that contains Python code. As you may have found, it can
be tiresome to keep typing function definitions into Python. This is why
using modules is so helpful. In this video, you'll examine how modules
can simplify function definitions and the role they will play in your work
throughout this course.
Video Transcript
Typing function definitions directly into Python can get a little annoying,
particularly when we start to make mistakes. That's why what we like to
do when we write function definitions is to write them in files; in particular,
modules. Now, to remind you, a module is a file that has Python code in
it. Modules typically end with the suffix .py, and you edit them with a code
editor. The code editor that I'm going to be using through my
demonstrations is Atom Editor. Right now on the screen I have Atom
Editor open, and it's open to a module. This is a very, very simple
module; it has a docstring at the beginning, which is sort of a comment to
myself; it also has a single-line comment; and it just has two assignment
statements in it. To use a module, I have to import it. Of course, the first
thing that I have to do is I have to make sure that I'm running Python in
the correct directory. So this particular file, module.py, is inside of a folder
that I call unit2. I'm going to have to open up the Terminal window, make
sure with "pwd" that I am inside the right folder before I continue.
And now I can start up Python, and now I'm going to import the module.
When you import a module, what it does is it's going to execute every
single statement in that file. In particular, it's going to execute the two
assignment statements here for the variable x. And now, from this point
on, I can access that variable if I want to. The way that I access that
variable is I type the name of the module, period, and then the variable x,
which is what I want to access. Now, modules also allow you to access
functions. By now you should be familiar with some basic Python
modules; for example, say, the "math" module. So I can "import math".
"Math" has a variable pi inside of it which I can access, which works very
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
much like the variable x that we just saw for the first module. But it also
has functions inside of it that I can call. Again, I would type the name of
the module, which is "math", and then dot, the name of the function. And
now I call the function with parentheses, giving it a value of 0.5, and I get
an answer back.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Define a Procedure
In the Codio project below, navigate to the page Overview of Exercise
1. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 1, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Defining a Fruitful Function
Now that you've worked on defining a procedure, let's take a closer look
at the other type of function, a fruitful function. A fruitful function is a
function that terminates by executing a return statement.
Video Transcript
In this video, we're going to show you how to define a fruitful function.
Before we do that, let's remind ourselves the difference, right? A
procedure is one where the function call is a statement. So if I were to
use print() and type "print('Hello')", this is a command to Python to display
the word "Hello" on the screen, but it doesn't produce a value. And if I
were to use this in an assignment statement like "x = print('Hello')", you'll
notice that there's nothing inside of the variable x. On the other hand, a
fruitful function is one where the call is an expression. If instead I were to
write an assignment statement with a fruitful function "round(26.54)", this
is going to produce a value. And when I look at the variable x, it now has
that value 27 inside of it. Now this seems like a minor difference, and it is
actually a minor difference. And for that reason, the definitions are almost
exactly the same. The only difference between a fruitful function and a
procedure is a minor change to the body.
Fruitful functions have a new type of statement in them, and this is what
is known as the return statement. In the code editor that I have here on
the screen, you'll notice that I have two functions for converting between
Celsius and Fahrenheit. So this is my definition of the function
to_centigrade(). Here, I have my keyword "def" to tell me that's the start
of a function definition, and I have my header. I have a very long
specification telling me a lot of details about this function, but I actually
only have one line of code inside of the body of the function. This is my
return statement. You can tell because right away it's starts with the word
"return". This is also a keyword; Python looks at this and it knows that
this line is supposed to be a return statement. After the return is an
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
expression. And what's going to happen is when I call this function,
Python is going to evaluate this expression after the return, and that is
the value that the function call is going to reduce.
Let's see this in practice. I'm going to import the module "temp". Now I'm
going to call temp.to_centigrade(); say, with 0 degrees. And we'll see that
that is -17 Fahrenheit. Similarly, if I look at to_centigrade() of 32, we'll see
that that is 0 degrees. At each time, what's happening is my argument —
in this case, 32 — is being plugged into the variable x and then the return
statement is processing that expression with that new value of x to give
me the final answer. Now, I don't just have to have a return statement by
itself; I could actually combine a return statement with other statements.
So let's look at another function that we have here, the function plus(). In
this case, you'll notice that in the body, I have two lines of code to
execute. I have an assignment statement that produces a local variable
x, and then in the end, I'm going to return the value of that local variable
x. I'm going to import this module. Now, I'm going to call this function
plus(); "plus(1)" is 2, "plus(4)" is 5. So at each time what's happening is
it's taking my argument, it's plugging into the variable n, it's executing
each line of code, and then at the end, it is returning the final answer. But
the key thing that you'll notice in each case is that the return statement is
always the last line of the function. That's because as soon as Python
sees this particular line, it stops executing.
Let's do an example. I'm going to add a print statement here at the end of
my definition. I'm going to save. I'm going to quit and start Python
because we have to do this anytime we make a change to a module. I'm
now going to "import fruit". And I'm going to call "fruit.plus(2)". You notice
that I get the answer of 3 but it doesn't actually print out "Hello." On the
other hand, if I were to put this print statement above the return; save,
quit, restart Python. This time we see the print statement. So, Python will
execute a body until it sees the return statement. An analogy to think of is
sort of like what you do when you're working on a math problem for an
exam. Typically, you do a lot of work on a sheet of scratch paper, and
then when you're done, you take a box and you use — or circle, and you
use that to circle your final answer. That's what the return statement is
doing; it's indicating what your final answer is. So once Python sees that
return, it's not going to do anything more.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Python beginners often confuse print statements and return statements.
In this video, Professor White shows what happens when you mix these
two statements up in a function definition.
Video Transcript
One of the problems that beginning students run into is they start to
confuse print statements and return statements. That's because, while
they are different, sometimes they can look a little similar. To show you
what we mean, I have a file open up here that has two function
definitions: print_plus() and return_plus(). You notice that these
definitions are very similar to one another. print_plus() ends in a print
statement, and return_plus() ends with a return statement. But in each
case, what we're doing is we're either printing or returning a number.
Let's watch what happens when we use each of these functions. I'm
going to import this module. If I call print_plus() on the number 1, looks
like I got the answer 2. If I call return_plus() on the number 1, once again,
I got the number 2. However, the key thing to understand here is that
only one of these two is a fruitful function. return_plus() is a fruitful
function because it has a return statement in it; whereas print_plus(),
because there's no return statement at all, is a procedure. How can we
tell this? Well, as always, the key is what happens when we put it in an
assignment statement. I'm going to call print_plus(), assign it to the
variable x; ah, you'll notice that it is displaying the number 2 on the
screen. But if I look at the variable x, there's nothing inside of that
variable. On the other hand, if I use return_plus(), this time the
assignment statement executes silently. And when I look inside of the
variable x, I have the value 2 inside of this. This is a very important
distinction, right? Return is necessary whenever we're trying to do
calculations. Print is not very useful for calculations; it's only useful when
we want to display things on the screen. In particular, one of the things
that we'll see is that print statement is very, very useful for testing.
Keeping these two things distinct from one another is going to be very
important to understand the types of functions that we're going to design
going forward.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Define a Fruitful Function
In the Codio project below, navigate to the page Overview of Exercise
2. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 2, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Visualizing a Function Call
A common ability of all skilled programmers is the ability to picture how
Python is functioning under the hood.
Video Transcript
This is our function plus(), it has a single parameter n, I have a body with
two lines of code with an assignment statement for a variable x and a
return statement. Let's take this definition and let's copy it directly into
Python. So now we can actually use this definition. Now I'm going to type
two lines of code. Let's type "x = 2", and then "y = plus(4)". Now, if I look
at y, that has the value 5; this is what we expect. But that's not actually
the interesting question; the interesting question is what happened to x?
In the beginning, I made this variable x, right, so that creates a box, and
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
that puts the number 2 inside of that box for x. But then after that I called
the function plus(). Remember what happens when we call a function;
what it does is it executes every single line of code in the body. Right
away we can see this line that does an assignment to the variable x.
So does that mean now that — OK, well, n is 4; I added 1 to it, that puts a
five. Does that mean that 5 is now inside of the variable x? Actually, no;
the variable x only has the value 2 in it. Now, why did that happen?
That's because I actually had two different variables here. This variable x
is what is known as a global variable. A global variable is any variable
that doesn't appear inside of the body of a function. This variable right
here is a local variable. It's inside of the body of the function. Because
one is a global variable and one is a local variable, these are two
completely different variables, even though they have the same name.
That can get really confusing, right; now we're talking about the prospect
where just because I know the name of a variable doesn't mean that I
know exactly what variable that I'm talking about. That is why we need to
build up visualization skills, and that is what we will address in the next
video.
In this video, Professor White introduces the call frame, a visual model
that helps show where variables are in relation to each other in Python.
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
talking about at any given time. If we look in this file right now where I
have the function definition for plus(), you will notice that to the left, my
code editor displays several numbers. These numbers represent what
line of code I'm currently on. So, line 10 is the line that is referring to the
assignment statement, and line 11 is the line that refers to the return
statement. These are the numbers that we're going to be putting in the
top right corner of our call frame.
Let's just work through an example with this particular definition that I
have right here. So here's my definition, but what we're actually modeling
is a function call. So inside of Python I have a call for "y = plus(4)." So
what happens when we call a function? Well, right away what we're going
to do is we're going to draw our box for our call frame. In the top left
corner I'm going to write "plus", which is the name of the function. Inside
of it, what I'm going to do is I'm going to be writing the variables. But at
the very beginning, the only variables that exist are the parameters. Even
though there's a variable x inside of the body, I'm not going to worry
about x yet. But I am going to worry about the variable n because that is
a parameter. So I'm going to put a variable called n, and inside of the box
for n I'm going to put the number 4. And that's because that was the
argument that I gave inside of the function call. Now, it's important to
understand that the only things that can fit inside of variables are values.
If I were to call this differently, like if I were to say, "y = plus(3+1)", this is
not going to put "3+1" in the box for n. What it's going to do first is
evaluate "3+1" to the number 4, and then it's going to put 4 into the box
for n. The last thing that we do when we're constructing our call frame is
we have to give an instruction counter. The instruction counter is not the
current line of code; it is the next line of code. It's telling me where to go
next. And where I want to go next is right here. I haven't done line 10 yet,
we don't have a variable x yet, but it is the next line because it is the first
part of the body that is not a comment. And so that's generally what
happens whenever we create a call frame, is our instruction counter will
always be the first line of the body after the specification. OK; so that's
our initial picture. Where do we go from here?
Well, now what we're going to do is we're going to execute the body until
the end. We're going to process one line of code at each time, so what
we're going to do is each line in this function body, we're going to process
it. Each time that we do this, we're going to redraw the frame. We're not
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
creating a new frame; what's happening is the original frame is changing.
So what you should think of is that we are animating the frame over time.
As you go through, you're going to be processing each type of statement.
Right now we only know three types of statements: a print statement, an
assignment statement, and a return statement. If you see a print
statement, it's easy; there's no change to the frame, there's no new
memory or anything like that. It's just that it displays something to the
screen. And we don't need to visualize that because Python shows that
for us. If it's an assignment statement, then we're actually going to have
to put a variable inside of the call frame. And if it's a return statement,
we're going to have to do something slightly different. Well, all right; once
again, let's go step through our example. So we have our call frame, and
right now the instruction counter is at line 10. So that means it is now
time to process line 10.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
return statement for my call frame, which would be the value 5. And now,
since I've used this function call as part of an assignment statement, that
means that now 5 is inside of the global variable y. But the key thing to
keep in mind is, notice that all of my work got lost. We've talked about
before how the return statement is much like working on a math exam,
and then after you've done all your work, you use "return" to circle your
final answer. It's actually worse than that. Everything that is not a return
statement is actually scratch paper. And when the function is done, we
essentially ball up the scratch paper and throw it away. The return value
is the only thing that is kept.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Read: Procedures vs. Fruitful Functions
Procedure
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Anatomy of the Header:
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Fruitful Function
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Visualizing Frames in the Python Tutor
The eCornell Python Tutor is a tool we've created to assist you in building
your visualization skills.
In this video, Professor White uses the Python Tutor to show exactly
what Python does when a function is being executed.
After you have watched the video, you can access the Python Tutor here
or from the next page.
Video Transcript
OK; so now what we've done is we've started to execute the Python code
but we haven't completed it. In fact, you'll notice that I have this red arrow
right here. This red arrow works a lot like the instruction counter in a call
frame. We don't have a call frame yet; we haven't called the function. But
this red arrow shows that, oh, line 1 is the next line that I want to execute.
I haven't finished executing line 1 yet, but it is the next one to execute. To
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
move forward in the code, you just hit the button "Forward." So, what
happened there was, Python read the definition for plus(); it did that
invisibly. That's because while there is a way to visualize function
definitions, that's a bit beyond the scope of this course. So, the Python
Tutor does that part invisibly. But the next part is where it gets a little
interesting. I haven't assignment statement for a global variable x. I
haven't processed it yet; the red arrow says it's the next line to execute.
So I hit forward, and now you'll notice that that's indeed what happened,
is the Python Tutor created a variable x and it put the number 2 in it; it
tells me right away that this is a global variable. Now, the next line is an
assignment statement, but inside this assignment statement, I have a
function call for plus(). So as soon as I hit "Forward" for plus(), it's going
to create a call frame. And we do this, and that's what it's done; it's
created the call frame right down here for plus(), in much the way that we
showed you to write call frames. The name of the function plus() is in the
top left corner. I had to put some variables inside of the call frame.
Remember at the beginning, the only variables that we put inside of the
call frame are parameters. But I do have a parameter n, so I make the
variable for n, and the argument, which in this case is 4, gets copied into
the box for n. The only thing that's missing from the call frame is the
instruction counter. Notice that I don't have an instruction counter here in
the right corner. That's because I don't really need it. That's what this red
arrow is doing right here. The red arrow is telling me that the next line to
execute is line 3. So if we were to draw this as our own picture, we would
write a number 3 up here in the top right corner.
Now what we're going to do is we're just going to go step through the
function one at a time. Let's hit "Forward." It's going to process line 3.
Line 3 is an assignment statement; it's going to create a variable called x
and it's going to put the variable 5 inside of that box. And now I'm ready
to do line 4. OK; so now what I'm going to do is I'm going to hit "Forward,"
and a return statement — remember, it just makes a special variable
called "return" and puts the value 5 in it. And now what do I do for my line
counter? Well, notice that the red arrow's really interesting. The red arrow
is not actually pointing to a specific line; it's sort of pointing halfway
between the 4 and the 5. That's indicating that we're at the end of the
function; there's nowhere left to go. When we did this with our call
frames, we would write an empty box in the top right corner. So, what
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
happens when I hit "Forward" one more time? Well, what it does is it's
now going to jump back to the function call, it's going to erase the call
frame, and right away it's going to create the variable y and it's going to
put the "return" result from that call frame into y. Let's step back; let's
watch it again. I have 5 inside of "return"; moving forward, it's going to
take that 5 and it's going to copy it into y. So, this is how we visualize
what's going on. This is a very, very powerful tool. One more thing that
we'd like to point out is, is that you aren't actually limited to a single field
here. You'll notice that I have multiple tabs here. One of the things that I
could do is I could take this particular function call, I could copy it from
this tab, and I can put it into the second tab. And why is this useful?
Because this essentially, simulates the way modules work. I can "import
tab2" as a module. And now what I can do is I can call plus() by writing
the module name in front. And now if we visualize — there we go; that
designs the global variable x. Now you'll notice that this defined the
global variable y and it didn't create a call frame. A call frame is still
actually created by Python. It's just that when you have the function
definition in another tab, the visualizer gets a little confused and it doesn't
know exactly how to show the call frame to you so it doesn't bother. So
you should keep this in mind if you're actually using the multiple tabs.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Explore the Python Tutor
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Visualizing a Function Call
9LVXDOL]LQJD)XQFWLRQ&DOOFRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Accessing Global Variables
Now that you've seen how useful the Python Tutor can be, it's time to
take a deeper look at how variables and functions work. In this video,
Professor White further explores variables through several examples and
also offers some considerations in using the Python Tutor.
Video Transcript
In this video, we're going to use the Python Tutor to give us a much
deeper understanding about how variables and functions work in Python.
On the screen in the Python Tutor, I have a definition of a function called
swap(). You notice that this is a function that actually takes two
arguments because it has two parameters in the function definition, a and
b. And we can read the description right here, and we can also look at
the code, that the purpose of this particular function is to swap the
variables a and b. So that's my function definition, and now what I've
done is I've created two global variables, a and b — a is 1 and b is 2 —
and now what I'm going to do is I'm going to swap them. Except that what
happens is that this function doesn't really do what we think it does, and
we can tell that by visualizing. Let's go step through the visualizer. We're
going to read the function definition, we're going to create a global
variable a because it's not inside of the body of a function definition.
I'm going to create a global variable called b, and now I'm going to call
swap(). What this does is this creates a call frame. This call frame has
parameters a and b. So right away I have two versions of a on the screen
and I have two versions of b on the screen. All right? So I'm going to
execute the next line of code, and that's going to create a variable called
tmp, and it's going to copy from a. Now, I'm going to copy from b into a,
and from tmp into b. This is a procedure; there's no return statement so it
doesn't have anything to return. And now when I'm done I erase the call
frame, and all of the work that I've done is lost. So what you've noticed
here is that I thought, oh, I wanted to write a function that would be able
to swap two variables, but I couldn't do it because these are global
variables, and these right here are all local variables. So right away you
might think, oh, OK, well, there's no way that I can have local variables
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
and global variables work with each other. Except that's not exactly true.
Let's look at another, more interesting function. So here what I've done is
I've created a variable called a, and now I've created a function called
get_a(), and this once again is a very interesting function definition. You'll
notice that it has no parameters inside of the function header. That's
perfectly fine; I can have a function like that. I just need to be sure that
when I call this function, I call it with no arguments. However, it has no
parameters, but right away, I say, "Hey, I want to return a." So what's
going on? Well, let's visualize what's happening. We'll step through.
We're going to create a global variable called a. Now, I'm going to define
the function get_a(), and now I'm going to call get_a(). That creates a call
frame. I put the name inside of here. There are no variables inside of the
call frame because it has no parameters. And now I'm going to return,
and it says "return a." Well, but how did it know what a was? Well, it knew
because there is a global variable called a. So in this case, it was actually
fine. I was able to use the global variable instead of a local variable inside
of my function definition. So why does this work and why did swap() not
work? Well, to answer that question, let's go a little deeper. Let's look at
another function definition.
Once again, I've created a global variable called a. And now I have a
function definition which I call mask_a(). And right away, what you'll
notice is that assigns a value to a, and it returns it. Step through the
visualization. I'm going to create a global variable called a. I'm going to
call the function; again, I have no parameters, but now I execute an
assignment statement, and that assignment statement now created a
new local variable called a. So now when I return that value, the value
that I'm going to return is the local variable a, which in this case was 3.5
and not 4. So right away we can see sort of this rule of thumb, which is if
we don't assign anything to a variable and that variable exists as a global
variable, we'll use the global version. But if I do assign something to it, it
becomes a local version, and I can only use the local version instead. So
what this means is that I can have global variables but only if I don't
change them. Now you might say, "Well, why is this useful? Why might I
want to have a variable that I can't change?" But we've seen examples
like this before, right? Things — constants like pi or e that are inside of
the "math" module; things that I want to refer to but I don't actually want
to change. With that said, it is possible to actually change a global
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
variable in a function definition, but to do it you have to add a new type of
statement. So as one last example, we're looking at a function called
change_a(). Once again, I've defined a global variable, and now I have
my function definition. It doesn't take any parameters. And at the very
beginning, you'll notice I have this line that says "global a." I'm telling
Python that I actually want to use the global variable a and I don't want to
make a local variable.
Let's watch what happens when I visualize this code. I'm going to make
the global variable a, I'm going to define the function. Now, I'm going to
call it; that creates a call frame that has no parameters. And, oh,
interestingly enough, you'll notice that as soon as I created the call frame,
it actually is not at line 7; it's skipped all the way to line 8. That's because
this is not aligned to execute. Python actually used this line when it was
reading the function definition. But now that it knows that it's going to be
a global variable, it goes, "Oh, OK, line 8 is actually the first line of code
to execute." That, by the way, is the reason why global needs to be at the
very beginning of the function definition; like the specification, it's not
really part of the body. OK; well, now we're going to do this line, and right
away we can see that there's no variable a in the call frame. Instead,
what happened was that assignment statement actually changed the
version of a inside of the global space. and now when I return a, it's going
to return the version that was the global variable. So as you can see, we
can actually modify global variables if we want to. With that said, you
should use this sparingly, right? Programming with globals can get very
confusing, particularly when multiple function definitions are involved, and
it can be very easy to get lost. This is why this technique is a little bit
frowned on by professional programmers. But sometimes you have no
other option, so that's why you might want to use the global keyword.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Access Global Variables
In the Codio project below, navigate to the page Overview of Exercise
3. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 3, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Calling Functions from Functions
A notable feature of functions is their ability to call other functions.
Video Transcript
One of the powerful things about functions is that inside of their bodies,
they can actually call other functions. We've seen examples of this
before, right; we use the function print() inside of our greet() function.
Now you might ask yourself, "Wait; does print() have a call frame?"
Actually, it does. The problem is that we can't visualize it because the
definition is hidden. We only know how to visualize a call frame if we can
see the definition. We saw a similar thing in the Python Tutor where if the
definition was hidden in another tab, we couldn't visualize it. However,
suppose I actually have a case where I can see all of the definitions. So
in fact, in the file that we have on the screen right here, you'll notice that I
have two definitions, foo() and bar(), and in the body of the function foo(),
I actually have a call to bar(). So, what happens in this case? At the
bottom of the file, I do have a call to foo() and we know what that does.
But what's going to happen to the call frame for bar() when we execute
the body for foo()?
Well, to understand this, let's step through this one line at a time, OK? So
we're going to look at the function call for foo() right here, and what this is
going to do is this is going to create a call frame. So let's draw this. We're
going to create a box. In the top left corner, we're going to write the word
"foo", which is for our function. foo() has a single parameter called x, so
we're going to make a variable box for x. And inside of that we're going to
put 2, which is our argument. And then, of course, we're going to have to
put the line counter for the next line to execute, which in this case is line
12.
All right. Now what we're going to do is we're going to step through one
line at a time. We go to the next line. We're going to create a variable for
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
y. Inside of y, we're going to take 2 plus 1, or 3, and put it inside of the
box. And then the next line that we're going to execute is line 13. So right
now, we're going to have a 13 in the top right corner of our instruction
counter. OK; but now I have a function call. So, what's going to happen?
Well, what's going to happen is, we're going to make another call frame.
So, right below the call frame for foo(), we're going to create a call frame
for bar(). And what we're going to do is we're going to completely freeze
the call frame for foo(). We're not going to change any variables. We're
not going to change any instruction counters. Right now the instruction
counter is going to be frozen at line 13, and that's because I cannot finish
computing line 13 until I have completely finished with bar(). And now
what we're going to do is we're going to work with bar(), OK? So I'm
going to create a call frame. I have "bar" in the top left corner. bar() has a
single parameter called x; I'm going to make its own version of an x
variable. Inside of that x variable, I'm going to put the argument that was
put there, which was y that I evaluated to 3. And then for the instruction
counter, I'm going to give it the next line of code to execute, which in this
case is line 17. So I have two call frames; one at line 13 and one at line
17.
Once again, we're going to go through lines one at a time. We're going to
process line 17. We're going to create a variable called y. We're going to
take 3 minus 1, or 2, and put that into the variable y. And now we're
going to change our instruction counter to line 18. Now we're going to
take line 18, which is a return statement. We're going to make a very
special variable called RETURN. We're going to take the contents of y,
which is 2, and put that into RETURN. So RETURN now has 2 and our
instruction counter for bar() is blank because we have reached the end of
our function definition. Finally, what we're going to do now is, now we're
going to erase the call frame for bar(), and that's going to allow us to
make progress on line 13. So, we make a variable for z inside of the call
frame for foo(). We put the number 2 inside of z. And now we can finally
increment our instruction counter to point to line 14. And now we just
keep on moving forward. We create ourselves a return variable, and we
put the number 2 in it. Our instruction counter is now blank. Finally, we
erase that, and now we create a global variable called w, which has our
final answer inside of it.
In this video, Professor White shows how visualization of the same code
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
differs when the Python Tutor is used.
Video Transcript
We've shown you how to visualize what happens when one function calls
another. But in that case, what we did was we showed you how to
visualize it in your head. In this video, we're going to show you how to do
this in the Python Tutor; and in doing so, we're going to show you why it
was that we didn't show you the Python Tutor to begin with. Here I have
the Python Tutor open, and I have the same code that we had from a
previous video. I have my functions foo() and bar(), and my function foo()
calls bar(). And at the very bottom, I have a call to foo(). This is pretty
much the same as what we had before, though we should note that the
line numbers are slightly different. Instead of foo() starting at line 11, it
now starts at line 1. So everything is off by 10. But otherwise, everything
is pretty much the same. So, let's step through and visualize this code.
We hit "Forward" and we're going to read the function definitions.
Remember that function definitions are processed silently. So now we're
ready to call the function foo(). We hit "Forward" and we're going to
create a call frame for foo(). Here we have the call frame. The name of
the call frame is the top left corner, and we've created a variable x, and
that's because I only have one parameter, which is the variable x. I don't
have a line number, but that's what the red arrow is doing right here; it's
telling me that the next line to execute is line 2.
I have now processed line 7, and that has created the variable y. And
now we start to see why it was that we didn't quite show you the Python
Tutor to begin with. Because look; we see this gray arrow right here, and
we thought maybe the gray arrow was keeping track of what was
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
happening in foo(), but actually it's just the previous line that I've
executed. I don't actually have an arrow pointing to anything in the
definition for foo() anymore. That means if I wasn't paying attention, I
may not remember where it was that I was supposed to go back to when
I was done. When we were visualizing things, we had solved that
problem; we would have written the number 3 up here in the top right
corner before we froze it, telling us where to go back to. But in the Python
Tutor, we're just going to have to remember those things. With that said,
let's keep moving forward. Now we make a return value; we're going to
put the number 2 in it, and we're done. As you can see, the red arrow
isn't actually pointing to any specific number, telling us it's ready to end
this particular function call. So now I'm going to erase it; and when I
erase it, it's going to take the return value and it's going to copy it into the
variable z, right here. And now you'll notice that foo() is no longer frozen
because it's blue, and the instruction counter has moved ahead to line 4.
Let's execute line 4; that creates a special return value which has 2 in it.
And then we go forward one last time; it's going to erase this frame and
it's going to take the return value and it's going to put it into a global
variable, which is
Video Transcript
We've seen how to visualize two functions, where one function calls
another. But we don't have to stop there, right? We could have lots of
functions all calling each other. To see what happens there, let's look at a
specific example. On the screen here I have four function definitions —
function1(), function2(), function3(), and function4() — and at the end I
have a call to function1(). You'll notice that function1() calls function2(),
and function2() calls functions 3() and 4(). Let's visualize this and let's
see what happens. At the beginning, stepping through our visualization,
we process each of the definitions invisibly, and now we've finally
reached the call for function1(). Going forward, it's going to create a call
frame for function1(), and you can see that the next line is line 2, which
itself has a call for function2(). When we hit "Forward", that's going to
execute that call. It's going to freeze function1(); it's no longer blue. And
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
now, I have a call frame for function2(). I'm ready to execute line 6, which
itself has a function call in it. Again hitting "Forward", now I've frozen
function2() and function1(), and I have a call frame for function3(). And
you can see how this process goes. This is what we call the call stack.
The idea is that we're working with an analogy like a stack of plates,
right? Every time that you call a new function, you stick a new plate on
top of the stack, and you can't access the previous plates in the stack
until you get rid of the plate that's on top. Now, it might seem a little
strange here because you notice that we're sort of working top-down,
right; the plate at the bottom of the stack is actually the top on the screen,
and the plate that is the top of the stack is down at the bottom. But if you
put yourself upside-down on your head, it'll all work out. OK; so let's just
continue with the call stack. This function is now going to return and it's
going to be erased. And now I'm ready to do function2(). But function2()
is still not ready to be finished, because what I'm going to do is I'm going
to stack one more plate on top of it as I call function4(). Now function4()
is finished. Now that both functions 3() and 4() are finished, I can finally
finish function2(). And now that I can finally finish function2(), I can finish
function1(). Already we can see that this the real power of the Python
Tutor. When we have lots of functions, which is typically what happens
when we have very, very complex Python code, we can put it into the
Python Tutor and see exactly what's happening with all of these call
frames. Because understanding call frames is really important when
you're understanding things like memory, right? Since all of the call
frames have to be in place and they can't be erased until you're finished,
if you keep making new call frames, you're just using more and more
RAM on your computer. And this can be really important for advanced
programs.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Break Up Functions
In the Codio project below, navigate to the page Overview of Exercise
4. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 4, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Visualizing the Call Stack
9LVXDOL]LQJWKH&DOO6WDFNFRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Defining Optional Arguments
The next activity in this module will ask you to add optional arguments to
a function. But what exactly are optional arguments?
Video Transcript
When you've worked with Python, you might have realized that some
functions actually take a variable number of arguments. To see what we
mean by that, let's talk about the function round(). So the round() takes a
float as an argument, and in this case I'm going to give it 26.54. And
when we hit the Return here, we'll discover that it evaluates it to the
integer that's rounded up from this; it takes the 5 and it rounds it up to 27.
That's OK if I give it one argument, but I can actually give two arguments
to round(), separated by a comma. So I'm going to give it that float and
I'm going to say ",1". Now what you'll notice is that I get a very different
answer, and what's happening is that Python is no longer rounding it to
the nearest integer; it's actually rounding it to the nearest ones place after
the decimal. So I get one set of behavior when I call it with one argument,
and another set of behaviors when I call it with two arguments. So this is
what we mean by a function that takes a variable number of arguments.
In this video, we're going to see how to do this with our own functions.
We're going to be working with a module called "opt". Inside of opt is
going to be a function called "point string". And what this does is this just
takes three values, which are essentially x, y, and z coordinates. So I'm
going to have x is 1, y is 2, z is 3, and it returns a string representation of
that three-dimensional point. So, that's what we've done here; we have
the point at 1, 2, and 3. The interesting thing about this function is I don't
actually have to call it with any arguments at all. I can actually just call it
with a set of empty parentheses, but it's not going to crash. What
happens in this case is that Python realizes that, oh, I must be talking
about the point at the origin, when x is 0, y is 0, and z is 0. So, how did I
get this to happen?
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
If we look at the definition of point_str here, we'll notice that it's very
similar to the definitions that we've seen before. We have a header, we
have a specification, and we have a body. The big thing that's different is
the parameters in the function headers. Normally, we would just list x, y,
z here. But you'll notice that we have some assignment statements. What
these assignment statements are doing is they are assigning default
values to each of these parameters. And we're telling Python, "OK; if
something is missing here, that's the value we should use." So since we
didn't put anything inside of the parentheses, it just used all zeros. Well,
we can actually build up on top of that. We don't have to do all or nothing.
We could just call it with a single value. I'm going to call it with — actually
let's call it with 4 so it looks a little different here. And now what you'll
notice is that since it saw 4, it's going to put a 4 in the very first
parameter, which is x; and for everything else, which is y and z, it's going
to use the default value. Similarly if I do 4, 5, now it has values for x and
y, but it doesn't have a value for z, so it's going to use the default. Now
you might say, "OK; well, what if I want to assign to y but not to x and z?
Because whenever I write a number by itself, it's going to go put it in the
first parameter that it can find." Well, so now we do a slight different
change in how we call the function. We're going to write "y=4".
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
so that we can reload it. Let's "import opt". And now, if I call point_str()
with no arguments, you'll notice that we get an error. And that's because
x doesn't actually have a default value; I have to give a value for x. But if I
give a value for x, it's now happy, and I can also essentially either give
values or use default values for y and z. But the last thing to keep in mind
is when we do this mix and match, once you assign a default value, like
we've done here for y, all of the remaining parameters must have default
values. So let's suppose we did something slightly advanced, where we
said, "OK, x and z are not going to have default values, but y will." We're
going to quit Python, reload this module, and now you notice that it
doesn't work; we've run into an error. And that's because Python can't
even read this definition at all. And that's because, as we said, once you
define one parameter with a default, all of the parameters from that point
on must have defaults as well.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Create Optional Arguments
In the Codio project below, navigate to the page Overview of Exercise
5. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 5, you
are done. However, do not submit the Codio project yet.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Tool: Fundamental Python Concepts 1
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Project Submission for Executing Functions
You have now completed all Codio exercises for the course module
Executing Functions. To submit the project, go to the page Complete
the Course Module in the Codio project and follow the instructions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Wrap-up: Executing Functions
In this module, you saw how to define your own functions and exactly
what happens when you execute these definitions. Professor White
reviewed basic terminology and explained how to define a procedure.
You also explored the differences between a procedure and a fruitful
function and practiced visualizing function calls using the Python Tutor.
Lastly, you examined how to access global variables, call functions from
functions, and define optional arguments.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module 2: Specifying Functions
Module Introduction: Specifying Functions
Watch: Motivating Function Specifications
Motivating Function Specifications
Watch: Writing Specifications
Reading Specifications
Watch: Specifying Preconditions
Watch: Identifying Types of Preconditions
Identifying Preconditions
Watch: Recognizing Bad Specifications
Tool: Fundamental Python Concepts 2
Identifying Bad Specifications
Module Wrap-up: Specifying Functions
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Introduction: Specifying Functions
In this module, we'll take a deep look at function
specifications. Professor White will explain the key role
that specifications play in the field of software
development. You will explore how to write specifications
and specify preconditions. In addition, you'll practice
identifying types of preconditions and recognizing bad
specifications.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Motivating Function Specifications
You may be wondering why this course has an entire module on
something as straightforward as specifications.
In this first video, Professor White explains the key role that specifications
play in the field of software development. Software development is a
business, and it's not just about writing code but also about proper
business processes.
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
better code development. One of the things to understand is that complex
projects need multi-person teams. Sure, you might have some projects
that involve a lone programmer for doing very simple contract work, but
anything else you typically need to have teams of people working
separately but bringing back the work that they do together. So a lot of
these processes are about how do we break up this work? What pieces
of the code do we give to each team member? And how do we fit these
pieces back together? Now, you might have heard of some of the names
of some of these processes; you might have heard of things like waterfall,
or iterative, or agile, or maybe even DevOps. All of these terms are
actually beyond the scope of this course. You need to have a stronger
programming background to understand what these terms mean. But
there is a basic principle underlying all of these methodologies; this
principle about enabling the communication between your programmers
and how to integrate your work. And that principle is the specification.
And that is why that is the focus of this course module.
In this video, Professor White explains how functions are a way to break
up software development among team members and how specifications
enable that separation of responsibilities.
Video Transcript
Now, an interesting thing that happens here is, what happens when a
code breaks, right? Suppose I have a bug in my code and I've narrowed
it down to a single function. But there are multiple people involved, right;
there's the person who defined the function and the person who called
the function. So, whose fault is it? Who wrote the code that had a
problem, and what do we have to do in order to fix this? Well, this is
exactly the purpose of a specification. It clearly lays out responsibility.
What does the function promise to do and what are the allowable uses of
that function? So from that responsibility, we can determine, is the definer
at fault or did the definer implement the function properly? On the other
hand, maybe the caller is at fault because the caller used the function in
a way that wasn't allowed. So, a specification isn't just helpful comments;
it's actually a business contract. And because it's a business contract, it
requires a much more formal documentation style. And in fact,
techniques like agile or iterative, what they are is they're ways to safely
modify a contract after it's written, but they don't necessarily have
anything to say about the contract itself. Now, you might ask, why do you
need to know all of this? Well, we've taught you how to write functions,
and you know all of the technical details that you need to know about
that, but that's not what people get paid money for. People get paid
money to write code to solve problems. You're given some sort of
specification of a problem in English, and you need to write code to that
specification. Doing that properly and well means that you need to
understand specifications. What makes a good specification, and what do
we do if we have a bad specification?
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Motivating Function Specifications
0RWLYDWLQJ)XQFWLRQ6SHFLILFDWLRQVFRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Writing Specifications
Now that you've seen the importance of specifications, the next step is
learning how to write specifications effectively. In this video, Professor
White breaks down the structure of a specification to show how, when
done correctly, it should clearly communicate what the function does.
Video Transcript
Now that we know why specifications are important, let's look at some
example specifications and let's see how they're structured. On the
screen, we have a definition for a procedure called greet(), which prints
out some information for the person who's calling it, which we'll call the
user. Right away, we can see that the specification's pretty long; it's
actually a lot longer than the rest of the body itself. There's a lot of stuff
going on here. The first thing that we'll notice in the specification is a
description of what the function does. It's supposed to print out a greeting
to the person. Interestingly enough, you'll notice that we have a blank
line, and then now we have another description, which seems to say a lot
of the same things that the first one did. And there's a reason for this. In
every single Python specification, what we like to have is a single one-
line summary of what the function does. The purpose of this is, I might be
a user who has several functions to choose from and I don't know which
function is right for me. Instead of having to read all of the specification in
detail, I can just very quickly look at these one-line summaries. But that's
exactly what they are; they're summaries, and they might not have all of
the details. In this case, in this summary, I know that, oh, it's going to
print out a greeting to the name n, but I don't know what it really means to
be a greeting. Well, after that I have some more detail where I tell the
user, "Oh, the greeting is actually something of the form 'Hello' plus
name. Oh, and then I might have something after that to try to start a
conversation.
Now, these extra details don't have to be a single sentence; they can
actually be multiple paragraphs. It's not uncommon for the function
specification to be a lot longer than the rest of the body of the function
itself. After you're done with all of these details, the last thing that we do
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
is talk about the parameters, right? Remember that parameters are the
variables that exist in the header of the function definition. To call a
function, I need to provide arguments which are evaluated and plugged
inside of these variables. That means that the person who is calling that
function needs to know, what are the right types of arguments to provide
this function? Right away I'm telling them that, "Oh, parameter n is
supposed to be the name that you use in the greeting." So I know that
when I call the function greet(), I'm supposed to give it a name as an
argument.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
lot harder than coding, and learning how to specify is actually a larger
part of a computer science degree than actually writing code. That's the
reason why our specifications are a lot more structure than what you're
going to see in this guidance. And the purpose of this is to hopefully cut
down on your mistakes right now. But with that said, we do adhere to the
guidance from the PEP guidelines.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Reading Specifications
5HDGLQJ6SHFLILFDWLRQVFRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Specifying Preconditions
Specifications are a form of business contract. Preconditions are a way of
making that contract formal. They are a promise that, when a function is
called with all arguments satisfying the preconditions, then the function
will work correctly.
Video Transcript
What I'm going to do is I'm going to import the module "temp" and I'm
going to call the function to_centigrade() on 32 degrees Fahrenheit. I hit
Return and I get 0 degrees centigrade back. That's what I expected. And
that's because I called this function properly. The argument that we
provided was a float, just as was requested by the precondition. We know
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
it's a float because of the decimal point right there. Now I'm going to do
something a little cutesy. I'm going to call to_centigrade() on 32, but
instead of doing 32 as a number, I'm going to do it as a string. Right
away, you'll notice that it crashed. Clearly it crashed, right?
to_centigrade() isn't designed to work with strings, right? I say that I want
it to be a float, but I provide it with a string instead. This is what we call a
precondition violation. We've called the function, but the argument that
we provided violates the precondition that's in the specification. Now, one
of the things you have to be careful with is, not all precondition violations
result in a crash. Sometimes, it might still work. Let's do one more
example. This one's a little less clear-cut. Again I'm going to call
to_centigrade() with 32; this time as a number, not a string. And of
course, it seems to work, but this is technically a precondition violation.
That's because 32 here is not a float; it's an int. There's no decimal point
here. So, that does indeed violate the specification, though it still worked.
This is what we call undocumented behavior. The definer didn't promise
that it worked with integers, but it still did. Taking advantage of
undocumented behavior is very bad practice, right? Remember that the
specification acts as a form of contract. The definer of the function is held
to that contract, but they can change anything else at any time. So the
definer for this function later on could decide, "Well, I didn't promise for it
to work for integers," and so they might make it crash instead. This hits
Microsoft developers a lot. Microsoft is very, very good about keeping its
specifications fixed, but they change details that are unspecified all the
time. If you take advantage of unspecified behavior and your code
doesn't work, you have no one to blame but yourself. So indeed, that's
what we're talking about: the notion of responsibility. Suppose I have a
function; the people involved with the function are the person who defines
the function, and then I have someone who might call the function. And I
get an error, and I've somehow localized that the error is somewhere
around this function. And I ask, which of the two parties here are at fault?
Well, right away we look at, was the precondition violated? If the
precondition was violated, we absolutely know that it is the fault of the
person who called the function, and they are responsible for cleaning it
up and fixing their code. On the other hand, if the precondition was
properly met and something didn't go right, then it's the fault of the
developer, the person who defined the function, and they are the one
who needs to go in and fix that code.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Identifying Types of Preconditions
Now that you've seen what preconditions do, it's time to examine them
further.
Video Transcript
But there's actually a more complicated thing here; I say that it has to be
a reference to a valid file. In addition, I say that the file only has a single
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
integer in it. So if I were to make a change, and instead of having a 4
here, I would replace it with an a. Now, if I were to read from this file, now
it would crash because it can't turn an a into a 4. So all of these things
that we see inside of the precondition are important things to tell me
about whether or not it's proper to call this function or not. Now, why do
we separate these two; why do we separate type restrictions from more
general types of restrictions? That's because it's actually easy to check
whether or not a type restriction is violated or not. Let's clear Python, and
let's make a variable called x. Well, there's actually a function in Python
called type(), which allows us to check on the type that is stored inside of
that variable right now. I can see right now that what's inside of that is an
int. If I were to change the value of x to 3.5, now when I look at the type
of x, I can see that it's a float. And I can actually write code to check
whether or not it's an int or a float. If I were to write this as a Boolean
expression, "== int", that's false because it's not currently an int. If I were
to write "== float", that's true because it is equal to a float. And this is nice
because having ways to actually check whether or not a precondition is
violated or not can often make it easy to find whether or not there are
problems in our code or not. Other languages, like C or Java, actually go
even further than this; they actually restrict the way that you can use
types in their languages. Here, in Python, I was able to take the variable
x and I could put an int in it at one time and a float at another time. In
those other languages, the variables themselves have types. So int
values can only go inside of int boxes, and float values can only go inside
of float boxes. These are what are known as statically typed languages,
and it acts as a way to enforce preconditions. People typically find this
ideal for very large and complex software. On the other hand, languages
like Python and JavaScript, well, they're dynamically typed. They'll allow
anything, but they'll crash if they're misused. That's the reason why those
languages have to rely entirely on the specification. So an interesting
question is, is statically typed better or not? Well, some people prefer
static typing for beginners because it'll quickly shut down whenever you
violate your preconditions, and a lot of errors are much easier to find. But
this creates a false sense of security. As we pointed out, not all
preconditions are expressible as a type; some are more general, like that
file. So you need to get used to reading specifications anyway, and that is
why we put so much focus on specifications.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Identifying Preconditions
,GHQWLI\LQJ3UHFRQGLWLRQVFRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Recognizing Bad Specifications
Now that we have introduced the idea of a function specification, let's
look at what exactly constitutes a good specification.
Video Transcript
Most of the time when you're working with a function you only have the
specification to go by. Typically, the definition of the function is hidden
away somewhere on your computer. Even if you have access to the
definition open in a file, well, a lot of times functions are written by
subject-matter experts, and it might be too complicated for you to
understand that definition. With that said, not all specifications are good.
They can be incomplete, they can be missing details. This is particularly
common in open-source software. But what this means is you need to be
able to figure out whether or not a specification is good just by reading
the specification itself; not by looking at the definition. This is a very
imprecise scale; it's not a science. And it's something you just build up
with experience. So with that said, there are some sort of rule of thumbs
that you can use. Right away, when you look at a specification, the first
thing you should ask yourself is, are the preconditions clear? Can you tell
what is allowed, what is not allowed? You should pay close attention to
whether or not there are any sort of type restrictions on the preconditions.
But with that said, you should also think about the weird cases; things
that aren't precondition violations, but all seem to violate the spirit of the
specification. Beyond the preconditions, you should think about the return
result if it's a fruitful function. For every single argument that satisfies the
precondition, do you know what answer should be returned? If not,
maybe you need some more information in the specification. Procedures
are the same way. They don't return anything, but they have an outcome.
So you ask yourself, is the outcome for this particular argument clear? All
of this is a little abstract, so let's look at a very specific example.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
can see, we don't actually have a definition here; we're going to
implement this definition later. But we do already have the specification,
and there are a lot of nice details in the specification. The specification
tells us that this is a fruitful function that's going to return the number of
vowels in the string w. The precondition seems to be fairly precise. It tells
us, OK, we only want to have strings. We don't want to count vowels
inside of a number like 24. And in addition, that string should only have
letters, so we don't want to deal with punctuation or any sort of fancy
things in our strings. That looks pretty clear. But let's think about it a little
more carefully, right? So, let's just think about particular examples. Let's
think about the word "hat." Well, this satisfies the precondition; it's a
string, it only has letters in it. So that's good; we understand that. But do
we know, without hitting Return now, what the return result should be?
Well, a is a vowel; it's the only thing here that's a vowel, so the answer
should be 1. Great. That's our answer. Let's do something a little more
complicated. Let's suppose I have two vowels. Let's do the word "heat,"
all right? Well, there's an e, and there's an a. Those are the only vowels;
there are two of them. I expect the answer to be 2. Indeed that's the
case. All right; let's go for some weirder questions. How about "sky"?
Right away, now we start to realize it's unclear what the answer is. Is y a
vowel, right? When we talk about vowels, we say, "and sometimes y." So
is the answer 1? Or is it 0? Well, in this case, from context I might say,
"OK; well, it's a vowel, so the answer is 1." But let's suppose we had the
word "year" instead. Now is the answer 3? Or is it 2? And right away we
start to realize that things are getting complicated. But the reason why
they're getting complicated is because we're actually adding extra
assumptions that are not in the specification itself.
For example, let's suppose I type this word. This is not an English word.
Nowhere in the specification did I say these strings have to be English.
This is a Welsh word. And in this case, there is a vowel; that vowel is w.
So should I include w as well? In fact, it's even more complicated than
that. At no point did we say these had to be words at all. All we said was
they were strings with letters. So we should be able to type anything in
here and it should give us an answer about the number of vowels.
Beyond that question, we also ask ourselves, what does it mean when
we talk about the number of vowels? Let's suppose we have the word
"beet." Is the answer here 2, or is it 1? I mean, there are two letters e, but
there's only one distinct vowel e. So do I allow repeats, or do I not count
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
repeats? If I don't count repeats, do I make a distinction between
lowercase or uppercase? So what we've actually seen here is this is
actually a pretty bad specification. It's very, very incomplete. And what's
happening is it's taking advantage of the confusion between string and
word. And what's happening is we're filling in the details with our own
assumptions. This is very, very dangerous. Most of our assumptions are
cultural, and a lot of times when you're working on a software team,
you're working on an international team. Cultural differences between a
specification can cause a breakdown between teams. In fact, even in my
own classes working with certain students from Asia, they don't
necessarily know right away what a vowel is. This is why we need to be
very, very precise in our reading and make sure that we're only doing
exactly what the specification says, and nothing more.
Video Transcript
Now, while this detail should be enough for someone to go off of, what
we're also doing here is we're providing several helpful examples to the
user to make these details clear. And one of the things that we're trying
to do in these examples is go through all of the possible arguments
where there might be some ambiguity and resolve that ambiguity. So we
have a simple example with just one vowel, and here we have another
example with multiple vowels, but we've made it clear in this particular
example that repeated vowels are counted as well. In the next two
examples, we say, "Aha, 'sky,' now this counts as a single vowel, but
here where y is at the beginning, it doesn't actually count as a vowel."
Now, so one of the things that happened here, right, is that we took a
specification that was imprecise and we made it much more precise.
However, one of the things you must understand is, typically you're not
allowed to do this; not even if you are the function definer. That's
because specifications typically come from higher up in the management
chain; they're coming from, say, your senior software engineer. And the
rules for changing specifications are very important. A lot of the
methodologies that you might have heard of, like agile or waterfall or
iterative, are really rules about changing specifications. If you're someone
who's implementing a function but you've been provided a specification, if
the specification is unclear, you should always ask for more guidance;
typically from the specification author who's higher up in the chain. In this
course, that means the instructor. So if any project, we give you a
specification that you don't understand, you should ask us those
questions because we would rather that you get it right the first time,
because adding new assumptions is always dangerous.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Tool: Fundamental Python Concepts 2
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Identifying Bad Specifications
,GHQWLI\LQJ%DG6SHFLILFDWLRQVFRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Wrap-up: Specifying Functions
In this module, you took a deep look at function specifications. Professor
White explained the key role that specifications play in the field of
software development. You explored how to write specifications and
specify preconditions. You also practiced identifying types of
preconditions and recognizing bad specifications.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module 3: Testing Functions
Module Introduction: Testing Functions
Watch: Identifying Errors
Reviewing Terminology
Watch: Designing Test Cases
Designing Test Cases
Designing Tests with Multiple Arguments
Watch: Scripting Tests
Activity: Script Tests
Read: IntroCS Documentation
Watch: Organizing Test Cases
Activity: Organize Test Cases
Watch: Debugging Discovered Errors
Activity: Debug Discovered Errors
Tool: Fundamental Python Concepts 3
Project Submission for Testing Functions
Module Wrap-up: Testing Functions
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Introduction: Testing Functions
In this module, Professor White explains how to check a
function for errors. Then, you will examine how to design
test cases to search for errors. Professor White also
introduces the unit test, a faster way to test, debug, and
retest a function. In addition, you'll explore how to
organize unit tests and how to debug discovered errors.
A tool that you'll encounter in this section is the IntroCS module. If you
haven't already installed this, do so now.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Identifying Errors
A common first step in checking a function for errors is to perform testing.
Video Transcript
In this course module, we're going to talk about how we test the functions
that we write. Now, before we do this, I want to introduce some important
terms; terms you've probably heard before, but I need to make them
more precise so we can be careful about how we use our language.
Throughout this course module, I'm going to talk about bugs, and bugs
are just errors in our programs. I'm not going to be any more precise than
that. And if we're talking about bugs, obviously we talk about debugging.
And debugging is just the process of finding and removing bugs. Testing
is not the same thing as debugging. Testing is just the process of
analyzing and running a program. Now, testing is a common way to
search for bugs; however, it's not the only way, and it also doesn't
address how you remove a bug once we know that one is there. With that
said, good debugging always starts with testing. Now, when we're using
testing to search for errors, we create what are known as test cases. A
test case is an input together with an expected output. Now, that input is
just one or more arguments for our function, and the output is what is
returned, provided that it is a fruitful function. If it's a procedure, we have
a slightly more broad notion of output, but it works pretty much the same.
So for this course module, we're only going to focus on fruitful functions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
notice that here I have a sample input for number_vowels(), and I say
that, "Oh, you should expect that this gives us the answer 1 back. And
here is another input; in this case, we should expect that 'hate' should
give us the answer 2 back." So we could consider all of these together as
a testing plan. So, now we're going to show you, how do we take this
particular specification and we actually do formal testing. We're going to
take Python, we're going to start up Python, and we're going to import
this Python module. And we're going to just start testing out the function.
How do you test a function? You just call it with those particular inputs.
So I'm going to call number_vowels(), and we're going to do 'hat', and we
expect the answer to be 1. Great; that's the correct answer. 'Hate'; we
expect the answer to be 2. So far, so good. 'Beet'; we expect the answer
to be 2. Once again, so far, so good. 'Sky'; now we noticed that when we
called the function, we got the answer 0, but the specification said that
the answer should be 1. Well, when the specification and the function
disagree, well, the specification always wins. The specification is always
more important than the function definition. So this tells me that I actually
have an error somewhere down here in my code. Now, there's a lot of
code here; I don't know exactly where the error is, but I'm just going to go
ahead and sort of reveal the secret, which is I forgot to add a y down
here at the bottom. So now what I'm going to do is, I'm going to save this,
and I'm going to quit, and now we're going to start this process all over
again. Let's "import vowels". And you might think, "OK; now all we need
to do is we just need to test 'sky'; it's working properly, and we can just
continue on from there." This is not how we do things. Every single time
that you fix a function, you go back to the beginning, and you start
working on the plan again. Why is that? Because maybe when you fixed
one thing, you broke something else. So we always very methodically go
through all of the test cases in our testing plan, and it is only after we
pass every single one of the cases in our plan — which we do in this
case — that we decide that our testing is complete. This is something
that we're going to go into much, much more detail throughout this entire
course module.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Reviewing Terminology
5HYLHZLQJ7HUPLQRORJ\FRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Designing Test Cases
You have seen how test cases are created to search for errors. Now you
need to decide which test cases you want to use.
In this first video, Professor White describes how to determine which test
cases will best check your function for errors.
Video Transcript
Then, what you'll notice is that we've started to look at more complex
cases, right? So, "hate" and "beet", these are examples with multiple
vowels. There's obviously a difference between "hate" and "beet," namely
that one has an a in it and the other one doesn't. But more importantly,
there's another distinction, which is "hate" has two different vowels in it,
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
and "beet" has two vowels that are the same. So now I have to think
about all of the different ways that I can come up with vowel
combinations. After I've just been counting vowels, then I need to start
thinking about weird possibilities. Well, what do I mean by "weird
possibilities"? Well, y is unusual. Y at the end of a word counts as a
vowel, but y at the beginning doesn't, so I need to make sure that I have
tests with y at the beginning and not at the beginning so that I can know
that it works properly. And finally, I want to have a test where there's no
vowels, and I need to make sure that I can actually work properly when
things are missing. So you might just say, "Oh, wow, I'm just going to be
testing any sort of string." And it is true, right? You may find yourself
adding lots and lots of tests. But there is one important rule, which is you
should never test a violation of a precondition. What do we mean by this?
Well, let's say we thought, "Let's do the test number_vowels('12a')."
Here, we have a string, but you'll notice that this string actually violates
the precondition because the precondition says that w is at least one
letter and only letters. I have things other than letters here. You might
say, "This doesn't harm anyone. I see that there's an a here; I should just
return 1." But remember, we've talked about assumptions before. You're
only supposed to follow the specification exactly as it is said. Since 12a
violates the precondition, you have no idea what is supposed to happen.
Maybe it's supposed to crash. Maybe it's supposed to give me an
answer. There are no guarantees at all, so there's no way for us to write
an answer here. So therefore, we should never include this in the test
that we pick.
Video Transcript
All right. First we'll start off with 'hat'; that has a single vowel in it, so the
answer is 1. Let's try a little more interesting word, 'charm'. That also has
a single vowel in it; its answer's 1. How about 'bet'? This also has a single
e in it; the answer is 1. OK; let's try a little more complicated, 'beet'. Now I
have two es, so the answer is 2. 2 is fine; let's — how about three? Let's
try 'beetle'. So what we have now is five test cases. But the question to
ask yourself is, how many really different tests are there here? Think
about this a second; pause the video if you need to before we give you
the answer. OK; have you thought about it? Well, the answer is, even
though there are five test cases here, there are really only three different
tests. Why is that? Well, obviously "hat" and "bet" are different because
they each only have one vowel but they're different vowels. Just because
you work for a doesn't necessarily mean that the function works for e.
"Charm," however — OK; I mean, it's got a c and it's got an r in it, but all
this function is doing is counting vowels, so I don't really care about the
consonants in there. And if I'm just counting vowels, I mean, "charm" is
the same as "hat"; it's not really different. OK; well, I can still think that
"beet" is different than "bet" because multiple vowels are very different
than one vowel. But I would also say that "beetle" is pretty much the
same thing as "beet." And that's because, in general, once you know that
something works for two things, you probably know that it works for more
than two. What we're talking about here is the rule of numbers. When
you're testing, the numbers are 1, 2, and 0. Number 1, well, that's the
simplest test possible, right? And the idea is things like, oh, a single
vowel, right? And I want to do those first because if that fails, I can't do
the complex tests. Number 2 is when you start adding things that are
more than expected, right? More than a single vowel; in this case maybe
multiple vowels, all the different ways of combining them with the same
vowel or different vowels. Finally, 0 is what do you do when something is
missing? Maybe the word has no vowels in it, or maybe I have a y there
but the y isn't actually a vowel, so it's something that's missing.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
To see what we mean by the rule of numbers, let's look at a slightly
different function that's a little more easy. This is a function called
last_name_first(). And what this function does is it takes the string of a
first name and a last name and it gives it back to me rewritten, with the
last name, a comma, and then the first name. This is all the function
does; I don't have to worry about different types of vowels or anything like
that. The only thing that matters is, how do I separate the first name from
the last name? And the specification says that it can be one or more
spaces. So you'll notice that in this example we have one test case. This
is the number 1, right; the simplest thing possible with a single space in
the middle. Here we have the number for Rule 2. We have more than one
space. We could put two spaces here, three spaces here; it doesn't really
matter. You'll notice that we don't actually have a test here for the
number 0. Why is that? Well, actually, it's because the precondition says
that there has to be one or more spaces, so it's not possible for anything
to be missing here. Remember, we never test anything that violates the
precondition, and the rule of numbers is not an exception to that. So all I
do in this case is I test number 1 and test number 2. We can be pretty
happy that, yeah, actually this is a pretty good enough test for this
particular function.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Designing Test Cases
'HVLJQLQJ7HVW&DVHVFRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Designing Tests with Multiple Arguments
'HVLJQLQJ7HVWVZLWK0XOWLSOH$UJXPHQWVFRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Scripting Tests
When you consider testing, debugging, and retesting a function over and
over, you might wonder if there is a better way.
In this video, Professor White introduces a faster way to do this: the unit
test.
Video Transcript
Up until now, we've been focusing on the theory of testing. But in this
video, we want to delve into the practice of actual testing. To help
ourselves understand, we're going to work with the function last-name-
first() again, because it's rather simple to test; it only has two test cases.
Remember that this is a function that takes a string that has two names,
and it's going to reorder them so that the last name is first, followed by a
comma. So let's think about how we would test this particular function.
Right now we have to type "python" to start it up. We have to import the
module. From that module we have to call the function last_name_first().
And we have to give it the input of our test cases. So we're going to do
the rule of one first with a single space, and everything is fine. Now we're
going to do the rule of two; I'm going to give it more than one space, hit
Return, and now we'll notice that actually there is a problem with this
function, right? It may seem like it behaves naturally, but the specification
makes it very, very clear that there are not supposed to be any spaces
before the last name, and here I have some spaces before the last name.
Remember we always read the specification exactly. So this is not
correct, and I have a function that doesn't work. We're not going to worry
about fixing this yet; we'll handle that in a later video. But let's keep in
mind what happens when we do fix it. What do we do?
Well, we have to quit Python. We have to start Python again, import the
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
module again, and we have to start this process all over. This is
incredibly annoying. And we're only talking about a function that just has
two test cases; I have to do this all the time? Look, we're learning how to
program. We're learning how to automate things. Why can't we just
automate testing? That's an interesting idea. What if we made a second
Python module, or even a script, that could test the first one? That's what
a unit test is; a unit test is a script that is used to test another module.
That script imports the module so it can test it, and inside of that script
you define one or more test cases, and it's just going to evaluate those
test cases. It's going to call the function on the input and it's going to
compare that result to the output that you expected. If everything is
working, it does nothing; it just prints out a clean bill of health. If it's not
working, well, it's going to try to give you some useful information.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
the function. And so I'm taking the answer that that function gives me and
I'm putting it inside of a variable. That's where the answer actually is.
Now, I want the answer to look like this. So what assert_equals() is doing
is it's comparing the answer that I expect against the answer that I
actually got. You'll notice that this is a procedure; I'm not trying to assign
it to a variable or anything like that. So what does this procedure do?
Well, if the answer matches, it does nothing. To see that in practice, let's
look at the test script for number_vowels. You'll notice that lots of single
test cases here, and then there's a print statement down at the bottom of
the module. When we ran that particular test script, the only thing that we
saw was this final print statement; we didn't actually see anything here.
And that's because, look, if you pass the test, I don't need to know
anything more; I can just move on to the next test. However, if you fail a
test, then what it's going to do is it's going to stop testing immediately and
give me the line number that failed. So if we look at this error message
here, we will notice that at line 19 we failed. So we can see that this test
case is the one that failed; the one that uses the rule of two. And now I
can use this particular test case and help me to search for my error.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Script Tests
In the Codio project below, navigate to the page Overview of Exercise
1. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 1, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Read: IntroCS Documentation
This documentation is available for you to refer to as needed throughout
the remainder of the course. However, you do not need to read all of this
documentation, as we will not be using all of the features of this module.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Organizing Test Cases
There will be situations where you will be testing multiple functions and
need a way to keep them organized.
Video Transcript
In the real world, we can have a lot of test cases. I mean, we've already
seen the function number_vowels(). And if we look at the test script for it
and we scroll through it, we can see, yeah, there are a lot of test cases
here. Well, this is nothing; there's not actually a lot of test cases here. For
my game design courses, I've written some software that my students
use, and because it's important to know that it's correct before I put it in
the hands of my students, I've tested it. It's not an exaggeration to point
out that there are over 20,000 test cases for this piece of software. Now,
that's not all for just one function; there are lot of functions in this piece of
software. But it does mean that I need a way of organizing all of these
tests. Now, the way we've seen that so far is we've created a separate
test script for each function. Here we have the unit test script for
number_vowels(), and here we have the test script for last_name_first().
Well, that's fine, but the problem is, is now I have to run each script for
each function. The way I got to 20,000 test cases is because I have a lot
of functions and I don't want to have to run each one separately. What I
really want is one script that runs it for all my functions. And so now
inside of that script, we have to have a way of separating the test cases
for each function. We do that with what are known as test procedures. So
here we're looking at a unit test script that tests both number_vowels()
and last_name_first().
You notice that I have a function definition here. This function is called
test_number_vowels(). It's different than number_vowels(). This is the
function that's defined here. Test_number_vowels() is a test procedure
whose purpose is to test the function number_vowels(). And the only
thing that's inside of the body of this function definition are my test cases.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Similarly, when I want to test last_name_first(), I have a separate function
called test_last_name_first(). It's not the same as the function
last_name_first(); it is a test procedure to test that function. And the only
thing in the body of this are my test cases. So, that's how I organize all
my tests; I've essentially created these test procedure definitions. But
remember, a function doesn't do anything unless you call it. So down at
the bottom of the file, I need to call these tests in order to execute them.
So let's see this in practice. I'm going to run this particular test, and you'll
notice that it crashed, because still, last_name_first() is not working
correctly. What happened was it actually crashed during these tests so it
didn't even reach any of the tests for number_vowels(). Now, right away
we can see an interesting thing about how we've organized our tests
here. Let's suppose I've decided that I can't find the bug for
last_name_first() and I just want to move on to other functions for right
now. Well, what I can do is I can put a hashtag in front of this function
call. And now I'm going to run it, and you'll notice that it passed all of the
tests.
Why did it pass all of the tests? Because I no longer have a function call
for test_last_name_first(), right? Putting a hashtag in front of it turns this
into a comment, and all comments are ignored. So the only test that is
actually executed is test_number_vowels(). And we know that function
works properly, so it passes all of the tests. So one of the things that you
notice is that I can easily turn tests on and off without just deleting the
tests. The tests are still here, but I've deactivated them. That's why I have
the separation of the test procedure and the call. It's also why you'll
notice that in our test procedures, we have these print statements. Notice
that I say here I'm printing last_name_first(), and here I say that I'm
printing — I'm testing number_vowels(). The reason why we do that is
because if you look on the screen here, I don't actually see a test for
last_name_first(). I'm reminding myself that I've deactivated that test. So
OK; now when I actually want to go test that, I'm going to remove the
comment, I'm going to run the script once again. Now you'll see that it is
actually running that test, because I have that print statement. But now it
crashes, and so I have to go working on this function once again.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Organize Test Cases
In the Codio project below, navigate to the page Overview of Exercise
2. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 2, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Debugging Discovered Errors
When testing code, locating an error is only part of the task. The next
steps are finding out exactly what the error is and fixing it.
Video Transcript
We've been working with this function called last_name_first() that has an
error in it. We haven't been able to remove the error yet, but we know
that it's there. Let's remind ourselves this is this function that takes a
string with two names in it and it reorders it so that the last name is first.
The reason why we know that it has an error in it is because we have a
test script. It has two test cases in it. One follows the rule of one; it has
one space between the two names. The other one follows the rule of two;
it has more than one space between the two names. When I run this test
script, you'll see that it crashes, and it's crashing on the rule of two; line
29 is the second test case. And that's great, but it doesn't actually tell us
what the error is or how to fix it. Well, to understand how to fix errors, you
need to understand that there are two different types of testing: black-box
testing and white-box testing. In black-box testing, the function is opaque;
it's a black box. You're not allowed to look at the function definition.
You're only allowed to look at what the function does. Well, that's what
we're doing here in these test cases; we're taking the function, we're
giving it an input, that's giving us a result, and we're comparing that result
with what we expect in order to test it. White-box testing, the function is
transparent. All of the tests and debugging take place inside of that
particular function. The way that we do that is through the use of print().
Sounds a little weird, so let's see this in practice.
Here we have the function definition for last_name_first(), and you notice
that I have a bunch of print statements that all have hashtags in front of
them. Well, these hashtags are essentially making them into comments.
Right now Python is ignoring the print statements. We call this
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
"commenting out" a line of code. What I'm going to do is I'm going to
remove these hashtags — this is called "uncommenting" — and that's
going to make it so that Python now pays attention to these print
statements. We're going to save, and we're going to run the test again.
You'll notice that I have a lot more information here on the screen. That's
because all of these print statements are being executed. In fact, we
have — one, two, three, four, five, six, seven — eight new lines here that
weren't there before. Now, by the way, you might think that's a little
strange because we've only added — one, two, three — four print
statements. But let's remind ourselves that we actually have two test
cases. So what we're looking at is two groups of four. These are four
print statements for the first test case, and these are four print statements
for the second test case. Now, we're only going to talk about how we
read these in the next video. But what I want to do is just think about at a
high level what we're trying to do with these print statements. You'll
notice that when we're writing our functions, right now, essentially all of
our functions are either assignment statements — like I have three
assignment statements here — or a return statement. There are only four
lines that are doing any of the work here. The print statements are just
used for debugging. The goal of white-box testing is to locate the exact
line with the error. Is it on this line? Is it on this line? Is it on this line? Or
is it in the return statement? Now, once you find the exact line, what do
you do? Well, you just look at that line really hard until you can find the
error.
There's no magic bullet there, and actually that's what you had to do in
an earlier assessment. You were given a function that had only one line
in it, and you had to look at it to find the error. But the key thing to
understand is, is that what's going on is very, very similar to black-box
testing. Each one of these lines is an assignment statement that changes
a variable. Here I'm changing "end_first", here I'm changing "first". The
print statement, I am printing out the variable that I have now changed.
And that's the idea, right; I expect that variable to be something, and
what I want to do is I want to print it out to see if it is indeed what I
expect. This is pretty much the same thing that's going on in black-box
testing. With that said, we don't use this all the time, right? Why do we
like to use unit tests as opposed to using print statements? That's
because print statements, you have to remove them when you are done.
If you remember, we had all of these things with hashtags in front of them
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
that were commented out. Why is that? That's because the presence of
print statements are not allowed. They're not part of the specification or
contract, so they're clearly a violation. They also slow everything down
unnecessarily. But finally, they're just unprofessional. The Apple App
Store will reject any app that you submit to it that has print statements
inside of it. So that's one of the reasons why it's not as ideal. With that
said, right, we don't have to completely erase them. That's what we've
done with the comments; we only have to turn them off. And this is a very
common thing that people do with debugging and testing. In fact, if you
look at a lot of open-source code, you'll notice that it is littered with print
statements that are written in comments so they're not actually active.
Video Transcript
All right; so the next line is an assignment statement, and you'll notice
that I have a function call here, find_str(), inside of the module IntroCS.
This module IntroCS is provided for this course and we link the
documentation on the course web page. If you look at the documentation
for this particular function, you notice that what it does is it tells me the
location of one string inside of another. What I'm doing is I'm using it to
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
find the location of the space inside of the string n. If we were to look at
this string here, we'll notice that this space appears at position 6. So
when I print out "end_first", well, yeah, 6 is what I expect the answer to
be. On the next line I'm using string slicing at "end_first" to give me the
first name. And indeed that's what I expect. And then I'm again using
string slicing to get the last name, and that's what I expect. Every single
line, the print statements are what I expect, and that's great, right? I
mean, there were no problems for this particular test case. So let's look
at the problem test case and let's see if we can use it to find the error.
Here we have more than one space. Now, on this line, I'm just trying to
find the end of the first name. So even though I have multiple spaces,
that's really just the first space. And the first space is still at position 6, so
there's no problem there. And when I slice out the first name, I'm still
slicing that out correctly. The last name is different, however, right? Now
you'll notice that I'm getting all of these extra spaces at the beginning, so
this is where my problem is, and this is what I need to fix in order to fix
the function. OK; so let's ask ourselves, how do we fix this function? I
mean, the problem is, is that we have more than one space here. So I
can use this space to slice out the first name, but I need to use this space
to maybe slice out the last name. And that would require another search.
I could do that, but I don't want to do solutions that make my code more
complicated.
Is there an easy way to solve this particular problem? Well, let's go look
at IntroCS again, and let's go look at all the functions that we have
available to ourselves. And you'll notice that there's this function called
strip(). And the entire purpose of this function called strip() is that it
removes extra spaces that I don't want to have. So this suggests a really
easy way to finish this function. I'm just going to call "introcs.strip()" on
this particular string. I'm going to save it, I'm going to run the script again,
and now you notice that there's no error. Actually, the module is working
correctly. I still have the print statements, and remember we do need to
remove these, else Apple's not going to be happy when we submit
something like this to the App Store. So I'm going to remove it. And
whenever you make any modification to a function, even if it's just
removing debugging code, please remember to run the test one more
time to make sure that you didn't break anything. And there we've done it,
right? We've fixed everything. OK; so that's great. That's how we use
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
print statements to test and debug our functions. There's one more thing
that I'd like to say, which is, notice if you're looking at these print
statements, it's really hard to tell what's going on. I mean, we were able
to look at it really closely and sort of line this up with our code, but there
was no need for doing that. We could have made our print statements a
little more helpful to have us understand what's going on. To see what I
mean by that, here's an alternate version of last_name_first(), and what
you'll notice is that our print statements are a little more descriptive.
Instead of just printing out variables, we actually sort of combine them
with strings to make complete sentences. I'm going to change my test
script to use that particular module instead. So I "import annotate". I'm
going to call it "name" so I don't have to change a lot of stuff in my test
script. And now what we're going to do is we're going to run this particular
test script again. We have the error because this version of the function
hasn't actually been fixed, but notice how everything is a lot different,
right? I have a lot more helpful messages on the screen, and this is a lot
more useful when I'm debugging. Now, should you do this? It is helpful.
With that said, there are a lot of people who do very bare-bones print
statements just like this. It really is sort of a matter of personal choice,
and you should do what it is that you're most comfortable with.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Debug Discovered Errors
In the Codio project below, navigate to the page Overview of Exercise
3. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 3, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Tool: Fundamental Python Concepts 3
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Project Submission for Testing Functions
You have now completed all Codio exercises for the course module
Testing Functions. To submit the project, go to the page Complete the
Course Module in the Codio project and follow the instructions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Wrap-up: Testing Functions
In this module, Professor White explained how to check a function for
errors. You then examined how to design test cases to search for errors.
Professor White also introduced the unit test and the IntroCS module,
which are faster ways to test, debug, and retest a function. In addition,
you explored how to organize unit tests and how to debug discovered
errors.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module 4: Designing Functions
Module Introduction: Designing Functions
Watch: Motivating the Implementation Process
Watch: Implementing with Function Stubs
Activity: Implement with Function Stubs
Watch: Implementing with Pseudocode
Implementing with Pseudocode
Activity: Implement with Pseudocode
Watch: Testing with Function Stubs
Watch: Implementing Backwards
Activity: Implement Backwards
Watch: Implementing with Helper Functions
Activity: Implement with Helper Functions
Tool: Fundamental Python Concepts 4
Project Submission for Designing Functions
Module Wrap-up: Designing Functions
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Introduction: Designing Functions
You've seen the basics of how to write a function
definition with the correct syntax. You've also explored
how to call and specify a function. In this module, we'll
put all of this together to implement a function based on
an English description of what it should do. We'll look at
the role of function stubs, how to use pseudocode to
design a function, implementing backwards, and
implementing with helper functions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Motivating the Implementation Process
You've examined how functions work, including writing specifications and
running tests. Now it's time to move to the hardest part: implementing a
function. Implementing a function involves writing code from an English
description of what the function should do.
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
understand the types of problems that you might run into. The problems
that you run into in implementing a function we like to separate into two
categories: syntax errors and conceptual errors. Syntax errors are when
Python can't understand you.
Let's look at this module called errors.py, and you'll notice that I have a
function definition, and then I have some other Python code at the bottom
of the script. There are actually two syntax errors here. To see what we
mean, let's start up Python and let's "import errors". You'll notice that it
says, "SyntaxError: invalid syntax", and that's because I'm missing
something important here. What I've forgotten is that every single
function definition must have a colon after the header. I'm going to save
this. Now I'm going to try to import it one more time. Let's "import errors".
And now you'll notice that once again, we have another syntax error. It
tells me, well, it looks like there's something wrong with line 21. There's
actually nothing wrong with line 21. The reason why it's confused is I
forgot to close the parentheses on the previous line. And when you do
that, that confuses Python and it's not actually able to find that error until
the next line. Let's save it. And now I can actually import this module
properly. Syntax errors are nice. I could essentially do what I just showed
you, which is we keep trying to import it, and as long as I have these
types of errors, I go into the file and modify it and fix them. That's very
different than conceptual errors. In conceptual errors, Python does what
you say but not what you meant, right? So an example of this might be
you slice a string a little too early and you cut off a character that you
didn't expect, or maybe you used the slightly wrong argument in a
function. For example, in this function call, maybe I didn't mean to use 3;
I meant to use 4 instead. Well, it's not going to crash there, but it's still
going to be wrong. These are the harder types of errors to find, and these
are the things that we're going to create strategies to try to avoid.
In this video, Professor White shows you the strategy that you'll be using
to implement functions.
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
of the definition of last_name_first(), and all we have is the specification;
we haven't actually done anything with the body yet. However, what we
have done is we've gone ahead and made the unit test script for this
particular function, right? Here are each of my test cases, which take an
input and give me an expected output. The reason why I can do that is
we've taught you that all you need is the specification to write test cases.
You don't need to be looking at the function definition. And the idea here
is that by having the test first, that can help drive forward what it is that
we should be doing while we're writing the function.
Now, you don't have to have a full unit test; if you wanted to, you could
just do everything by hand, right? Starting up Python, importing the
module, and calling the module on the various inputs. Of course, in this
case, right now, it's not going to do anything because I haven't actually
implemented the function. So let's talk about how we would make forward
progress. The next idea is you want to take small steps. We want you to
do as little as you can at a time and to make use of what are known as
placeholders. We'll talk a little bit more about placeholders in a later
video. But the idea is that once you finish one step, we want you to test it
immediately.
Let's see this in practice here. What we're going to do is we're going to
search for the first space. So inside of the string n, I'm going to look for
the position of the first space. And I'm going to assign that to the variable
"end_first". What I'd like to do is I'd like to be able to test this right away.
OK; so fine. We're going to quit, restart Python, "import name", and we'll
call the function. What you notice is that, oh, I don't actually see what's
going on here. And that's because I haven't returned anything, right? I
could only test these fruitful functions that return a value. I haven't
finished the function; I'm not ready to return anything. Oh, but I can return
what I've done so far. So let's just "return end_first". Let's save. Let's quit.
Restart Python, "import name", last_name_first(), and ha! By running the
function and testing it, I can see right away that "end_first" is what I
expect. And now I can move on to the next line. So this is the idea, right?
We finish a step, we test it immediately using various techniques like the
one that I just showed you, and we do not move on to the next step until
we're done. All of the various strategies that we're going to teach you
throughout this course are really just variations and more advanced
versions of this idea.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Implementing with Function Stubs
In the Testing First strategy, you saw the idea of step-by-step testing on a
function. But how is it possible to test a function while still working on it?
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
read that and it stored the specification.
So, how do we get around this? Well, in general, it's a bad idea to start a
function without the specification anyway because you need that
specification to get started. But there is another solution that you could
do, and that is to type the word "pass". Well, what does "pass" mean?
"Pass" just means do nothing. It's a placeholder when I don't want to do
anything but I still need to have a function body there. If we save this, and
now we try to import this module, we don't get an error. In fact, we can
now call the function last_name_first() inside of this module with no
problems whatsoever. "Pass" is a very powerful tool and it's one of these
things that we're going to use to sort of give us placeholders to
essentially put in a note that, "Hey, I need to do some work here but I'm
not ready to do it." In general, the best thing to do is to combine the two.
So, here we have our original definition which had the specification. I
don't actually have to put "pass" here; it's not going to crash. But still, why
don't we just put "pass" here? Now what this is going to be is it's going to
be a note to myself that I haven't actually finished the function and I'm not
going to remove it until I'm done.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Implement with Function Stubs
In the Codio project below, navigate to the page Overview of Exercise
1. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 1, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Implementing with Pseudocode
Conceptual errors are errors that, if you are not careful, can be
introduced early in the implementation process.
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
to take each of these English statements and turn it into a Python
command.
Let's try the first one right away. So if I want to find the space between
two names, there's a function called find_str() inside of the IntroCS
module. find_str() takes the string n, and I tell it I want to search for a
space. It's going to give me the location of that space inside of the string
n and it's going to give that to me as a value. Now I don't want to forget
that value, so what I've done is I've assigned it to a variable. And this is
roughly what we would do when we're trying to turn our pseudocode into
actual Python code. Let's do it a little more step by step for the next
example. I want to get the first name. Well, remember what I'm doing is
I'm working with this string n. Where does the first name start? Well, it
starts at position 0 and it goes all the way up to the first space. Well, I
remember where the first space is; it is currently stored inside of the
variable "end_first". That's going to give me an expression for the first
name, but expressions by themselves can't be evaluated; they have to be
part of a command. In this case, what I want to do is I want to take this
expression and assign it to a variable. So that's what I do, and now I've
assigned it to the variable "first". When implementing this definition, we
just keep following this example, taking each of these comments, turning
them into Python code, until we're finished.
Video Transcript
Here, we have four lines of pseudocode telling us all the steps that we
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
want to do. Well, there's no reason we couldn't have written it all as one
line of pseudocode. Here I have an English statement as a comment that
says, "Hey, I want to return the last name, plus a comma, plus a first
name." But if I could do all of this in one line, why am I trying to
implement a function called last_name_first() in the first place; right? So
obviously this is not something that's easy to do in Python, where those
original four statements that we had before all are very easy to do in
Python. Well, OK; so how can I tell? How do I know what it is that's OK
as pseudocode and what's not? Well, that depends upon the types that
are involved. Different types have different operations, and one of the
things that you need to do before you start coding is memorizing the
important operations that are available for that type and use those as
building blocks. In this case, let's talk about strings because that's what
we have here; a function that's working with strings.
So let's think about the building blocks that we have. Well, one of the
things that we can do is we can take a string and we can slice it up into
little pieces. So slicing. And in fact, one of the things you'll know right
away is, even though I didn't explicitly say it, these two lines of
pseudocode really are about slicing. In fact, it might have even been
better for me to explicitly note to myself that what I'm doing in this case is
slicing out the first and the last name. Another thing that I can do with
strings is I can glue them together. And again, if we look at the definition
— well, we'll just say "put" instead of — we'll say "glue" here because
that's what's really happening in the last line of the pseudocode. So we
have slicing and we have gluing. OK; is that all we can do? Well, no; we
actually have a lot of things at our disposal, particularly if we look at the
module IntroCS.
So here if we pull up the module IntroCS, we'll notice that we have a lot
of things that we can do with strings. And in fact, our Table of Contents
actually organizes them in nice ways together. I can do things like
change the uppercase and lowercase parts of a string. I can search in a
string. I can count the number of times that a character is with a string. I
can determine if a text ends with or begins with a certain part of a string. I
can look for the location of an element in a string. So all sorts of things
that I can do with searching. I can also pad a string. I can do things like
center a string, or left-justify it or right-justify it. I can even strip spaces
away from it. Finally, in addition to all of these functions, one of the things
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
that we can do is we can often cast from strings to a different type. So I
might have a string like 12, and then I can do something like convert it
from a string to an integer. All of these things are basic building blocks.
So when you're writing your pseudocode, what you want to be doing is
thinking what are the things that my building blocks can do? The slicing,
the gluing; all of the functions that are available to me in IntroCS, and
write those as part of your outline. And that's what's going to make it easy
to replace it with actual code.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Implementing with Pseudocode
,PSOHPHQWLQJZLWK3VHXGRFRGHFRGLQJH[HUFLVH
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Implement with Pseudocode
This activity is optional and will not be included in your course grade.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Testing with Function Stubs
You have now worked with function stubs and seen how they help you
verify that each step works the way you intended it.
In this video, Professor White delves deeper into function stubs, showing
you different ways to alternate programming with testing.
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
"Forward", the variable is created, and you'll notice it is the number 6.
Well, if we look at the argument, yes, 6 is the position of the space.
That's what I expect the answer to be. Great. So we go on to the next
line, hitting "Forward". And now we're looking at the first name, and yes,
that's what I expect; it's my entire name, it hasn't been cut off, and there
aren't any additional spaces. Now we've reached the end of the function;
if we were to hit "Forward", the call frame would be erased and we're
done. But that's OK because all we're doing is looking at how much that
we've implemented so far, and since there haven't been any surprises,
we can somewhat be assured that we're on the right track. Now, you
don't always have the Python Tutor available. The Python Tutor isn't fully
featured; it doesn't implement all of Python, and so sometimes you have
to test directly in Python itself. Well, in that case, what you could do is
you could work with print statements, just like we've shown you how to do
with debugging, right? In each case here I had an assignment statement,
so a natural thing that I want to be doing is looking at the variable after
the assignment statement. So here I've just added some sort of print
statements, and now we can run this in Python. We'll do "python". We'll
import the stub definition. We're going to call this stub definition
"last_name_first" on my name as a test. And, you know, it's not finished,
but right away you can see that, oh, I'm getting pretty much what I
expect. I'm seeing the 6. I'm seeing my first name being done correctly.
So I'm happy. That's one way to do things. Though, remember, if you
approach it this way, you do have to comment out your print statements
when done because they are not part of the specification. An alternative
approach is, OK, instead of doing print statements, why don't we just
return a value? Well, we're not done yet; we don't know what to return,
but what we could do is we could just return the last thing that we've
seen. So here we have — I've implemented "end_first". Hopefully, I
tested "end_first" before moving on to the variable "first". Now I want to
be testing that "first" is working correctly. So I'm going to return it. To test
it out, I'll just import the module. And now I'm going to call it on my test
case, "last_name_first". And the answer is what I expect. Notice an
advantage of doing it this way. Because I've returned it, it's returned it in
the string format, and I can see exactly where the quotes are. So I can be
sure that there's no extra spaces here; something that I wasn't exactly
sure when I was doing the print statements. I could call this on other
names as well, like, say, Abe Lincoln. I can see that the first name is
working correctly. Doing a couple of these tests, I can be sure that I've
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
done the first name correctly, and now I can move on to the next line of
code.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Implementing Backwards
When preparing to implement a function, deciding where to start can be a
daunting task.
In this video, Professor White explains how working in reverse order can
help you begin function implementation.
Video Transcript
When implementing a function, the first step is always the hardest. Many
times we give students projects to work on, and they come to us for help
saying, "Well, we don't really know where to start." And that's true even
when we've taught them techniques like pseudocode. Sometimes the first
step just really isn't obvious. Well, there are techniques that we can use
to get around those blocks. One of them is, why don't we think about
working in reverse? I mean, when you read a specification, it tells you
what it is that it wants you to return. So you know where you need to be
in the end; your final answer. Well, what if you started with that final
answer and worked backwards to figure out how to get there from what
you're given? Let's try to make this a little more concrete with a specific
example. Here I have a function called middle(). It's a function we haven't
seen before, but it's relatively simple. All it does is it takes some text and
it gives me the middle portion of this.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
So, now how do I go about actually implementing this function? You'll
notice that in my function stub it's not a completely empty body; I do have
a return statement here. This return statement returns a variable result.
Now, there is no variable result. If I were actually to import this module
and try to call this function, I would get an error, saying it doesn't know
what "result" is. What I'm doing here is I'm using this variable as a
placeholder to tell me what should I be doing in a previous line. OK; well,
what should I be doing? The purpose of middle() is to give me the middle
third of text. So one of the things I should be doing is cutting out the
middle third. Well, let's do that as an assignment statement. "result" is I'm
going to take the text, and now I'm going to cut out the middle third.
Where is the middle third? I don't know, but let's make some placeholder
variables, like "start" and "end". I don't know what "start" and "end" are
yet; I will come back to those later. But they're nice little placeholders for
me to know, "OK, this is what I should be doing; I should be cutting the
text."
Next, I'm going to define the "end". Well, this is the end of the middle
third. Reading the specification, that's two times the size of the string
divided by 3, rounded down, and remember that for integers the double
slash rounds things down when I define them. So that's just reading the
specification itself. And now for "start", it's the size of the string divided by
3, rounded down. Everything's looking good so far, but, however, I still
have a variable that I don't know what it is; this "size" variable. But the
size is just the length of the string. And now this length has a variable in
at "text", but "text" is a variable that I know. It's the variable that I was
given to start with, namely my parameter. So now, since I don't have any
more undefined variables, probably my function is done. Well, let's test it.
Let's "import mid2". I'm going to call it on all of the inputs in my testing
plan. 'abc', looking good. 'abcd', looking good. 'abcde', and now 'abcdef'.
So, I can be pretty confident that my function is actually correct, and that
was not a bad way to implement it. However, there was a drawback,
right? Notice that we couldn't actually test this function until we were
done with it, and that's because it would've crashed if any of these
variables were not defined. And that kind of goes against our rule of
interleaving, programming, and testing. So, this is not necessarily the
most ideal technique. But with that said, it is certainly acceptable if you
have no idea what to do at all. We're just saying that you should use this
technique sparingly.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Implement Backwards
In the Codio project below, navigate to the page Overview of Exercise
3. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 3, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Implementing with Helper Functions
The one drawback of the backwards approach is not being able to
intersperse testing and programming. However, this is possible through
another technique called top-down design.
Video Transcript
That's great, but this is a new function; it's not the original function that
we were trying to solve. But that's OK; we can use this new function to
help us solve the first. So if we want to find the first name, we just call the
function first_name() on our parameter n. And now we can see how it is
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
that we design lazily. If I want to find the last name, I'm just going to
make a new function called last_name() and call it. And I don't exactly
know how to implement the last_name(), but I do have a specification for
it. So I can create a function stub, and now I can move on. And the nice
thing here is that I have no undefined variables. So that means that I can
actually test this function, what little I've done so far, without actually
having it crash on me. In fact, I can even go a little further; I can start
saying. "Hey, let's return not just the first name or the last name, but let's
go ahead and glue them together in the final return statement so that I
can look at both of these values in my testing."
OK; let's try this. Let's start Python. Let's "import name", and now let's
call last_name_first() on one of my typical test cases, using my name.
And notice it doesn't crash, and it's because everything's working so far. I
have the first name, I have a comma. I don't see the last name, but that's
because I'm not ready to actually implement the function last_name();
correct. But I already noticed that I have one error right away; this is why
we always test while we're writing. Notice that there's no space after the
comma. So I'm going to want to go ahead and make sure that I add a
space there and then test this once again. What we're talking about here
is what is known as top-down design. When you're working on a function,
you're given the specification for that function. And you can't change this
specification at all; changing this specification would break your team.
But what you can do is you can split up this specification into little
problems; things like finding the first name, or finding the last name. And
each one of those little problems becomes naturally its own function. And
now you can define the specification for each function and implement it
on your own. As a word of warning, don't go overboard on this technique,
right? We don't want you writing functions where the actual body of the
function was a single line. Because if you could write it in one line, why
do you really need to have that function in the first place? The general
rule of thumb for these types of things is, do it if your code is getting
really long. I personally have a one-page rule. If the code is going more
than one page, then what I should do is I should take that code and break
it up into different functions. Another reason why you might do this
technique is if you find yourself repeating yourself a lot. If you see the
same code over and over again, then take that code and replace it with a
new function call of your own.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
In this video, Professor White shows some tips to keep in mind when
using top-down design.
Video Transcript
As we saw with the idea of top-down design, we're given a function that
we want to implement, and we do that by breaking it up into smaller
functions that we implement as well. So, once again, we're looking at the
definition for the function last_name_first(), and we've broken this up into
two smaller functions called first_name() and last_name(). And now what
we've done is we've separated those, and we're going to implement
those on their own. When you do this approach, you also want to make
sure that you test these new functions in addition to the first one. So here
I have a unit test script for testing the function last_name_first().
Finally, down at the bottom, when I'm writing my script code, remember,
every single unit test, it's not enough to have a test procedure; we also
have to call the test procedure to make sure that the tests are invoked.
You'll notice that we've called the three test procedures. And the order
here is particularly important. I've called first_name() first because that
appeared first in the body of our definition for last_name_first(). Here, it
appears first in the body of the definition, so I need to make sure that I
test it before I go any further. Then I can test the function last_name().
And then finally, when I'm sure that all of those are working, now I can
test the function together. Let's try this as a test script. We'll do "python
test_name.py". And right away, we see first_name() is working correctly.
We passed all the tests. However, last_name() is not working correctly,
so it's not even going to go ahead and start testing last_name_first(); it's
going to expect us to fix this function before we move forward.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Implement with Helper Functions
In the Codio project below, navigate to the page Overview of Exercise
4. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 4, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Tool: Fundamental Python Concepts 4
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Project Submission for Designing Functions
You have now completed all Codio exercises for the course module
Designing Functions. To submit the project, go to the page Complete
the Course Module in the Codio project and follow the instructions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Wrap-up: Designing Functions
In this module, you explored how to implement a function based on an
English description of what it should do. You also took a look at the role
of function stubs and practiced using pseudocode to design a function.
You also explored how to implement a function backwards and
implement with helper functions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module 5: Enforcing Specifications
Module Introduction: Enforcing Specifications
Watch: Reading Error Messages
Watch: Interpreting Error Messages
Activity: Interpret Error Messages
Watch: Asserting Preconditions
Activity: Assert Preconditions
Watch: Designing Error Messages
Activity: Design Error Messages
Watch: Identifying Trade-offs
Watch: Enforcing with Helper Functions
Activity: Enforce with Helper Functions
Tool: Fundamental Python Concepts 5
Project Submission for Enforcing Specifications
Module Wrap-up: Enforcing Specifications
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Introduction: Enforcing Specifications
In this module, Professor White explains in depth about
error messages, how to read them, and how to determine
responsibility. You'll also look at how to use assert
statements to help with identifying errors, how to design
error messages, and enforcing preconditions with helper
functions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Reading Error Messages
When code crashes, the information that enables you to find and fix the
problem is in the error message.
In this video, Professor White shows how to look at the trace of an error
message and find the source of the problem.
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
the Python Tutor. Here we're looking at the script once again, and we've
reached the point where we're at line 28, and we're one line away from
the error. If I were to step forward, you would notice that I would run into
the error. What's going on here is, I have called function_1(), function_1()
has called function_2(), and function_3() is now being executed. It's
what's going to run into an error. All of these call frames right here are
sitting on top of the call stack. The printout in the error message is what
is known as the stack trace. We may not have the Python Tutor; we may
not be able to visualize all of these things. But it gives us enough printout
so we know exactly what is going on in the entire call stack. So in
particular, you'll notice that it tells us we ran into an error on line 36. Now,
that error is inside of the definition of function_3(). We pull up the
definition, side by side. We can see line 36 inside of function_3(). Now,
how did we get to function_3()? Well, we had a call to function_3(), and
that occurred in line 26 of function_2(). And here we are. That
function_2() was called on line 16 of function_1(). And here we go. And
then finally, function_1() was called on line 41. Now you notice that we
have two calls to function_1(); one on line 40 and one on line 41. The
important thing about the stack trace is it's telling me exactly which one of
the two is it that called the problem. Because that's what can happen,
right? I might have a function, I might be calling it multiple times in
different places. I need to know exactly which function call is it that
caused the problem. Together, all of these things together help me
visualize the call stack that I would look at if I were, say, looking at things
in the Python Tutor. And that's what I'm going to use to help me to figure
out what's going on.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Interpreting Error Messages
When a function crashes and multiple developers are involved, it may not
be obvious who is responsible for fixing the problem.
Video Transcript
Let's remind ourselves that when we're reading an error message, the
purpose is to help us understand where the responsibility lies. When
code crashes around a function, remember that there are two people
involved: There's the person who defined the function, and the person
who called the function. And we have to figure out whose fault it is and
who must fix it. So, let's look at this example here where I just have two
functions. I have function_1(), which calls function_2(); function_2(),
which does some computation. At the bottom of this script, I have a call
to function_1(). Let's run this script, and we're going to see that this
crashes. And we're going to get a complete stack trace here, right,
because at the bottom I call function_1(), which calls function_2(), and
then function_2() does some computation. The important thing to
understand is the actual error can be on any of the three lines that are
presented here, and that's the reason why we have to have the entire
stack trace when we're figuring out where the responsibility lies. So how
do we approach this? Well, the idea is we start from the top. In this case,
we're looking at the top of the stack trace, which is line 32. Ironically,
that's the bottom of the file, right; it's the call to function_1(). And what
we're going to do is we're going to look at this function call, and we're
also going to look at the arguments. In this case, we see right away that
the arguments are 1 and 0. Sometimes when we see the arguments,
they're variables, so we're going to have to have some form of
visualization, either using print statements or the Python Tutor, but the
key thing is we need to know exactly what the arguments were.
Once we know what the arguments are, we verify that the precondition is
met. If it's met, then there can't be anything wrong with that line, because
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
that's the point of a precondition. As long as the precondition is satisfied,
the function is supposed to work. So here I'm looking at 1 and 0; they are
both numbers. And if we look at the precondition for the specification for
function 1(), that's what it says. I haven't done anything wrong, so there
can't be anything wrong with line 32. OK; so function_1() calls
function_2(), and this is line 18, as I can see, and that's the next line I
need to be looking at. Now you'll notice that it's x and y. Well, I can
remember that, well, x and y got whatever x and y was before, namely 1
and 0, so I don't need to print out anything or visualize anything; I can be
pretty sure that these are 1 and 0 once again. OK; well, they're numbers,
and that's what the precondition of function_2() says. It says as long as
these are numbers, they have to work. So, there cannot be anything
wrong with line 18 either. Well, there's only one line left; it's line 28. And
so therefore, line 28 must be at fault. And indeed that's correct, right,
because the precondition for function_2() promised that it works
whenever x and y are numbers, even when y is 0. But you can't do this
division this way whenever you have y is equal to 0.
The important thing to understand is, is that this highly depends not on
the code, but on the specifications themselves. Let's look at a slightly
different script. This script is almost identical to the first one. And in fact, if
we were to run this script, you will notice that we get exactly the same
error message as the first one. There's the call on line 32, there's a call
on line 18 in function_1(), and then there's line 28. The only difference in
this case is that the precondition for function_2() is different. It says that y
doesn't just have to be a number; it also has to be greater than 0. So
actually, the error is no longer on line 28. Now, the error is on line 18, and
this is why specifications are so important. Line 18, I have a call to
function_2(). Inside of x is the value 1, inside of y is the value 0; that
violates the precondition for function_2(), and so now this line of code is
at fault and this is what must be fixed. Finally, let's look at a third file.
Once again, it looks exactly like the previous ones we've looked at. If we
were to run this script, it will give us exactly the same error message
once again. The only thing that has changed is the specification for both
function_1() and function_2(). Now for both functions, I'm specifying that
not only must y be a number, but it must be greater than 0. So now, the
error isn't on line 18, it isn't on line 28, but it's actually on line 32. And this
is why we need to combine looking at the stack trace in our error
message with the specifications to figure out what the correct approach
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
is.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Interpret Error Messages
In the Codio project below, navigate to the page Overview of Exercise
1. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 1, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Asserting Preconditions
You've seen how the source of an error can be found by examining the
stack trace and comparing it to the specification.
In this first video, Professor White shows you how to simplify reading the
stack trace, using a new type of command called an assert statement.
Video Transcript
So we've talked about how while we're reading an error message, we're
using this to assign responsibility and figure out exactly which line of
code to fix. But to do this, we have to go through the entire stack trace
and compare it to the specification at each step. It's going to be a little
tedious, so we'd like to know, how can we make this easier? Well, what if
we could control the error messages? The idea is that what we want to
do is we want to write exactly what the responsibility was directly into the
error, and that way we would only need to look at the error message and
not the full stack trace. The way we're going to do this is with assert
statements, which are a new type of command in Python. Let's start
Python. To write an assert statement, it looks like a return statement. I
write the word "assert", and then after "assert", I'm going to write an
expression. But the expression that I write needs to be a Boolean
expression; something like "assert True". Now I'm going to hit Return.
You'll notice that nothing happened, and that's the way "assert" works. If
the Boolean expression is true, nothing happens. On the other hand, if I
were to write "assert False", you'll notice that it crashed, and it gave me
an error, and it told me that the error was an assertion error. Now, there's
no error message here, but I could actually create one if I wanted to. And
the way that I do that is I'd write my assert statement, and then I write a
comma, and then after that comma, I'm going to write a string with my
message. I'll say, "You lose". Now I hit Return, and now you'll notice that
I have an error message, and it tells me directly in there that, OK, I have
a problem. Now, this may seem sort of cute, but why is this useful? OK;
well, let's try to do something a little more interesting. Let's suppose that I
have a variable x, which I'm going to assign to a value of 2. And what I
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
want to do is I want to assert that x is greater than 1. And now what I
want to have is an error message, and the error message that you give it
needs to be, well, maybe what I want to assert, now that x is greater than
1, didn't happen. So I want to assert that "x is out of range", OK? I'm
going to hit Return. Nothing happened. That's because x is indeed
greater than 1; no problem. But if I were to change this to assert x is
greater than 3 and hit Return, you notice that I get an error message, and
it tells me that "x is out of range". That's what my problem is. If you
notice, this looks kind of familiar, right? What I'm doing is I'm using assert
statements to verify that a fact is true. If the fact is true, nothing happens.
If the fact is not true, it's going to crash, and it's going to give me an error
message. This is really similar to the function assert_equals() that we've
been using in all of our unit tests. The thing that's different is, is that this
is a little more versatile, right? And this is something that I don't have to
just put into a unit test but I can actually write in my code, and it can help
me to understand the error messages a lot better.
Video Transcript
So now that we've seen how to use assert statements, let's use this to
help us assign responsibility when we actually have a crash. What we're
going to do is what is called enforcing preconditions. And the idea is
inside of our function body, what we're going to do is we're going to have
assert statements for all of our preconditions. The idea is if any
precondition is violated, it's going to crash immediately, and the message
is going to indicate what the problem is. To see this in practice, let's look
at our function last_name_first(). We've actually decided to make things a
little simpler here with our precondition and only allow a single space.
We're not going to handle the multiple-space case. As you can see, this
keeps our precondition fairly simple; we're only requiring that n is a string
and it has a single space in it. This function is no longer a stub; it has
been fully implemented, and here is the body of the function.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
and verifying that the precondition is satisfied; namely, I'm asserting that
the type is a string, and I'm also asserting that there's exactly one space
inside of it. OK; so let's see this in practice, and let's do a function call
that violates the precondition. I'm going to give it not a string, but instead
a number. I've written this in script format with the definition at the top
and the call at the bottom, and so let's run this script. And you'll notice
that we get an error message. And the error message tells me right away
that I have a precondition violation.
Now, OK, so where is the line with the error? Well, line 26 is the line that
generated the error message, so that can't actually be the problem. The
problem line is actually the line before it, which does the function call.
And so now this is nice; when I see something that is a precondition
violation, I don't have to look at the entire stack trace. I don't look at the
bottom of the stack trace, but I look at the one line just above the bottom
of the stack trace, and that is where the error message is. And this
makes it a lot easier to figure out what's going on. That's one advantage
of enforcing preconditions. Another advantage of enforcing preconditions
is it makes undocumented behavior completely impossible. Let's look at
another example. Here is a function called to_centigrade() that we've
actually seen before. I'm going to remove the assert statement that we
have here in the definition, I'm going to start up Python, and I'm going to
import this module and call to_centigrade(). Remember, this function
converts things in centigrade to Fahrenheit. There we go; 100 degrees
Fahrenheit is 37 degrees Celsius. One of the problems that we see with
this is, well, I could call this with an integer, and I'm still going to get an
answer. However, technically what I just did is wrong. Because the
precondition clearly says that x has to be a float, and I called it without a
float but it didn't crash.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
So the nice thing about assert statements is that we can shut it down now
before it causes any more problems.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Assert Preconditions
In the Codio project below, navigate to the page Overview of Exercise
2. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 2, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Designing Error Messages
At this point, you've worked on ways to detect where code has broken
and identify responsibility. The next step is writing effective error
messages.
In this first video, Professor White shows how to design error messages
that are helpful to users.
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
We're going to import this module, which is called name1. I'm going to
call name1 with last_name_first(), and now we're going to give it the
argument of 12. And now you'll notice that we got a much, much more
helpful error message telling us that, wait, 12 is not a string, so that
argument is not going to work. When we did this, notice that we used the
function str(). That's because n is not actually a string, and if I wanted to
glue it to another string to produce an error message, I have to turn it into
a string first.
You'll notice that I don't actually do that in the later error messages; in the
later error messages I'm just gluing n together directly to the string. Why
is that? Well, if you passed this particular assert statement, I know that
you're already a string, so I don't actually have to convert you to a string,
and now I can just glue n directly to the error message itself. There's
another thing that's going on with these error messages, which is,
remember that we only had one assert statement here in the previous
example; namely just a count that there's exactly one space there. This
time we've actually split it up into two different assert statements, and
that's because I wanted to display two different error messages. To see
this in practice, let's suppose we call this function. We're going to give it a
string, so that part is OK, but while we're going to violate the precondition
by having no spaces inside of it at all. Now you'll notice that we get an
error message saying that, "Hey, this is missing a space." And that's
because this particular search statement failed. On the other hand, if I
were to give it something with, say, my middle name — so now I have too
many spaces — now I get a different error message. So before, in the
first file, you notice that we only needed two assert statements in order to
do the job, but sometimes we like to split up our assert statements
because it allows us to have more helpful and useful error messages.
In this video, Professor White introduces a new function that helps error
messages provide users as much information as possible.
Video Transcript
We've seen that we've been designing our error messages for our assert
statements that sometimes we actually have some fairly complex string
expressions. In this case, where we're asserting that the type of n had to
be a string, you'll notice that we wanted n to be part of the error
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
message, but to do that, we had to first make sure that n was indeed a
string and then we could glue it to the rest of the error message. This
creates another interesting question. Let's look at the following example.
I'm going to create an error message, and what I'm going to do is I'm
going to take a variable "var", convert it to a string, and then say that
particular variable is invalid. So here's my error message: "2 is invalid".
Now, the question is, just looking at this error message, do you know
what was inside of the variable "var"? Was it an int, like, say, the number
2? Or, well, there's another number type, so was it a float; namely 2.0?
Or is it actually a string? Remember, I could have '2' inside of quotes. In
fact, you may look at these three examples and you just may say it's
impossible to tell. Well, it's one of those four, so think about it, pause the
video, and then we'll tell you the answer.
OK; have you thought about it? The answer is, it is impossible to tell. In
this particular case, "var" was actually the string '2'. And why is that?
Because when you take the string function and you apply it to something
that is already a string, like '2' inside of quotes, you're just going to get
the same answer back. And so that's the reason why if I have "var" as the
string '2', and I take that and convert that to a string, it's just going to look
like that. On the other hand, if I take "var" and set it equal to the number
2, I'm going to get the same message, and that's because it's taking that
2 and it's turning it into the string with quotes. This is slightly unsettling.
When we have error messages, we want the error messages to give us
as much information as possible. We may not be completely aware of
what are stored inside all of our variables at each step, so we'd like to
have that information in the error message. To do that, we're going to
introduce another function. This function is called repr(), or I like to call it
"repper." It stands for "representation." And just like the string function, I
can use it to turn expressions into strings. So if I call repr() on the number
2, it's going to give me the string '2'. However, it does something slightly
different on strings. If I'm going to call repr() on the string '2', you'll notice
that this time we've still got a string — actually, this string is going to
begin and end with double quotes — but now what it's done is it's taken
the single quotes and it's put them inside of the string as part of the
string.
Now why is this useful? Let's think about using repr() in our error
messages. So I'm going to make an error message using repr() instead
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
of str(). I'm going to glue it together to "is invalid". And now we'll look at
the message. OK; this time, you'll see it was this before, and that's
because "var" is currently an integer. But if I make "var" equal to the
string '2' and now redo the message, now you'll see that it's actually put
the quotes around '2' inside of the error message. So I can see that in
this case, the variable must have been a string, and in this case the
variable must have been an integer. So I can get full information about
what's going on from the error message. That's the reason why, when we
look at our function last_name_first(), this is actually how we want to
design our assert statements. Instead of using str() for error messages,
we want to use repr(). And that way I'm going to get my quotes around it
if it's a string; and if it's missing quotes, it can't be a string. Let's see this
in practice. Let's "import name2", let's call last_name_first(). We'll do it
with 12, which is a number. Again, you'll see that 12 is not a string. It was
the same as if we had str(). But now let's actually see the more
interesting feature. Let's call this on my name. And I'm going to call it
without a space. Now you notice that I get an error message and it tells
me, "Aha, 'Walker' is missing a space." And the nice thing about that is
that I'm looking at the entire string and I can see where the string begins
and ends because the quotes were actually included in this error
message. To see why this is important, let's add a space after this. In
fact, let's add two spaces. Once again, I'm getting an error message, but
now I have an error message because I have too many spaces. And the
only way that I can actually see those spaces is because the quotes are
here. And I can see, "Wait; there's actually some mysterious extra
spaces before this quote." If we had done this in a previous version of the
module, which only used the str() function, it would be unclear what's
going on. If I do last_name_first() again of my name with two spaces,
now you'll get an error message, but I can't immediately tell what's going
on because were these extra spaces just an artifact of the error
message, or are they actually part of the string involved? Whereas, in the
error message that I had previously, this is absolutely clear because I can
see the quotes.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Design Error Messages
In the Codio project below, navigate to the page Overview of Exercise
3. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 3, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Identifying Trade-offs
You've seen the benefits of enforcing preconditions with assert
statements. However, there can be some drawbacks.
Video Transcript
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
enforcement. It'll allow us to identify the biggest errors. But some of the
more obscure things or hard to check things will go unchecked and we'll
just catch them in the system some other way.
To see this example, here we have two assert statements. Now, we're
asserting that n is a string. But when it comes to the number of spaces,
all we're doing is asserting that there must be one space there. We're not
doing anything about the number of spaces or whether or not the spaces
are right next to each other. So let's see what type of behavior this
causes. We're going to import this particular module and we're going to
call last_name_first(). We give it, say, the obvious wrong argument, like a
number. It's going to tell us that that's not a string and it's going to give us
an error message. We'll get another obvious error where there's no
spaces inside of it. Now it's going to tell us that it's missing a space. On
the other hand, let's suppose we did something with multiple spaces, like,
say, three names. Notice that it didn't actually crash this time and it did
something; it assumed that these last two names were my entire last
name and only pulled out the first name. Now, this is a violation of the
specification, but it's my fault because I've called it with an argument that
violates the precondition. And I've just discovered that by getting a
mysteriously wrong answer. Now, so looking at this, you might say, "OK;
well, what things should I enforce? What things should I not enforce?"
Well, this is really a trade-off and this is just something that you build up
with experience. You'll discover that, OK, some things you'll know how to
do and some things you won't. And you should just right now, in the
beginning, starting out, check the things that are easy and avoid the
things that aren't.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Watch: Enforcing with Helper Functions
Earlier, you saw how top-down design is used to implement the body of a
function.
In this video, Professor White shows how this approach can be used with
helper functions to fully enforce a precondition.
Video Transcript
OK; well, since I'm calling it, that means I need to have a definition, and
here we've written out the definition along with a specification. We
haven't implemented it yet — we'll come back to that in a second — but
let's just look at the specification as it is. Remember that assert
statements need to have a Boolean expression. So that is the reason
why, if I'm going to have a helper function that checks whether or not a
precondition is violated, it must be a Boolean function; It must be
something that returns "true" or "false". OK; well, you'll also notice that
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
specification has a lot more information here. That's because, all right,
actually implementing this particular function is tricky. You theoretically
know enough computer science to be able to implement this function, but
it also requires you to be clever. So we have to break up all of these
things that we actually want to be true; namely that there's no space at
the beginning or end, the space is in the middle, and if there's more than
one space the spaces are adjacent. So let's see how we would do this
function.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Activity: Enforce with Helper Functions
In the Codio project below, navigate to the page Overview of Exercise
4. Complete all of the instructions for this exercise, checking your work as
you go. After you have completed the page Reflect on Exercise 4, you
are done. Do not move on to the next exercise until you are directed.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Tool: Fundamental Python Concepts 5
In this module, we explored error messages, how to read them, and how
to determine responsibility. This reference tool contains key Python
concepts we've covered in this module. Check out pages six through
seven to help you review things such as how to approach error
messages and how to enforce preconditions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Project Submission for Enforcing Specifications
You have now completed all Codio exercises for the course module
Enforcing Specifications. To submit the project, go to the page
Complete the Course Module in the Codio project and follow the
instructions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Module Wrap-up: Enforcing Specifications
In this module, you explored error messages, how to read them, and how
to determine responsibility. You also examined how to use assert
statements to help with identifying errors, how to design error messages,
and how you can enforce preconditions with helper functions.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Course Project
Programming with Functions
Read: Thank You and Farewell
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Programming with Functions
The Codio project below contains the instructions for the final course
project. Follow all instructions to complete the project and submit your
work.
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Read: Thank You and Farewell
Walker M. White
Senior Lecturer and Stephen H. Weiss Provost's Teaching Fellow
College of Computing and Information Science
Cornell University
Sincerely,
Walker M. White
© 2019 eCornell. All rights reserved. All other copyrights, trademarks, trade names, and logos are the sole property of their respective owners.
Glossary
Accumulator
An accumulator is a variable in a for-loop that stores information computed by the for-loop and will be still
available when the for-loop is complete. For example, in the code
total = 0
for x in range (5):
total = total + x
Application
An application is another name for a script.
Argument
An argument is an expression that occurs within the parentheses of a method call. The following call has two
arguments: x+y and y+w:
min(x+y, y+w)
As a general rule, the number of arguments should equal the number of parameters that are called, except
when using default arguments.
Assert statement
An assert statement is a statement in Python used to enforce an assertion. It has one of the following forms:
assert <bool>
assert <bool>, <message>
In both cases, Python evaluates the boolean expression. If the expression is True, nothing happens. If it is
False, Python will raise an exception with the (optional) provided message.
Assignment statement
An assignment statement is a statement with the following form:
<variable> = <expression>
If the variable does not yet exist, the statement creates the variable and stores the given value inside it. If the
variable does exist, then it replaces the old value with the one provided.
Basic type
A basic type is any type that has a corresponding literal. Basic types are fully integrated into Python (they
do not require an import statement to use) and are not mutable. In Python, the basic types are int, float,
bool, complex, and str.
Bool
A bool or boolean is a basic type whose values are True and False.
Bug
A bug is an error in a program.
Call frame
A call frame is an area of memory created whenever a function is called. The call frame contains the
following:
The call frame changes over time as the function executes, adding, changing, or deleting variables.
Call stack
A call stack is the area of memory containing all active call frames. The call frames are arranged top to
bottom with the most recent call on the bottom of the stack. A call frame may not be erased until all call
frames below it are erased.
Cast
A cast is an operation that converts a value from one type to another. For example, the cast float(5+6)
converts the value of the expression, which is 11, to float format.
A widening cast converts from less information to more information and can be done implicitly. A
narrowing cast must be done explicitly because it may lose information or may be illegal. See narrower type
for more information.
Command shell
The command shell is an OS-specific application that responds to text commands. On Windows, it is called
the PowerShell. On MacOS (and most Linux systems), it is called the Terminal.
Comment
A comment is a line of code that is ignored by Python and only serves as a note to the programmer. Most
comments start with a #. We sometimes refer to docstrings as comments. However, these are not actually
ignored because they are used by the help()function.
Conditional expression
A conditional expression is an expression with the following form:
To evaluate this expression, Python first evaluates the boolean expression <bool>. If the boolean is True,
Python uses the value of <expr1> as the value of the conditional expression. Otherwise, Python uses the
value of <expr2> as the value of the conditional expression.
Conditional statement
A conditional statement is a statement of the form
if <bool>:
<statements>
Python executes this statement as follows: If the boolean expression is True, if executes the statements
underneath. Otherwise, it skips over them.
if <bool>:
<statements>
else:
<statements>
This form is executed as follows: If the boolean expression is True, it executes the statements underneath
the if. Otherwise it executes the statements underneath the else. There are additional forms of conditional
statements that use the keyword elif.
<classname>(<arguments>)
For example, Point (1,2,3) is a constructor call for the class Point.
Debugging
Debugging is the act of searching for and removing bugs in a program.
Default argument
A default argument is an argument that is provided automatically if it is omitted in the function call. Default
arguments are specified by writing the associated parameter in the form of an assignment statement. For
example, the following function header has a default argument for y:
def foo(x,y=2):
In this case, the function call foo(1) is legal; the y parameter is given the default argument 2. In this
course, we are primarily interested in object and class folder.
Dict
A dict or dictionary is a mutable type that associates keys with values. It is similar to a sequence, except that
elements are accessed by the key, not the position. For example, in the dictionary
d = { ‘alpha’:1, ‘beta’:3 }
Docstring
A docstring is a string literal that begins and ends with three quotation marks. Document strings are used to
write specifications and are displayed by the help() command.
New-Line Character \n
Backslash Character \\
Double-Quote Character \”
Single-Quote Character \’
Evaluate
Python evaluates an expression by turning it into a value.
Float
A float is a basic type whose values are scientific numbers like 3.46E-4.
For-loop
A for-loop is a statement of the form
Python executes this statement as follows: It puts the first element of the iterable in the variable, and then
executes the statements indented underneath. It repeats this process as long as there are still elements
left in the iterable.
In a for-loop, the variable after the keyword for is called the loop-variable while the iterable is called the
loop-iterable or loop-sequence.
Fruitful function
A function that terminates by executing a return statement, giving an expression whose value is to be
returned. Fruitful functions (possibly) return a value other than None.
Function
A function is a set of instructions to be carried out via a function call. You can think of it like a recipe in a
cookbook. We often separate functions into fruitful functions and procedures.
Function body
The function body consists of the lines of code specifying what is to be done when the function is called. The
function body appears indented after the function header. We typically use the term function body to just
refer to the actual Python code and exclude the function specification.
<function-name>(<arguments>)
It is important to understand how a function call is executed. Its execution is performed in five steps:
If the function is a fruitful, it returns the value of the appropriate return statement. If it is a procedure, it
returns None.
Function definition
The function definition is the code that tells Python what to do when a function is called. It consists of the
function header and function body.
Function frame
A function frame is another term for a call frame.
Function header
The function header is the part of the definition that starts with the keyword def, followed by the function
name and the parameters delimited by parentheses. The function body is immediately indented underneath.
Function name
The function name is the name used to call a function. It appears in the function header, immediately
after the keyword def.
Function specification
The function specification defines what the function does. It is used to understand how to call the function. It
must be clear, precise, and thorough, and it should mention all parameters, saying what they are used for. It
should also include a precondition for each parameter.
In Python, function specifications are written with a docstring. They appear immediately after the function
header, indented with the function body.
Global space
Global space is the region of memory that stores global variables.
Global variable
A global variable is a variable that is defined outside of a function or class definition. Typically, both function
names and module names are global variables; these variables each contain the identifier of the folder that
stores the function or module content in the heap.
Heap
The heap is the region of memory that stores all folders. Anything that is not a basic type must be
represented as a folder in the heap.
Immutable
The adjective immutable means “incapable of being changed.” It is typically used to refer to an attribute or
a type.
Immutable attribute
An immutable attribute is a hidden attribute that has a getter but no setter. This implies that a user is not
allowed to alter the value of this attribute. It is an important part of encapsulation.
Immutable type
An immutable type is any type that is not mutable. All of the basic types are immutable.
implementation
An implementation is a collection of Python code for a function, module, or class that satisfies a specification.
This code may be changed at any time as long as it continues to satisfy the specification.
In the case of a function, the implementation is limited to the function body. In the case of a class, the
implementation includes the bodies of all methods as well as any hidden attributes or methods. The
implementation for a module is similar to that of a class.
It is used to access the global variables, functions, and classes defined within a module. When we import a
module, all module variables (and function and class names) are stored in a folder. Unless the from syntax is
used, all content must be accessed using a variable with the same name as the module.
import math
then we write math.pi to access the variable pi in this module. On the other hand, if we write
then all of the contents of the module math are dumped into global space. In that case, we only need to write
pi to access the same variable.
Instance
An instance of a class is an object. The terms instance and object are synonyms. We typically use the word
instance when we are referring to a collection of objects all of the same class.
Instantiate
The word instantiate means “to create an instance of.” Evaluation of a class expression C(...) instantiates
an instance of the class C.
Int
An int is a basic type whose values are integers.
Interactive shell
The interactive shell is a program that allows the user to type Python expressions and statements one at
a time, evaluating them or executing them after each step. It is not to be confused with the command shell,
which is a more general purpose tool. However, the interactive shell is often run within the command shell.
For a function, the interface is typically the specification and the function header. For a class, the interface is
typically class specification as well as the list of all unhidden methods and their specifications. The interface
for a module is similar to that of a class.
Is
The is operator works like == except that it compares folder names, not contents. The meaning of this
operator is can never be overloaded.
Iterable
An iterable type is the type of any value that may be used in a for-loop. Examples include lists, strings,
and dictionaries.
Keyword
A keyword is a special reserved word telling Python to do something. Examples include if, for, and def.
Keywords may not be used as variable names.
List
A list is a mutable type representing a sequence of values. Lists are represented as a comma-separated
sequence of expressions inside square brackets, like this
Literal
A literal is a way to represent a value in Python. Literals are used to write expressions with any of the basic
types. Here are several examples of literals:
int 354
float 3.56
complex 3.5j
bool True
str “Hello World!”
str ‘Hello World!’
Local variable
A local variable is a variable that is created within the body of a function or method.
Loop iterable
A loop iterable is the entity that produces values for a for-loop. Because most loop iterables are sequences,
this term is often used interchangeably with a loop sequence.
Loop sequence
A loop sequence is the entity that produces values for a for-loop. The name is chosen because most of the
time for-loops use a sequence. However, the term loop iterable is more accurate.
Loop variable
A loop variable is a variable that acquires the next element of the loop iterable through each iteration of a
for-loop. In some cases, the term loop variable is extended to include any variable that appears in the loop
condition of a while-loop.
Method
A method is a function that is contained within a class definition. Methods are able to access the attributes of
the class, in addition to the parameters and local variables that any function can access.
Method call
A method call is a function call for a method. It is similar to a function call except that it has the following
form:
<folder>.<method-name>(<arguments>)
where <folder> is an object in the case of an instance method and a class in the case of a class method.
A method call creates a call frame exactly like a function call except that the folder name of <folder> is
assigned to self.
Module
A module is a file containing global variables, function definitions, class definitions, and other Python code.
The file containing the module must be the same name as the module and must end in .py. A module is used
by either importing it or running it as a script.
Module specification
A module specification is a description of the purpose of a module and how to use it. Module specifications
are typically written using docstrings.
Mutable attribute
A mutable attribute is an attribute that can be freely changed. Mutable attributes need not be encapsulated.
But if the attribute is hidden, then it will need both a getter and a setter.
Mutable type
A mutable type is a type where the values are containers and it is possible to alter the contents of the
container. An int is not mutable because it does not contain anything; it is just a number. A string is a
container of characters but it cannot be changed. The types list and dictionary are examples of mutable
types, as are most user-defined classes.
Narrower type
A narrower type is a type for which Python will perform a cast from automatically. In other words, Python will
automatically cast from a narrower type to a wider type. This concept only applies to bool and the numeric
types. The progression below shows the progression from narrowest type to the widest type:
None
The value None actually represents the absence of a value. If Python attempts an attribute reference like
None.b or a method call like None.m(...), it will raise an exception.
Object
An object is an instance of a class. Each object for a class can access the attributes and methods defined in
and inherited by the class.
Object attribute
An object attribute is an attribute that is unique to a specific object and is therefore stored in the
object folder.
Object class
The object class is a special class built into Python named object. It is the superest class of all. Any class that
does not extend another class must extend the object class.
Object folder
The object folder is the folder in the heap that contains the attribute values unique to an object. We use this
term in place of the term object when we want to exclude any attributes or methods that might be inherited.
Object representation
An object representation is a string that can uniquely identify an object. It is retrieved with the repr function.
Parameter
A parameter is a variable that is declared within the header of a function or method.
Pass statement
A pass statement is a statement that tells Python to do nothing. It is used to fill the body of a function stub.
Procedure
A procedure is a function that has no explicit return statement. A function call on a procedure always
evaluates to None.
Return statement
A return statement is a statement that terminates the execution of the function body in which it occurs. If it is
followed by an expression, then the function call returns the value of that expression. Otherwise, the function
call returns None.
Scope
The scope of a variable is the set of statements in which it can be referenced. For a parameter, it is the
corresponding function body. For a local variable, it is from the initial assignment statement until the variable
is deleted, or the end of the function body.
Script
A script is a Python program that is meant to be run outside of interactive mode. In particular, scripts often
have the following line of code to prevent them from being imported:
if __name__ == “__main__”:
To run a script, type python <script> in the OS command shell. When a script is run, it will execute all of the
code indented under the conditional above.
Statement
A statement is a command of Python to do something. Python executes a statement.
Str
A str or string is a basic type whose values are text, or sequences of characters. String literals must be either
in double or single quotes.
Test-case
A test-case is a collection of inputs, together with an expected output, used to test a single run of a program.
It is often used in unit tests to test a function.
Testing
Testing is the act of analyzing a program, looking for bugs. Unlike debugging, it does not necessarily involve
removing bugs.
Try-except statement
A try-except statement is a statement of the following form
try:
<statements>
except:
<statements>
Python attempts to execute all of the statements underneath try. If there is no error, then Python does
nothing and skips over all the statements underneath except. However, if Python crashes while inside the try
portion, it recovers and jumps over to the except. It then executes all the statements underneath there.
Tuple
A tuple is an immutable type representing a sequence of values. Tuples are represented as a
comma-separated sequence of expressions inside parentheses, like this
Typing
Typing is a special form of precondition that requires that a variable hold a value of a specific type. It is the
most common form of function precondition in this course.
Unit test
A unit test is a collection of test cases used to test a unit of code, typically a function or method.
Variable
A variable is named with an associated value. We sometimes refer to it as a named box with the value in the
box. In Python, there are four basic kinds of variable, determined by the memory region they fit in:
• Parameters
• Local variables
• Attributes
While-loop
A while-loop is a statement with the following form:
while <bool>:
<statements>
Python executes this statement as follows: It first evaluates the boolean expression. If it is True, then it
executes the statements indented underneath. It repeats this process as long as the boolean expression is
True. This boolean expression is often referred to as the loop condition.
While-loops are difficult to use properly and are often combined with loop invariants.
Wider type
A wider type is a type for which Python will perform a cast from automatically. In other words, Python will
automatically cast from a narrower type to a wider type. This concept only applies to bool and the numeric
types. The progression below shows the progression from narrowest type to the widest type: