The main goal of this module is to step back and
review core concepts in programming: loops, conditionals, lists, functions
5.0 Audio:
5.0 Variables
Consider:
a = 3
x = 3.14
s = 'hello'
this_is_fun = False
odds = [1, 3, 5, 7]
x = 2.718
odds[2] = 9
Variables have four aspects to them:
A name, like x or this_is_fun above.
⇒ Variable names don't change during execution, they are
given by the programmer
A current value.
⇒ At any moment during execution, a variable has a value.
This value can change (often does) during execution.
A scope. More about this in Unit-2.
A type, such as integer, float, string, Boolean,
list (and there are a few more, an advanced topic).
Let's examine what happens when each line in the above
program executes:
When the first line executes:
After the second line executes:
After the third line:
After the fourth:
After the fifth:
Then, one of the variables does have its value replaced:
Finally, one of the elements in the list gets replaced:
Now, there is a somewhat highly technical point to be
made:
The "box" is what we're using to conceptualize what
a variable is, and how it behaves when we change it.
Generally, this is how you should think of it.
However, some books will present certain kinds of
variables differently, with more detail.
Underneath the hood, in fact, string and list variables
are a bit different.
So, for the sake of completeness, we'll just point out this
more detailed version:
Here:
String and list variables are themselves small boxes.
These variables contain a reference (conceptualized
by the black arrow above) to the actual data.
The actual data for a string (the letters in it) are
stored side by side, which is why we can "get at" letters
via code like
s[2]
(3rd char in string).
This more detailed version will make sense when you
eventually get to see objects (a feature of Python)
in the future.
5.1 How to read and mentally execute programs
Let's start with a simple example:
x = 2
y = 3
x = x + y
for i in range(6):
x = x + i
print('x =', x)
Let's look at this in steps:
First, start by noticing the chunks of code:
Next, walk through the execution of the first chunk, and
notice that the value of x is what's used later:
Note:
If it helps to picture the boxes for x and y, then do so.
Look at the statement
x = x + y
and say to yourself: "First, let's look at the right side, and the
values of x and y now".
Go backwards in the code to see the values of x and y.
Then perform the operation to get the new value of x (5 in
this case) that
replaces what was there (which was 2).
Next, notice key aspects of the loop without yet executing
iteration by iteration:
Then you get down to the iterative level and execute
iteration by iteration:
i x
Before loop starts: 5
After first iteration 0 5
After i is 1 1 6
After i is 2 2 8
After i is 3 3 11
After i is 4 4 15
After i is 5 5 20
Finally, there's the print:
5.1 Exercise:
Examine and mentally execute the following:
i = 1
j = 4
A = [2, 4, 6, 8, 10, 12]
total = 0
for k in range(i, j+1):
total = total + A[k]
print('total =', total)
Then confirm by writing in
my_execution_practice.py.
Next, let's look at reading a complex conditional:
x = 7
y = 6
z = 5
c = 'b'
if x < 5:
x = x + 10
elif x > y:
if (x > 0) and (x+y > z):
if c == 'a':
print('yes, a')
else:
if c == 'b':
print('no, b')
else:
print('boo')
else:
print('ok, that is enough')
We'll do this in steps, working from the "outside going in":
We'll start by noticing some variables and values being
assigned, followed by a large nested conditional:
Notice:
The conditional has both an elif and an else-part.
The conditions depend on the variables above.
Next, we identify which part of the outer conditional executes:
Then, working inwards, we examine and see that the elif's
code block is itself a big conditional:
The combined Boolean expression
if (x > 0) and (x+y > z):
does turn out to be true, and so we enter the if-block:
The if-condition fails:
if c == 'a':
and so execution proceeds into the else-block:
Notice:
There's no else-part (which is allowed).
Clearly, in this case, the condition succeeds and we get to
the print:
Observe that if the condition was
if c == 'z':
then we would jump right out of the whole:
Important: This example illustrates how critical it is
to make sure the indentation is correct.
For example, consider these three variations:
Indent-case #1:
if c == 'a':
print('yes, a')
else:
if c == 'b':
print('no, b')
print('hello')
Indent case #2:
if c == 'a':
print('yes, a')
else:
if c == 'b':
print('no, b')
print('hello')
Indent case #3:
if c == 'a':
print('yes, a')
else:
if c == 'b':
print('no, b')
print('hello')
Can you see why they're all different?
5.2 Exercise:
Type up the above in
my_execution_practice2.py.
Then, try different assignments to c. That is, try c = 'a',
then c = 'b'. Show and then explain in your module pdf why
the above are different. (Submit your program with c = 'a').
Consider this variation (we've added a few print's):
x = 7
y = 6
z = 5
c = 'b'
if x < 5:
x = x + 10
elif x > y:
print('one')
if (x > 0) and (x+y > z):
print('two')
if c == 'a':
print('yes, a')
else:
if c == 'b':
print('no, b')
print('three')
print('four')
else:
print('boo')
print('five')
else:
print('ok, that is enough')
print('six')
print('seven')
5.3 Exercise:
Which of the above added print's execute? See if you can
identify them just by reading. Then confirm in
my_execution_practice3.py.
5.4 Exercise:
Examine and mentally execute the following:
A = [-1, 2, 3, -5, 6, 7]
total = 0
for i in range(len(A)):
if A[i] > 0:
if A[i] % 2 == 0:
total = total + A[i]
else:
print('rejected:', A[i])
else:
print('illegal:', A[i])
print(total)
Then confirm by writing in
my_execution_practice4.py.
5.5 Audio:
5.6 Exercise:
Consider the following program:
def printword(n):
if n == 1:
print('one')
elif n == 2:
print('two')
elif n == 3:
print('three')
elif n == 4:
print('four')
else:
print('cannot handle n > 4')
printword(1)
printword(2)
printword(3)
printword(4)
printword(5)
In
my_conditional.py.
rewrite the function so that it does the same thing
but without using any elif's.
First, Python reads through and stores the
function definition:
Then execution starts with the first print, followed
by the first function call (or function
invocation, if you like big words).
At this point, execution enters the function:
After the function executes, execution proceeds to
just after the function call:
In this particular case, the very next line is a call
to the same function:
After which:
Consider this program:
def print_two_xs():
print('xx')
def print_three_ys():
print('yyy')
def print_more():
print_two_xs()
print_three_ys()
print('z')
print('start')
for i in range(8):
if i % 3 == 0:
print_two_xs()
elif i % 3 == 1:
print_three_ys()
else:
print_two_xs()
print_three_ys()
print_more()
print('end')
5.8 Exercise:
Examine and mentally execute the above program.
Then confirm by writing it up in
my_execution_practice5.py.
5.9 Audio:
5.3 Functions with parameters
Here's an example:
def print_stuff(n):
print('n =', n)
n = 2*n
print('twice n =', n)
n = n // 4
print('half n =', n)
m = 4
print(m)
print_stuff(m)
print(m)
Let's point out:
The parameter variable n gets its value from the argument
variable:
Recall: // is the integer-division operator.
Later in the function, the parameter variable gets its
value changed:
It's important to realize that we could have named
the argument variable n as well:
Any code inside the function that uses or
modifies n affects only the parameter variable n
(the n inside the function and not the n outside the function).
Let's emphasize one more thing by looking at:
def print_stuff(n):
print('n =', n)
n = 2*n
print('twice n =', n)
n = n // 4
print('half n =', n)
print('m =', m) # Trying to access m
m = 4
print(m)
print_stuff(m)
print(m)
print('n =', n) # Trying to access n
5.10 Exercise:
Type up the above in
my_variable_access.py.
Also, in your module pdf, explain why half of the original n
is calculated as n // 4.
Note:
You will notice that m is accessible in the function whereas
the parameter variable n is not accessible outside.
This is a topic (called scope) that we'll address
in Unit-2.
5.4 Functions that return values
Let's look at an example:
def incr(n):
m = n + 1
return m
i = 5
j = incr(i)
k = incr( incr( incr(j) ) )
print(k)
Note:
Consider the moment the first call to the incr() function occurs:
j = incr(i)
Here, the value in argument variable i is copied into parameter
variable n:
Then, the value m becomes 6, which is returned
when the return statement executes:
Moments later (in a tiny fraction of a second), you can
think of the function call itself as replaced by the return value:
After the return occurs, remember that the parameter
and other function-defined variables disappear.
(Fresh versions will be created whenever another function call occurs.)
Now let's examine
k = incr( incr( incr(j) ) )
At this moment j has the value 6.
The innermost call happens first:
k = incr( incr( incr(j) ) )
which goes to incr(), which returns 7, which results in
k = incr( incr( 7 ) )
Then, for the next call to incr(), the value 7 is copied
into the parameter variable, resulting in:
k = incr( incr( 7 ) )
which goes to incr(), which returns 8, which results in
k = incr( 8 )
And finally,
k = incr( 8 )
results in
k = 9
Lastly, remember that a return statement
can have expressions. Which means we can shorten incr() to:
def incr(n):
return n+1
We could also place another function call in
the return statement itself:
def incr(n):
return n+1
def double_incr(n):
k = incr(incr(n))
return k
Consider a function with multiple returns:
def strange(n):
print('start-of-strange')
if n < 0:
return 0
elif n == 0:
return 1
else:
s = 1
for k in range(n+1):
s = s + k
return s
print('end-of-strange')
print(strange(-3))
print(strange(0))
print(strange(3))
Note:
In the first call to the function strange(), the
parameter n will have the value -3.
In this case, we see 'start-of-strange' printed.
The if-condition is true, which means the first return
executes:
def strange(n):
print('start-of-strange')
if n < 0:
return 0
elif n == 0:
return 1
else:
s = 1
for k in range(n+1):
s = s + k
return s
print('end-of-strange')
Execution leaves the function immediately, which means
nothing else in the function executes.
5.11 Exercise:
For each of the three calls to the function strange(),
can you tell whether 'end-of-strange' gets printed?
Then confirm by typing it up in
my_strange_example.py.
5.5 Lists are different
Consider
def add_one(n):
n = n + 1
print('incr: n=', n)
def list_add_one(A):
for k in range(0, len(A)):
A[k] = A[k] + 1
print('list_incr: A=', A)
p = 3
add_one(p)
print(p)
B = [1, 2, 3]
list_add_one(B)
print(B)
5.12 Exercise:
Type up the above in
my_list_example.py.
Observe:
The add_one() function has no effect on the variable p,
since the value in p gets copied into n.
The parameter variable n does get 1 added (as the print
in add_one() confirms.
One the other hand, print(B) shows that the elements of
B have 1 added to each of them.
Why is this? Why are lists different as parameters?
List variables are actually references, sometimes
called pointers.
Think of a reference or pointer as a special token that
provides access to the list elements.
Whoever has the token can access the list elements.
When the variable B's contents are copied into parameter
variable A, then the variable A has the special token.
Which means variable A access the list elements and
can change those.
Why was the Python language designed this way?
It turns out that copying over big lists into parameter
variables can greatly slow down execution.
Thus, lists (and other such complex objects) don't have
their elements copied.
Instead, it's only the reference (which is really a single
number, under the hood) gets copied.
For practice, let's look at a list example with Boolean values (True, False):
def list_or(A):
x = True
for k in range(0, len(A)):
x = x or (not A[k])
return x
B = [True, True, False, False]
print(list_or(B))
5.13 Exercise:
Trace through each iteration of the loop in the function list_or(),
showing x, k, and each A[k].
Then confirm in
my_list_example2.py.
And another example for practice:
def within1(x, y):
# Write your code here to return True or False
# Return True if the difference between x and y is 1 or less
def first_diff(A, B):
k = 0
while (k < len(A)) and (within1(A[k], B[k])):
k = k + 1
if k == len(A):
return -1
else:
return k
X = [1, 2, 3, 4]
Y = [2, 2, 3, 3]
Z = [1, 1, 1, 4]
print(first_diff(X,Y)) # Should print -1
print(first_diff(X,Z)) # Should print 2
5.14 Exercise:
The function first_diff() is intended to take two lists,
compare elements in the same positions, and identify the first
position where the two lists differ by more than 1. If
no such difference exists, the function should return -1.
In
my_list_example3.py,
complete the code in the within1() function so that it returns
True only when the difference between x and y is 1 or less.
Thus, True when x=1, y=2, or when x=4, y=3, or when x=5, y=5.
After that trace through the iteration in the while-loop.
5.6 Why are functions useful?
Functions are very useful for four different reasons:
Code written in a function can be re-used.
For example, compare
X = [1, 3, 5, 7]
total = 0
for k in X:
total += k
avg = total / len(X)
print(avg)
Y = [2, 4, 6, 8, 10]
total = 0
for k in Y:
total += k
avg = total / len(Y)
print(avg)
with
def mean(A):
total = 0
for k in A:
total += k
return total / len(A)
X = [1, 3, 5, 7]
print(mean(X))
Y = [2, 4, 6, 8, 10]
print(mean(Y))
The second big reason is composability,
as this example shows:
z = incr( diff(x, y) )
(You can imagine what the functions incr() and diff() do.)
Another example showing compactness with functions:
s = ' hello '
print(len(s.strip()))
A long program broken up into functions will make
the program more readable and therefore more easily understood.
The biggest reason, perhaps, is that it has become one
of two important ways by which multiple programmers
use each others' code.
Example: you have used functions in
drawtool
and Python functions
like math.random().
How do you know when to create functions vs. writing long code?
There are no rules. The judgement comes with practice.
Generally, tiny computations like increment don't
need functions.
Any significant computation that is likely to be
re-used should probably be in a functions.
Use functions when breaking things into functions greatly
improves readability.