Recursion basics¶
~20 min ยท prerequisite: Scope: where names live
What you will be able to do¶
- Explain what recursion means, and write a function that calls itself.
- Write a base case that stops the calls, and a recursive case that makes the problem smaller.
- Recognize the
RecursionErrormessage and find the missing base case that caused it.
The idea¶
Recursion is when a function calls itself. It sounds strange at first, but it is useful when a problem can be split into a smaller copy of the same problem. For example, the number of items in a list is one more than the number of items in the list without its first element.
A recursive function does not need a loop, because the repeated work happens through the calls. Every recursive function needs two parts. The base case is a situation that is answered directly, without another call. Check for it first in the function. The recursive case is the part that calls the function again on a smaller input. Each call must move closer to the base case. If it never does, the calls never end. Problems like nested lists follow the same pattern, because each level looks like the whole.
Think of a set of nesting dolls. You open one and find a smaller doll inside. You keep opening until you reach the smallest doll, which does not open at all. The smallest doll is the base case.
Each call gets its own local variables, so the call factorial(4) and the call it makes, factorial(3), do not overwrite each other's n.
Python limits how deep the calls can go. The default limit is 1000 nested calls. If a function goes deeper than that, Python stops it and raises a RecursionError.
Recursion is not a faster way to do work. A loop is usually clearer for a plain count. Use recursion when splitting the problem into smaller parts makes the code shorter.
Worked example¶
Expected output:
Line by line, this is what Python does:
if n <= 1:checks the base case. Whennis 1 or less, the function returns1without calling itself.return n * factorial(n - 1)is the recursive case. It asks for the factorial of one less, and multiplies the answer byn.factorial(5)needs5 * factorial(4). That call needs4 * factorial(3), which needs3 * factorial(2), which needs2 * factorial(1).factorial(1)hits the base case and returns1.- The waiting calls now finish in reverse order:
2 * 1is2, then3 * 2is6, then4 * 6is24, then5 * 24is120. printshows120.
Try it¶
The first block checks the base and recursive cases of factorial. The second block adds up a list by taking off its first item each time. The third block splits a number into its last digit and the rest.
Common mistakes¶
-
Leaving out the base case. The following function calls itself forever:
Python stops the calls and reports:
Add an
ifthat returns a value when the input is small enough. -
Returning nothing from the base case. The following base case prints a message but does not return a value:
def bad_factorial(n): if n <= 1: print("base case") else: return n * bad_factorial(n - 1) bad_factorial(2)Python reports:
The base case returned
None. Return1there instead. -
Expecting very deep recursion to work. The following call needs five thousand nested calls:
Python reports:
Use a loop for long counts, or keep the input small.
-
Forgetting to return the recursive result. The following function computes the answer and then throws it away:
Python reports:
The call
add_up(1)givesNone, because its recursive line has noreturn. Writereturn add_up(n - 1) + n.
Practice¶
- Define
countdown_list(n)that returns a list fromndown to 1, such as[3, 2, 1]. Assert thatcountdown_list(3)equals[3, 2, 1]. - Define
count_items(items)that returns the number of items in a list, without callinglen. Assert thatcount_items(["a", "b", "c"])equals3, and thatcount_items([])equals0. - Define
reverse_text(text)that returns the text reversed. Assert thatreverse_text("abc")equals"cba", and thatreverse_text("")equals"". - Define
fibonacci(n)wherefibonacci(0)is0,fibonacci(1)is1, and each later value is the sum of the two before it. Assert thatfibonacci(6)equals8and thatfibonacci(10)equals55. - Stretch: define
is_palindrome(text)that returnsTruewhen the text reads the same forwards and backwards. Use recursion ontext[1:-1]. Assert thatis_palindrome("level")isTrue, thatis_palindrome("python")isFalse, and thatis_palindrome("")isTrue.
Solution
- ```python def countdown_list(n): if n == 0: return [] return [n] + countdown_list(n - 1)
assert countdown_list(3) == [3, 2, 1] ```
- ```python def count_items(items): if not items: return 0 return 1 + count_items(items[1:])
assert count_items(["a", "b", "c"]) == 3 assert count_items([]) == 0 ```
- ```python def reverse_text(text): if text == "": return "" return reverse_text(text[1:]) + text[0]
assert reverse_text("abc") == "cba" assert reverse_text("") == "" ```
- ```python def fibonacci(n): if n <= 1: return n return fibonacci(n - 1) + fibonacci(n - 2)
assert fibonacci(6) == 8 assert fibonacci(10) == 55 ```
- ```python def is_palindrome(text): if len(text) <= 1: return True if text[0] != text[-1]: return False return is_palindrome(text[1:-1])
assert is_palindrome("level") is True assert is_palindrome("python") is False assert is_palindrome("") is True ```
Recap¶
- A recursive function calls itself on a smaller input, and must have a base case that returns without calling again.
- Each call has its own local variables, and results return in reverse order.
- Calls that go too deep raise
RecursionError, which often means a missing base case.