Recursion
A function can call itself. That sounds like it should loop forever, and it does unless the function also knows when to stop. Used with care it solves a kind of problem that loops handle badly: problems whose parts look like smaller copies of the whole.
A function that calls itself
def count_down(n):
if n == 0:
print("done")
return
print(n)
count_down(n - 1)
count_down(3)
count_down(3) prints 3 and calls count_down(2), which prints 2 and
calls count_down(1), and so on until count_down(0) prints done and
returns without calling anything. Two parts make it work:
- A base case: an input so small the answer is immediate. Here,
n == 0. - A smaller call: the function calls itself with an input closer to the
base case. Here,
n - 1.
Remove the base case and the calls never stop. Python gives up after a few thousand nested calls:
def forever(n):
return forever(n + 1)
forever(0)
That error, RecursionError, almost always means a missing or unreachable
base case.
Where the calls wait
When count_down(3) calls count_down(2), the first call is not finished:
it is paused, waiting for the inner call to return. Python keeps the paused
calls on a stack, the same last-in-first-out pile from
Module 4.
Each new call goes on top; when it returns, it comes off and the call below
resumes.
You can see the stack grow and shrink by printing after the recursive call too:
def show(n, depth=0):
pad = " " * depth
print(pad + "enter", n)
if n > 0:
show(n - 1, depth + 1)
print(pad + "leave", n)
show(3)
Every enter happens on the way down, every leave on the way back up,
in reverse order. Whatever a function does after its recursive call happens
once the deeper calls are all finished.
Returning values
The useful form returns something. The sum of a list is the first item plus the sum of the rest, and the sum of an empty list is zero:
def total(items):
if not items:
return 0
return items[0] + total(items[1:])
print(total([3, 5, 7]))
print(total([]))
Trace it: total([3, 5, 7]) is 3 + total([5, 7]), which is
3 + (5 + total([7])), which is 3 + (5 + (7 + total([]))), which is
3 + (5 + (7 + 0)). The base case supplies the first concrete number and
the additions happen on the way back up.
Factorial has the same shape. 5! is 5 × 4!, and 0! is 1:
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
print(factorial(5))
Every recursive function you write will have this outline:
def solve(problem):
if problem is trivial:
return the immediate answer
smaller = solve(a smaller version of problem)
return combine(problem, smaller)
When a loop is not enough
The examples so far could be loops, and as loops they would be fine. The case for recursion is data that is nested, where "the rest" is not a shorter list but a list inside a list.
A shopping list might contain sub-lists: a list for the bakery, a list for the dairy aisle, each possibly containing further lists. To count every item, a single loop cannot look inside the inner lists, and a loop inside a loop only goes one level deeper. Recursion goes as deep as the data does:
def count_items(items):
count = 0
for item in items:
if isinstance(item, list):
count += count_items(item)
else:
count += 1
return count
shopping = ["apple", ["bread", "rolls", ["jam"]], "milk", ["eggs"]]
print(count_items(shopping))
isinstance(item, list) asks whether item is a list. If it is, the
function hands it to itself; if not, it is one item. The base case is
hidden in the loop: an item that is not a list contributes one and
causes no further call.
This is the shape of most real recursion: folders inside folders, replies to replies, parts made of parts. Trees and graphs, later in this section, are built on it.
Counting the calls
Recursion is code, so the previous page applies. The Fibonacci numbers are
defined by each being the sum of the two before it: 0, 1, 1, 2, 3, 5, 8, …. Written straight from the definition:
calls = 0
def fib(n):
global calls
calls += 1
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
for n in [10, 15, 20, 25]:
calls = 0
print("fib(", n, ") =", fib(n), "after", calls, "calls")
Five more in n means about eleven times the calls. fib(40) would take
minutes. The waste is visible in the trace: fib(5) calls fib(3) twice,
fib(2) three times, and so on, recomputing answers it already had. The
fix is to remember answers, which has its own page later. For now the
lesson is that a recursive function calling itself twice per step can be
far more expensive than it looks.
Recursion or a loop?
- If the data is flat, a list or a range, a loop is usually clearer and cannot hit the recursion limit.
- If the data is nested, or the problem is "do this, then do the same to each part", recursion is clearer and a loop gets awkward fast.
- Either way, find the base case first. If you cannot say when it stops, it will not.
Try it
The point of these is to practise recursion, so write each one as a function that calls itself, even where a loop would do.
Sum, recursively
Write total(items) that returns the sum of a list of numbers, using
recursion: the total of a list is its first item plus the total of the
rest, and the total of an empty list is 0.
total([3, 5, 7]) -> 15
total([]) -> 0
The point is to practise recursion, so write it without a loop and without
sum.
def total(items):
...
Power, recursively
Write power(base, exp) that returns base raised to exp, where exp
is a whole number zero or more. Use the fact that base ** exp is
base × base ** (exp - 1), and anything to the power 0 is 1.
power(2, 5) -> 32
power(3, 0) -> 1
power(5, 1) -> 5
The point is to practise recursion, so write it without a loop and without
**.
def power(base, exp):
...
Flatten a nested list
Write flatten(items) that takes a list whose elements are either values
or further lists, nested to any depth, and returns one flat list of all
the values in order.
flatten(["apple", ["bread", ["jam"]], "milk"]) -> ["apple", "bread", "jam", "milk"]
flatten([1, [2, [3, [4]]]]) -> [1, 2, 3, 4]
flatten([]) -> []
The point is to practise recursion: a loop over the items, and a recursive call for each item that is itself a list.
def flatten(items):
...
Fibonacci, recursively
The Fibonacci numbers start 0, 1 and each one after that is the sum of
the two before it: 0, 1, 1, 2, 3, 5, 8, 13, …. Write fib(n) that
returns the n-th one, counting from fib(0) = 0.
fib(0) -> 0
fib(1) -> 1
fib(7) -> 13
fib(20) -> 6765
The point is to practise recursion with two base cases and two recursive
calls. The checks stay at n of 25 or less, where the straightforward
version is still quick.
def fib(n):
...
Next: more pages are on the way. Until then, the practice problems are a good place to try recursion on your own.