6b. Prof. Rosenthal’s Review#
A review of Chapter 6: Return Values, for students who have already studied the chapter. Use the two lists under Concepts covered as a checklist (click a list’s title to hide it), then work through the Questions, which go from easy to hard. Each question has a suggested answer under Answer. Try the question first, then open the Answer.
Concepts covered#
Python Syntax and Semantics
Return values
This defines a function that returns a value:
def FUNCTION_NAME(PARAMETERS): BODY return EXPRESSION
A call to the function is an expression whose value is the return value. For example, after
def double(x): return x * 2
double(5)is10,y = double(5)assigns10toy, anddouble(5) + 1is11.A
returnstatement ends the function immediately; any statements after it in the same block never run.A function returns
Noneif it ends without running areturnstatement (or runsreturn None).Noneis a special value, likeTrueandFalse.printdisplays a value;returnhands a value back to the caller. A function that only prints returnsNone, so its result cannot be used in an expression.In a notebook, a cell whose last expression is
Nonedisplays nothing;print(None)displaysNone.Calling a function without using its return value (not assigning it, printing it or using it in an expression) throws the value away.
Local variables of a function, including the variable it returns, cannot be used outside the function: that is a
NameError.A function can have several
returnstatements, for example one in each branch of anifstatement. Every possible path through the function should reach areturn; a path that does not returnsNone.
Boolean functions
A function can return
TrueorFalse. A comparison already has a boolean value, so it can be returned directly:return x % y == 0instead of anifstatement that returnsTrueorFalse.A boolean function can be used directly as a condition:
if is_divisible(6, 2):. Comparing its result with== Trueis unnecessary.
Recursion with return values
A recursive function can return a value: the base case returns a value directly, and the other cases combine the result of a recursive call into their return value. For example:
def factorial(n): if n == 0: return 1 else: return n * factorial(n - 1)
If an argument can skip past the base case (for example,
factorial(1.5)callsfactorial(0.5), thenfactorial(-0.5), …), the result is infinite recursion and aRecursionError.
Checking types
isinstance(VALUE, TYPE)returnsTrueif the value has that type:isinstance(3, int)isTrueandisinstance(1.5, int)isFalse.A function can check its arguments first, and display an error message and return
Noneif they are not valid.
The math module
import mathmakesmath.sqrt(X)(square root, returns a float) andmath.piavailable.
Definitions, etc.
Definitions
DEFINITION: A return value is the result of a function. If a function call is used as an expression, the return value is the value of the expression.
DEFINITION: A pure function is a function that does not display anything or have any other effect, other than returning a value.
DEFINITION: Dead code is part of a program that can never run, often because it comes after a
returnstatement.DEFINITION: Incremental development is a way of writing programs by adding and testing only a small amount of code at a time.
DEFINITION: Scaffolding is code used while developing a program (such as
printcalls that show intermediate values) that is not part of the final version.DEFINITION: A language, or part of a language, is Turing complete if it can perform any computation that can be described by an algorithm. Python with functions, conditionals, return values and recursion is Turing complete.
DEFINITION: Input validation is checking the parameters of a function to make sure they have the correct types and values.
Other ideas
Incremental development:
Start with a working program (for example, a function that always returns
0), make small changes, and test after every change.Use variables to hold intermediate values so you can display and check them.
Once the program works, remove the scaffolding.
When testing a function, choose arguments for which you know the right answer (for example, points 3 and 4 apart, whose distance is 5).
The “leap of faith”: when reading a function call, including a recursive call, assume the function works correctly and returns the right result, instead of following the flow of execution into it.
A recursive definition (such as: the factorial of 0 is 1, and the factorial of n is n times the factorial of n minus 1) can be translated directly into a recursive function.
If a function is not working, there are three possibilities: its arguments are wrong (a precondition is violated), the function is wrong (a postcondition is violated), or the caller uses the return value wrongly (or not at all).
printcalls at the beginning and end of a function (showing the parameters and the return value) make the flow of execution visible.
Questions#
The questions use functions that return values, if statements, recursion and the math module.
The setup cell below imports math.
import math
Question 1 (easy): what is displayed?#
Predict what this code displays, then open the Answer to check.
def double(x):
return x * 2
y = double(5)
print(y, double(y))
print(double('ab'))
Answer
10 20
abab
double(5) returns 10, which is assigned to y; then double(y) returns 20.
'ab' * 2 is 'abab': the function works with any value that can be multiplied by 2.
Question 2 (easy): what is displayed?#
def shout(word):
print(word + '!')
result = shout('hi')
print(result)
Answer
hi!
None
shout displays hi! but has no return statement, so it returns None, and None is assigned to result.
Question 3 (easy): what is displayed?#
def add_one(x):
print('start')
return x + 1
print('never')
print(add_one(1))
Answer
start
2
return ends the function immediately, so print('never') never runs. Code like that is called dead code.
The 2 is displayed by the print outside the function, which displays the return value.
Question 4 (easy): write a function#
Write a function called average that takes two numbers and returns (not displays) their average.
Start from this header (replace ... with the body):
def average(a, b):
...
Examples
print(average(3, 5))
4.0
result = average(1, 2)
print(result * 2)
3.0
Answer
def average(a, b):
return (a + b) / 2
Then try: print(average(3, 5)).
The parentheses matter: a + b / 2 would divide only b.
Another approach uses a local variable, which is useful while testing (you can display it):
def average(a, b):
total = a + b
return total / 2
Question 5 (medium): what is displayed?#
def absolute_value_wrong(x):
if x < 0:
return -x
if x > 0:
return x
print(absolute_value_wrong(-3), absolute_value_wrong(4), absolute_value_wrong(0))
Answer
3 4 None
When x is 0, neither condition is true, so the function ends without reaching a return statement and returns None.
Every possible path through a function should reach a return. One fix is to replace the second if with else:
def absolute_value(x):
if x < 0:
return -x
else:
return x
print(absolute_value(-3), absolute_value(4), absolute_value(0))
3 4 0
Question 6 (medium): what is displayed?#
def is_even(n):
return n % 2 == 0
print(is_even(4), is_even(7))
if is_even(10):
print('10 is even')
if not is_even(3):
print('3 is odd')
x = is_even(5) or is_even(6)
print(x)
Answer
True False
10 is even
3 is odd
True
n % 2 == 0 is a boolean expression, so is_even returns True or False.
A boolean function can be used directly as a condition, or combined with not, and and or. is_even(5) or is_even(6) is False or True, which is True.
Question 7 (medium): what is displayed?#
def square(x):
return x * x
def sum_of_squares(a, b):
return square(a) + square(b)
print(sum_of_squares(3, 4))
print(square(square(2)))
print(square(sum_of_squares(1, 1)))
Answer
25
16
4
A call is an expression, so its return value can be used as an argument of another call:
square(square(2)) is square(4), which is 16, and square(sum_of_squares(1, 1)) is square(2), which is 4.
Question 8 (medium): find the bug#
This code should display 60 (a bill of 50 plus a 20% tip). What does it display instead, and how do you fix it?
def add_tip(bill):
return bill + bill // 5
bill = 50
add_tip(bill)
print(bill)
Answer
50
add_tip(bill) returns 60, but the caller does nothing with the return value, so it is thrown away.
The function does not change the variable bill outside it (its parameter is a different, local variable).
Fix it by using the return value:
bill = add_tip(bill)
print(bill)
60
or simply print(add_tip(50)).
Question 9 (medium): what is displayed?#
def half(n):
if not isinstance(n, int):
print('half needs an integer')
return None
return n // 2
print(half(9))
print(half(9.0))
Answer
4
half needs an integer
None
isinstance(9, int) is True, so half(9) returns 9 // 2, which is 4.
9.0 is a float, not an integer, so half(9.0) displays the message and returns None, which the outer print displays.
Checking the arguments like this is called input validation.
Question 10 (medium): write two functions#
Write a function larger(a, b) that returns the larger of two numbers, and a function largest(a, b, c) that returns the largest of three numbers by calling larger.
Do not use the built-in function max.
Start from this header (replace ... with the body):
def larger(a, b):
...
def largest(a, b, c):
...
Examples
print(larger(3, 7), larger(7, 3))
7 7
print(largest(4, 9, 2), largest(-1, -5, -3))
9 -1
Answer
def larger(a, b):
if a > b:
return a
else:
return b
def largest(a, b, c):
return larger(larger(a, b), c)
Then try: print(larger(3, 7), larger(7, 3)).
larger(a, b) is the larger of the first two, and the larger of that and c is the largest of all three.
Because larger returns its result (instead of displaying it), largest can use it.
larger can also be written without else, because return a ends the function:
def larger(a, b):
if a > b:
return a
return b
Question 11 (medium): write a boolean function#
A year is a leap year if it is divisible by 4, except that years divisible by 100 are not leap years, unless they are also divisible by 400.
Write a function is_leap that takes a year and returns True if it is a leap year and False otherwise.
Start from this header (replace ... with the body):
def is_leap(year):
...
Examples
print(is_leap(2024), is_leap(2023))
True False
print(is_leap(1900), is_leap(2000))
False True
Answer
A chained conditional that checks the most specific rule first:
def is_leap(year):
if year % 400 == 0:
return True
elif year % 100 == 0:
return False
elif year % 4 == 0:
return True
else:
return False
Then try: print(is_leap(2024), is_leap(2023)).
Or return one boolean expression:
def is_leap(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
Question 12 (medium): simplify a boolean function#
This function works, but it is longer than it needs to be.
def is_teen(age):
if age >= 13 and age <= 19:
return True
else:
return False
Rewrite it with a single return statement and no if statement.
Start from this header (replace ... with the body):
def is_teen(age):
...
Examples
print(is_teen(15), is_teen(12))
True False
print(is_teen(19), is_teen(20))
True False
Answer
The condition already has the value True or False, so return it:
def is_teen(age):
return age >= 13 and age <= 19
Then try: print(is_teen(15), is_teen(12)).
The same, with the shorthand for a range of values:
def is_teen(age):
return 13 <= age <= 19
Question 13 (hard): what is displayed?#
def power(b, e):
print('power', b, e)
if e == 0:
return 1
result = b * power(b, e - 1)
print('returning', result)
return result
print(power(2, 3))
Answer
power 2 3
power 2 2
power 2 1
power 2 0
returning 2
returning 4
returning 8
8
Each call displays its arguments and then waits for its recursive call to return. The call with e == 0 is the base case: it returns 1 without displaying returning.
Then the calls finish in the opposite order: 2 * 1 is 2, 2 * 2 is 4, 2 * 4 is 8.
The last line is displayed by the print outside the function. The print calls inside power are scaffolding, useful to see the flow of execution.
Question 14 (hard): what is displayed?#
Predict what this code displays. What does mystery compute, in general?
def mystery(n):
if n < 10:
return n
return n % 10 + mystery(n // 10)
print(mystery(7), mystery(123), mystery(9999))
Answer
7 6 36
mystery returns the sum of the digits of n: n % 10 is the last digit and n // 10 is the number without its last digit.
With the leap of faith: assuming mystery(12) correctly returns 1 + 2, mystery(123) returns 3 + mystery(12), which is 6.
A number with one digit (less than 10) is the base case.
Question 15 (hard): missing the base case#
Run this cell to define factorial.
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
Two correct ways to call it:
print(factorial(3))
6
print(factorial(0))
1
What happens with this call? Change factorial so that it displays a message and returns None when its argument is not a non-negative integer.
factorial(1.5)
Answer
The calls get n = 1.5, 0.5, -0.5, -1.5, … and never 0, so the base case is never reached (a RecursionError):
----> 1 factorial(1.5)
In factorial(n):
2 if n == 0:
3 return 1
----> 4 return n * factorial(n - 1)
... last 1 frames repeated, from the frame below ...
In factorial(n):
2 if n == 0:
3 return 1
----> 4 return n * factorial(n - 1)
RecursionError: maximum recursion depth exceeded
Check the argument before the recursion (input validation):
def factorial(n):
if not isinstance(n, int):
print('factorial is only defined for integers.')
return None
elif n < 0:
print('factorial is not defined for negative numbers.')
return None
elif n == 0:
return 1
else:
return n * factorial(n - 1)
print(factorial(1.5))
print(factorial(-2))
print(factorial(3))
factorial is only defined for integers.
None
factorial is not defined for negative numbers.
None
6
Negative integers also need a check: factorial(-2) would call factorial(-3), and so on, forever.
Question 16 (hard): write a recursive function#
Write a recursive function multiply that takes two integers a and b, where b is not negative, and returns a times b, using only + and - (no * and no loops).
Hint: a times b is a plus a times b - 1.
Start from this header (replace ... with the body):
def multiply(a, b):
...
Examples
print(multiply(6, 7))
42
print(multiply(-3, 4), multiply(5, 0))
-12 0
Answer
The base case is b == 0, because anything times 0 is 0:
def multiply(a, b):
if b == 0:
return 0
return a + multiply(a, b - 1)
Then try: print(multiply(6, 7)).
With the leap of faith: if multiply(a, b - 1) is right, adding one more a gives a times b.
Another approach puts the recursive result in a variable first, which makes it easy to display while testing:
def multiply(a, b):
if b == 0:
return 0
else:
rest = multiply(a, b - 1)
return a + rest
Question 17 (hard): incremental development#
The area of a triangle with sides a, b and c can be computed in two steps (Heron’s formula):
sis half the perimeter:(a + b + c) / 2.The area is the square root of
s * (s - a) * (s - b) * (s - c).
Write a function triangle_area that returns the area. Use incremental development: start with a version that returns 0, then add one step at a time, displaying the intermediate values, and remove those print calls at the end.
Start from this header (replace ... with the body):
def triangle_area(a, b, c):
...
Examples
print(triangle_area(3, 4, 5))
6.0
print(triangle_area(5, 5, 6), round(triangle_area(2, 2, 2), 3))
12.0 1.732
Answer
A version in the middle of the development, with scaffolding, could be:
def triangle_area(a, b, c):
s = (a + b + c) / 2
print('s is', s) # for 3, 4, 5 it should be 6.0
return 0
print(triangle_area(3, 4, 5))
s is 6.0
0
And the final version, without the scaffolding:
import math
def triangle_area(a, b, c):
s = (a + b + c) / 2
product = s * (s - a) * (s - b) * (s - c)
return math.sqrt(product)
Then try: print(triangle_area(3, 4, 5)).
The examples were chosen because their answers are known: a 3-4-5 right triangle has area 3 * 4 / 2 = 6.
Summary and questions by Prof. Y. Rosenthal, based on Think Python: 3rd Edition by Allen B. Downey. Text license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International