7.1 Motivation: Adding Up Numbers#
This week, we’re going to learn about a powerful technique called recursion, which we’ll be using in various ways for the rest of the course. However, recursion is much more than just a programming technique, it is a way of thinking about solving problems. This new way of thinking can be summarized in this general strategy: identify how an object or problem can be broken down into smaller instances with the same structure.
Let’s begin with a series of problems that will demonstrate the need for recursion.
Summing lists and nested lists#
Consider the problem of computing the sum of a list of numbers. Easy enough:
def sum_list(lst: list[int]) -> int:
"""Return the sum of the items in a list of numbers.
>>> sum_list([1, 2, 3])
6
"""
s = 0
for num in lst:
s += num
return s
But what if we make the input structure a bit more complex: a list of lists of numbers? After a bit of thought, we might arrive at using a nested loop to process individual items in the nested list:
def sum_list2(lst: list[list[int]]) -> int:
"""Return the sum of the items in a list of lists of numbers.
>>> sum_list2([[1], [10, 20], [1, 2, 3]])
37
"""
s = 0
for list_of_nums in lst:
for num in list_of_nums:
s += num
return s
And now what happens if we want yet another layer, and compute the sum of the items in a list of lists of lists of numbers? Some more thought leads to a “nested nested list”:
def sum_list3(lst: list[list[list[int]]]) -> int:
"""Return the sum of the items in a list of lists of lists of numbers.
>>> sum_list3([[[1], [10, 20], [1, 2, 3]], [[2, 3], [4, 5]]])
51
"""
s = 0
for list_of_lists_of_nums in lst:
for list_of_nums in list_of_lists_of_nums:
for num in list_of_nums:
s += num
return s
Of course, you see where this is going: every time we want to add a new layer of nesting to the list, we add a new layer to the for
loop. Note that this is quite interesting from a “meta” perspective: the structure of the data is mirrored in the structure of the code which operates on it.
Simplifying using helpers#
You might have noticed the duplicate code above: in fact, we can use sum_list
as a helper for sum_list2
, and sum_list2
as a helper for sum_list3
:
def sum_list(lst: list[int]) -> int:
"""Return the sum of the items in a list of numbers.
"""
s = 0
for num in lst:
# num is an int
s += num
return s
def sum_list2(lst: list[list[int]]) -> int:
"""Return the sum of the items in a list of lists of numbers.
"""
s = 0
for list_of_nums in lst:
# list_of_nums is a list[int]
s += sum_list(list_of_nums)
return s
def sum_list3(lst: list[list[list[int]]]) -> int:
"""Return the sum of the items in a list of lists of lists of numbers.
"""
s = 0
for list_of_lists_of_nums in lst:
# list_of_lists_of_nums is a list[list[int]]
s+ = sum_list2(list_of_lists_of_nums)
return s
While this is certainly a nice simplification, it does not generalize very nicely.
If we wanted to implement sum_list10
, a function which works on lists with ten levels of nesting, our only choice with this approach would be to first define sum_list4
, sum_list5
, etc., all the way up to sum_list9
.
Heterogeneous lists#
There is an even bigger problem: no function of this form can handle nested lists with a non-uniform level of nesting among its elements, like
[[1, [2]], [[[3]]], 4, [[5, 6], [[[7]]]]]
We encourage you to try running the above functions on such a list—what error is raised?