Study material generated by AI from this openly licensed book (CC BY-NC 3.0). Not affiliated with or endorsed by the publisher.
Think Python, 2nd Edition, by Allen B. Downey, Green Tea Press, licensed under CC BY-NC 3.0. Read the original free
Loading…
Chapter 6 · pp. 73–84
Fruitful Functions
So far, the functions you have written only do things, such as print text or move a turtle. This chapter shows how to write functions that hand a value back to the caller, and how to build them safely one small step at a time. You will also meet recursion, where a function is defined using itself, and learn habits for checking inputs and debugging.
Functions that give back values
Fruitful vs. void. A fruitful function sends back a value using a return statement that includes an expression. A void function does something (prints, draws) but has no useful result; technically it gives back None. Math functions are fruitful: you can store the result, as in e = math.exp(1.0). (p. 73)
What return does. When Python runs a return statement, the function stops right there and the value of the expression becomes the result of the call. For example, def area(radius): return math.pi * radius**2 computes the whole expression and sends it back. You could first store the value in a temporary variable like a and return a. That is a little longer, but you can print a while debugging. (p. 73)
Dead code. Anything that can never run is called dead code. Since a function ends as soon as a return executes, a line placed after return x in the same block will never run. A print there will never show anything. (p. 74)
Every path needs a return. A common mistake is to think a function is correct because the conditions cover the usual cases. Suppose absolute_value tests only x < 0 and x > 0. If x is 0, neither test is true, so the function reaches its end without a return. Python then gives back None, not 0. The fix is to make sure that every possible path ends in a return, for example by using else. (p. 74)
Building a function step by step
Incremental development. Large functions take long to debug if you write them all at once. Instead, start with a skeleton that runs, then add and test a little code at a time. If something breaks, you only need to check the newest lines. (pp. 74–76)
The example is distance(x1, y1, x2, y2):
Write a skeleton that just returns 0.0. It runs, so you can call it.
Add dx = x2 - x1 and dy = y2 - y1, and print them.
Add dsquared = dx**2 + dy**2 and print it.
Finally use math.sqrt(dsquared) and return the result.
At each stage, test with inputs whose answer you already know. The call distance(1, 2, 4, 6) is a good choice because it should give 5.0. (pp. 74–76)
The formula. The distance between (x1,y1) and (x2,y2) is
(x2−x1)2+(y2−y1)2
This comes from the Pythagorean theorem. The horizontal and vertical differences are the two legs of a right triangle, and the distance is the hypotenuse. For (1, 2) and (4, 6), the legs are 3 and 4, so the distance is 9+16=25=5. (pp. 74–75)
Scaffolding. The temporary print statements for dx, dy and dsquared helped you build the function, but they are not part of the finished product. This kind of helper code is called scaffolding. Once the function works, remove the prints so it only returns a value. Test values with known answers make scaffolding much more useful. (pp. 75–76, 82)
Composition. Composition means calling one function from inside another, or passing one call's result straight into another call. To find a circle's area from its center (xc, yc) and a point on its edge (xp, yp), first get the radius with distance(xc, yc, xp, yp), then pass it to area. Once this works, you can drop the temporary variables and write return area(distance(xc, yc, xp, yp)). (p. 76)
Boolean functions
A boolean function returns True or False. This is handy for hiding a complicated test behind a clear name. Such functions are usually named like yes/no questions, for example is_divisible. (pp. 76–77)
The expression x % y == 0 is already a boolean, so you can write return x % y == 0 instead of an if/else that returns True or False. For example, is_divisible(6, 3) is True and is_divisible(6, 4) is False. In a condition, write if is_divisible(x, y):. Writing == True after the call adds nothing. (p. 77)
Recursion
Recursive definitions. A recursive definition mentions the thing being defined. A purely circular definition is useless because it never ends. A recursive one works because it has a base case that stops the chain. Factorial is defined by 0!=1 and n!=n⋅(n−1)!. So 3!=3⋅2⋅1⋅1=6, and the rule 0!=1 is what ends the self-reference. (pp. 77–78)
Recursive functions. To write a recursive function, check the base case first. If it does not apply, make a recursive call on a smaller input and combine the result. For factorial, if n == 0 return 1. Otherwise compute factorial(n-1) and multiply by n. (p. 78)
Tracing factorial(3): it calls factorial(2), which calls factorial(1), which calls factorial(0). That last call returns 1. The values then come back up: 1⋅1=1, then 2⋅1=2, then 3⋅2=6. A stack diagram shows one frame for each call, with the return values passed back up through them. In the last frame, the variables recurse and result do not exist, because that branch never ran. (pp. 78–79)
The leap of faith. Following every call quickly becomes overwhelming. Instead, assume the recursive call returns the correct answer, just as you trust math.cos without reading its code. Then ask: if factorial(n-1) is right, can I get factorial(n)? Yes, multiply by n. (pp. 79–80)
This makes fibonacci manageable. It has base cases fibonacci(0) = 0 and fibonacci(1) = 1, and otherwise it makes two recursive calls. Tracing both is very hard, but with the leap of faith you simply add their two results. (pp. 79–80)
Guarding against bad input
Calling factorial(1.5) never reaches the base case. The value of n goes 1.5, 0.5, -0.5, and so on, never equal to 0, so the recursion runs until Python raises a RuntimeError. Negative integers cause the same problem. (p. 80)
A guardian is a conditional at the start of the function that checks for bad inputs. In factorial, one guardian uses isinstance(n, int) to reject non-integers, and another rejects n < 0. Each prints an error message and returns None. If the code gets past both checks, n must be a non-negative integer, so you can be sure the recursion reaches 0 and stops. (pp. 80–81)
Debugging fruitful functions
If a function misbehaves, check three things in order. (pp. 81–82)
The arguments. Print the parameters (and maybe their types) at the start to see whether a precondition is violated.
The function itself. Print the value just before each return to see whether a postcondition is violated. Use inputs that are easy to check by hand.
The caller. Make sure the returned value is actually used, and used correctly, where the function is called.
Printing at the start and end of a recursive function, with indentation that grows with n, also shows the order of the calls and returns. For factorial(4), you would see the calls go down to 0 and then the returns of 1, 1, 2, 6, 24. (pp. 81–82)
Check yourself
What is the difference between a fruitful function and a void function? What does a void function return?
A function has if x < 0: return -x and if x > 0: return x. What does it return for x = 0, and why?
Describe the four stages of building distance incrementally. What should distance(1, 2, 4, 6) return?
Why does factorial(1.5) cause an error, and how does a guardian prevent it?
Explain the leap of faith by using fibonacci(n) as your example.