CS61A Lab 1: Control Flow

Week 2, Spring 2013

Warm Up: What would Python print?

Predict what Python will print in response to each of these expressions. Then try it and make sure your answer was correct, or if not, that you understand why! If you don't remember how to start Python, type in: python3 into the command line.

# Q1
>>> a = 1
>>> b = a + 1
>>> a + b + a * b 
______________

# Q2
>>> a == b
______________

# Q3
>>> z, y = 1, 2
>>> print(z)
______________

# Q4
>>> def square(x):
...     print(x * x)        # Hit enter twice
...
>>> a = square(b)
______________

# Q5
>>> print(a)
______________

# Q6
>>> def square(y):
...     return y * y        # Hit enter twice
...
>>> a = square(b)
>>> print(a)
_______________

Boolean operators

1. What would Python print? Try to figure it out before you type it into the interpreter!

# Q1
>>> a, b = 10, 6
>>> a > b and a == 0
_______________

# Q2
>>> a > b or a == 0
_______________

# Q3
>>> not a > 0
_______________

# Q4
>>> a != 0
_______________

# Q5
>>> True and False
_______________

# Q6
>>> True and True
_______________

# Q7
>>> True or False
_______________

# Q8
>>> False or False
_______________

# Q9
>>> True and True or True and False
_______________

Boolean order of operations: just like with mathematical operators, boolean operators (and, or, and not) have an order of operations, too:

# highest priority
not
and
or
# lowest priority

For example, the following expression will evaluate to True:

True and not False or not True and False

It might be easier to rewrite the expression like this:

(True and (not False)) or ((not True) and False)

If you find writing parentheses to be clearer, it is perfectly acceptable to do so in your code.

Short-circuit operators: in Python, and and or are examples of short-circuit operators. Consider the following line of code:

10 > 3 or 1 / 0 != 1

Generally, operands are evaluated from left to right in Python. The expression 10 > 3 will be evaluated first, then 1 / 0 != 1 will be evaluated. The problem is, evaluating 1 / 0 will cause Python to raise an error! (You can try dividing by 0 in the interpreter)

However, the original line of code will not cause any errors -- in fact, it will evaluate to True. This is made possible due to short-circuiting, which works in the following ways:

Some examples:

>>> True and False and 1 / 0 == 1     # stops at the False
False
>>> True and 1 / 0 == 1 and False     # hits the division by zero
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

>>> True or 1 / 0 == 1                # stops at the True
True
>>> False or 1 / 0 == 1 or True       # hits the division by zero
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

Short-circuiting allows you to write boolean expressions while avoiding errors. Using division by zero as an example:

x != 0 and 3 / x > 3

In the line above, the first operand is used to guard against a ZeroDivisionError that could be caused by the second operand.

if statements

2. What would the Python interpreter display?

>>> a, b = 10, 6

# Q1
>>> if a == b:
...     a
... else:
...     b
...
_______________

# Q2
>>> if a == 4:
...     6
... elif b >= 4:
...     6 + 7 + a
... else: 
...     25
...
________________

# Q3
# ';' lets you type multiple commands on one line
>>> if b != a: a; b  
_________________

The following are some common mistakes when using if statements:

1. Using '=' instead of '==': remember, = (single equals) is used for assignment, while == (double equals) is used for comparison.

# bad
>>> if a = b:
...     print("uh oh!")
...

# good!
>>> if a == b:
...     print("yay!")
...

2. Multiple comparisons: for example, trying to check if both x and y are greater than 0.

# bad
>>> if x and y > 0:
...     print("uh oh!")
...

# good!
>>> if x > 0 and y > 0:
...     print("yay!")
...
Guarded commands

Consider the following function:

>>> def abs(x):
...     if x >= 0:
...         return x
...     else:
...         return -x
...

It is syntactically correct to rewrite abs in the following way:

>>> def abs(x):
...     if x >= 0:
...         return x
...     return -x       # missing else statement!
...

This is possible as a direct consequence of how return works -- when Python sees a return statement, it will immediately terminate the function. In the above example, if x >= 0, Python will never reach the final line. Try to convince yourself that this is indeed the case before moving on.

Keep in mind that guarded commands only work if the function is terminated! For example, the following function will always print "less than zero", because the function is not terminated in the body of the if suite:

>>> def foo(x):
...     if x > 0:
...         print("greater than zero")
...     print("less than zero")
...
>>> foo(-3)
less than zero
>>> foo(4)
greater than zero
less than zero

In general, using guarded commands will make your code more concise -- however, if you find that it makes your code harder to read, by all means use an else statement.

while loops

3. What would Python print?

>>> n = 2
>>> def exp_decay(n):
...     if n % 2 != 0:
...         return
...     while n > 0:
...         print(n)
...         n = n // 2 # See exercise 3 for an explanation of what '//' stands for
...
>>> exp_decay(1024)
__________________
>>> exp_decay(5)
__________________


3. Before we write our next function, let's look at the idea of floor division (rounds down to the nearest integer) versus true division (decimal division).

True Division Floor Division
>>> 1 / 4 >>> 1 // 4
0.25 0
>>> 4 / 2 >>> 4 // 2
2.0 2
>>> 5 / 3 >>> 5 // 3
1.666666666667 1

Thus, if we have an operator "%" that gives us the remainder of dividing two numbers, we can see that the following rule applies:

b * (a // b) + (a % b) = a

Now, define a function factors(n) which takes in a number, n, and prints out all of the numbers that divide n evenly. For example, a call with n=20 should result as follows (order doesn’t matter):

>>> factors(20)
20
10
5
4
2
1 

Helpful Tip: You can use the % to find if something divides evenly into a number. % gives you a remainder, as follows:

>>> 10 % 5
0
>>> 10 % 4
2
>>> 10 % 7
3
>>> 10 % 2
0

Error messages

By now, you've probably seen a couple of error messages. Even though they might look intimidating, error messages are actually very helpful in debugging code. The following are some common error messages (found at the bottom of a traceback):

Using these descriptions of error messages, you should be able to get a beter idea of what went wrong with your code. If you run into error messages, try to identify the problem before asking for help.

ucb.py Features

For this course, there are a few features that you might find useful for your assignments – the staff have provided these in a file called ucb.py, which will be provided with every project. If you would like to use the features in ucb.py, you will need to import the ucb.py file into your Python files: First you'll need to copy the ucb.py to your current directory, you can do this by running the command

cp ~cs61a/lib/ucb.py .
Then, you should add the following statement to the top of your Python file:
from ucb import main, interact
For now, we are going to go over the main feature, which allows you to easily test your functions:

ucb.py's main:

An entry point of a program is the place where the execution starts happening. It is usually very convenient to be able to mark an entry point in a Python file for testing purposes. Say we have the following file cube.py:

def cube(x):
    return x * x * x 

print("Should be 1:", cube(1))
print("Should be 8:", cube(2))
print("Should be 27:", cube(3))
star [123] ~ # python3 -i cube.py
Should be 1: 1
Should be 8: 8
Should be 27: 27
>>>

One problem with this file is that the tests are not cleanly arranged: it would be much better if we had a test function that performed these tests:

def cube(x):
    return x * x * x

def run_tests():
    print("Should be 1:", cube(1))
    print("Should be 8:", cube(2))
    print("Should be 27:", cube(3))

However, now, if I run the file, nothing happens:

star [123] ~ # python3 -i cube.py
>>>

This is because, to Python, all we have done is define two functions: cube and run_tests. We want Python to actually do something when we type in 'python3 -i cube.py'. So, we specify an entry point with the @main annotation:

def cube(x):
    return x * x * x

def run_tests():
    print("Should be 1:", cube(1))
    print("Should be 8:", cube(2))
    print("Should be 27:", cube(3))

@main
def main():
    print("Starting.")
    run_tests()
    print("Ending.")

star [123] ~ # python3 -i cube.py
Starting.
Should be 1: 1
Should be 8: 8
Should be 27: 27
Ending.
>>>

As you can see, Python will start execution at the beginning of a function with @main typed above it. The @main feature is a handy way to control what happens when you run your Python script from the command line.

A little word about -i: If you don't know already, what the -i option does is runs your Python script, then throws you into an interpreter. If you omit the -i option, Python will only run your script. Note that for the last few exercises, we didn't actually need to use the interpreter prompt, so it would have sufficed to only run python3 cube.py.