Programming 1, 2021-2
Week 5: Notes

Some of today's topics are covered in these sections of Think Python:

Here are some additional notes.

None

None is a special value in Python that represents nothingness. It is often useful to represent the absence of a value.

For example, here's a program that computes the maximum of all numbers read from standard input. It keeps the maximum in a variable 'mx', which is initialized to None before any numbers are read:

import sys

mx = None

for line in sys.stdin:
    x = int(line)
    if mx == None or x > mx:
        mx = x

print(f'max = {mx}')

Be aware that the Python REPL prints nothing at all when you give it an expression whose value is None:

>>> x = None
>>> x
>>> 

Tuples

A tuple in Python is an immutable sequence. Its length is fixed, and you cannot update values in a tuple. Tuples are written with parentheses:

>>> t = (3, 4, 5)
>>> t[0]
3
>>> t[2]
5

All operations that read sequences in Python will work with tuples: for example, you can access elements using slice syntax, and you can iterate over a tuple:

>>> t = (3, 4, 5, 6)
>>> t[1:3]
(4, 5)
>>> for x in t:
...    print(x)
... 
3
4
5
6
>>>

The built-in function tuple() will convert any sequence to a tuple:

>>> tuple([2, 4, 6, 8])
(2, 4, 6, 8)
>>> tuple(range(10))
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

We will often use tuples in our code. They are more efficient than lists, and their immutability makes a program easy to understand, since when reading the code you don't need to worry about if/when their elements might change.

A tuple of two elements is called a pair. A tuple of three elements is called a triple.

Nested lists

A list may contain any type of elements, including tuples or sublists:

>>> l = [(2, 3), (10, 20), (5, 6)]

>>> m = [[2, 4, 6], [5, 7, 9], [1, 3, 5]]

We can use nested lists to solve many two-dimensional problems. In particular, a list of lists is a natural way to represent a matrix in Python. Consider this matrix with dimension 3 x 3:

5  11  12
2   8   7
14  2   6

If we want to store it in Python as a list of lists, we have two choices. We could store the matrix in row-major order, in which each sublist holds a row of the matrix:

m = [ [5, 11, 12], [2, 8, 7], [14, 2, 6] ]

Or we can use column-major order, in which each sublist is a matrix column:

m = [ [5, 2, 14], [11, 8, 2], [12, 7, 6] ]

The choice is arbitrary. However, usually by convention we will use row-major order. With this ordering, we can use the syntax m[i][j] to access the matrix element at row i, column j.

Defining functions

In Python, and in almost every other programming language, we may define our own functions. Here's a function that takes an integer 'n' and prints the word 'orange' n times:

def orange(n):
    for i in range(n):
        print('orange')

n is a parameter (or argument) to the function. When we call the function, we will pass a value for n.

Let's put this function in a file called 'orange.py'. We may run Python in this file and specify the '-i' parameter, which means that Python should read the file and then remain in an interactive session. In that session, we can call the function as we like:

$ py -i orange.py
>>> orange(5)
orange
orange
orange
orange
orange
>>>

A function may return a value. For example:

def add(x, y, z):
    return x + y + z + 10

>>> add(2, 4, 6)
22

If a function doesn't explicitly return a value, then it will return the default value None.

Note that the 'return' statement will exit a function immediately, even from within the body of a loop or nested loop. This is often very convenient.

Here's a function to compute the factorial of a given integer:

def factorial(n):
    prod = 1

    for i in range(2, n + 1):    # 1 .. n
        prod = prod * i
    
    return prod

We will use functions extremely often in programs that we write. Functions can call other functions, and a typical program will have many nested function calls of this sort. (A function can even call itself, which is a phenomenon called recursion that we will extensively explore later in this course.)

I recommend that you limit functions to be at most about 50 lines of code, which is as many as will fit on a single screen. If a function is longer than that, I'd suggest breaking it up into smaller functions.

Passing by value and reference

Consider this program:

def inc(i):
    i += 1
    print(i)

j = 7

inc(j)
print(j)

The program will print

8
7

Here is why. First the function inc receives the value i = 7. The statement "i += 1" sets i to 8, and inc writes this value.

Now control returns to the top-level code after the function definition. The value of j is still 7! That's because Python passes integer arguments by value: a function receives a local copy of the value that was passed. A function may modify its local copy, but that does not change the corresponding value in the caller, i.e. the code that called the function. And so the second number printed by this program is 7.

Now consider this variant:

def inc(l):
    l[0] += 1
    print(l[0])

a = [3, 5, 7]
inc(a)
print(a[0])

This program will print

4
4

This program behaves somewhat differently from the preceding one, because Python passes lists by reference. When the program calls inc(a), then as the function runs l and a are the same list. If we modify the list in the function, the change is visible in the caller.

Really this is similar to behavior that we see even without calling functions:

a = 4
b = a
a += 1   # does not change b

a = [4, 5, 6]
b = a
a[0] = 7  # change is visible in b[0]

local and global variables

Consider this Python program:

x = 7

def abc(a):
    i = a + x
    return i

def ha():
    i = 4
    print(abc(2))
    print(x + i)

The variable x declared at the top is a global variable. Its value is visible everywhere: both inside the function abc(), and also in the top-level code at the end of the program.

The variables i declared inside abc() and ha() are local variables. They are different variables: when the line "i = a + x" executes inside abc(), that does not change the value of i in ha().

Now consider this variation of the program above:

x = 7
i = 4

def abc(a):
    i = a + x
    return i

print(abc(2))
print(x + i)

In this version, the variables x and i declared at the top are both global. abc() declares its own local i. This is not the same as the global i. In particular, when abc() runs "i = a + x", this does not change the value of the global. The local i is said to shadow the global i inside the function body.

Local variables are a fundamental feature of every modern programming language. Because a local variable's scope (the area of the program where it is visible) is small, it is easy to understand how the variable will behave. I recommend making variables local whenever possible.

In the program above, what if we want the function abc() to use the global i, rather than making a new local variable? We can achieve this by declaring i as global:

x = 7
i = 4

def abc(a):
    global i
    i = a + x
    return i

print(abc(2))
print(x + i)

Now the line "i = a + x" will update the global i.

Notice that in all of the programs above, abc() is able to read the global x without declaring it as global. But if a function wants to write to a global variable, it must declare the variable as global.

To be more precise, here is how Python determines whether each variable in a function is local or global:

'else' clauses in 'while' and 'for' statements

Python allows a 'while' or 'for' statement to contain an 'else' clause, which will execute if the loop does not exit via a 'break' statement. This is an unusual feature among programming languages, but can sometimes be convenient. For example, consider a program that checks whether a number is prime:

n = int(input())

i = 2
prime = True
while i * i <= n:
    if n % i == 0:
        prime = False
        break
    i += 1

if prime:
    print('prime')
else:
    print('composite')

We may rewrite this program without the boolean variable 'prime', by adding an 'else' clause to the 'while' statement:

n = int(input())

i = 2
while i * i <= n:
    if n % i == 0:
        print('prime')
        break
    i += 1
else:
    print('composite')