CS 61A Lab 2

Control Flow

Python flags

Sometimes, you can append certain "flags" on the command line to inspect your code further. Here are a few useful ones that'll come in handy this semester. If you want to learn more about other python flags, you can type man python

Interactive sessions

Sometimes, you just want to try some things out in the python interpreter. If you want to test out functions in a file, you'll need the -i flag as we specified above.

However, if you just need to try something out in the interpreter, without any user defined functions this is how you start an interactive session:

python3

On Cygwin:

python3 -i

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

Problem 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 or False
_______________

# Q7
>>> not True and False
_______________

# Q8
>>> not (True and False)
_______________

# Q9
>>> False or False
_______________

# Q10
>>> 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:

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, stopping function evaluation altogether! (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

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

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, and the rest of the function will not be evaluated. 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

Problem 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)
__________________

>>> def funky(k):
...     while k < 50:
...         if k % 2 == 0:
...             k += 13
...         else:
...             k += 1
...         print(k)
...     return k
>>> funky(25)
__________________

>>> n, i = 7, 0
>>> while i < n:
...     i += 2
...     print(i)
__________________

>>> n = 3
>>> while n > 0:
...     n -= 1
...     print(n)
__________________

>>> n = 3
>>> while n >= 0:
...     n -= 1
...     print(n)
__________________

>>> n = 4
>>> while True:
...     n -= 1
...     print(n)
__________________

>>> n = 10
>>> while n > 0:
...     if n % 2 == 0:
...         n -= 1
...     elif n % 2 != 0:
...         n -= 3
...     print(n)
__________________

Division

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

Next, write a function divide(num, divisor) without using the '/' or '//'.

def divide(num, divisor):
    """
    >>> divide(8, 2)
    4
    """
    "*** YOUR CODE HERE ***"

Hint: Use a while loop.

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 better idea of what went wrong with your code. If you run into error messages, try to identify the problem before asking for help. You can often Google unknown error messages to see what similar mistakes others have made to help you debug your own code.

Here's a link to a helpful Debugging Guide written by Albert Wu.

Higher ORder Functions

Higher order functions are functions that take a function as an input, and/or output a function. We will be exploring many applications of higher order functions. For each question, try to determine what Python would print. Then check in the interactive interpreter to see if you got the right answer.

>>> def square(x):
...     return x*x

>>> def neg(f, x):
...     return -f(x)

# Q1
>>> neg(square, 4)
_______________
>>> def first(x):
...     x += 8
...     def second(y):
...         print('second')
...         return x + y
...     print('first')
...     return second
...
# Q2
>>> f = first(15)
_______________
# Q3
>>> f(16)
_______________

>>> def foo(x):
...     def bar(y):
...         return x + y
...     return bar

>>> boom = foo(23)
# Q4
>>> boom(42)
_______________
# Q5
>>> foo(6)(7)
_______________

>>> func = boom
# Q6
>>> func is boom
_______________
>>> func = foo(23)
# Q7
>>> func is boom
_______________
>>> def troy():
...     abed = 0
...     while abed < 10:
...         def britta():
...             return abed
...         abed += 1
...     abed = 20
...     return britta
...
>>> annie = troy()
>>> def shirley():
...     return annie
>>> pierce = shirley()
# Q8
>>> pierce()
________________

Environment Diagrams

If you haven't found this gem already, tutor.composingprograms.com has a great visualization tool for environment diagrams. Post in your python code and it will generate an environment diagram you can walk through step-by-step! Use it to help you check your answers!

Try drawing environment diagrams for the following examples and predicting what Python will output:

# Q1
def square(x):
    return x * x

def double(x):
    return x + x

a = square(double(4))


# Q2
x, y = 4, 3

def reassign(arg1, arg2):
    x = arg1
    y = arg2

reassign(5, 6)


# Q3
def f(x):
  f(x)

print, f = f, print
a = f(4)
b = print(4)

# Q4
def adder_maker(x):
  def adder(y):
    return x + y
  return adder

add3 = adder_maker(3)
add3(4)
sub5 = adder_maker(-5)
sub5(6)
sub5(10) == add3(2)

I Heard You Liked Functions So I Put Functions In Your Functions

Define a function cycle which takes in three functions as arguments: f1, f2, f3. cycle will then return another function. The returned function should take in an integer argument n and do the following:

  1. Return a function that takes in an argument x and does the following:
    1. if n is 0, just return x
    2. if n is 1, apply the first function that is passed to cycle to x
    3. if n is 2, the first function passed to cycle is applied to x, and then the second function passed to cycle is applied to the result of that (i.e. f2(f1(x)))
    4. if n is 3, apply the first, then the second, then the third function (i.e. 3(f2(f1(x))))
    5. if n is 4, apply the first, then the second, then the third, then the first function (i.e. f1(f3(f2(f1(x)))))
    6. And so forth.

Hint: most of the work goes inside the most nested function.

def cycle(f1, f2, f3):
    """ Returns a function that is itself a higher order function
    >>> def add1(x):
    ...     return x + 1
    ...
    >>> def times2(x):
    ...     return x * 2
    ...
    >>> def add3(x):
    ...     return x + 3
    ...
    >>> my_cycle = cycle(add1, times2, add3)
    >>> identity = my_cycle(0)
    >>> identity(5)
    5
    >>> add_one_then_double = my_cycle(2)
    >>> add_one_then_double(1)
    4
    >>> do_all_functions = my_cycle(3)
    >>> do_all_functions(2)
    9
    >>> do_more_than_a_cycle = my_cycle(4)
    >>> do_more_than_a_cycle(2)
    10
    >>> do_two_cycles = my_cycle(6)
    >>> do_two_cycles(1)
    19
    """
    "*** YOUR CODE HERE ***"