Skip to content

Commit fc1f597

Browse files
bobahopBethanyG
andauthored
[New Concept]: Recursion (#3051)
* Create instructions.append.md * Create hints.md * Update exercises/practice/twelve-days/.docs/hints.md Co-authored-by: BethanyG <[email protected]> * Update exercises/practice/twelve-days/.docs/hints.md Co-authored-by: BethanyG <[email protected]> * Update exercises/practice/twelve-days/.docs/hints.md Co-authored-by: BethanyG <[email protected]> * Update exercises/practice/twelve-days/.docs/instructions.append.md Co-authored-by: BethanyG <[email protected]> * Update exercises/practice/twelve-days/.docs/instructions.append.md Co-authored-by: BethanyG <[email protected]> * Update exercises/practice/twelve-days/.docs/instructions.append.md Co-authored-by: BethanyG <[email protected]> * Update exercises/practice/twelve-days/.docs/hints.md Co-authored-by: BethanyG <[email protected]> * Update config.json * Create config.json * Create links.json * Create about.md * Create introduction.md * Update introduction.md * Update about.md * Update about.md * Update about.md * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update about.md * Update about.md * Update introduction.md * Update introduction.md * Update about.md * Update about.md * Update about.md * Update about.md * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update about.md Rearranged as per our Stack discussion. * Update about.md Added in for points 7, 9, 11, and 12 of our stack discussion. For point 10, I think I'd prefer just adding links to links.json, as I don't feel comfortable introducing those concepts into the text. * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> * Update about.md * Update concepts/recursion/about.md Co-authored-by: BethanyG <[email protected]> Co-authored-by: BethanyG <[email protected]>
1 parent 42467b9 commit fc1f597

File tree

5 files changed

+242
-0
lines changed

5 files changed

+242
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"blurb": "Recursion repeats code in a function by the function calling itself.",
3+
"authors": [
4+
"bobahop"
5+
],
6+
"contributors": []
7+
}

concepts/recursion/about.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# About
2+
3+
Recursion is a way to repeatedly execute code inside a function through the function calling itself.
4+
Functions that call themselves are know as _recursive_ functions.
5+
Recursion can be viewed as another way to loop/iterate.
6+
And like looping, a Boolean expression or `True/False` test is used to know when to stop the recursive execution.
7+
_Unlike_ looping, recursion without termination in Python cannot not run infinitely.
8+
Values used in each function call are placed in their own frame on the Python interpreter stack.
9+
If the total amount of function calls takes up more space than the stack has room for, it will result in an error.
10+
11+
## Looping vs Recursive Implementation
12+
13+
Looping and recursion may _feel_ similar in that they are both iterative.
14+
However, they _look_ different, both at the code level and at the implementation level.
15+
Looping can take place within the same frame on the call stack.
16+
This is usually managed by updating one or more variable values to progressively maintain state for each iteration.
17+
This is an efficient implementation, but it can be somewhat cluttered when looking at the code.
18+
19+
Recursion, rather than updating _variable state_, can pass _updated values_ directly as arguments to the next call (iteration) of the same function.
20+
This declutters the body of the function and can clarify how each update happens.
21+
However, it is also a less efficient implementation, as each call to the same function adds another frame to the stack.
22+
23+
## Recursion: Why and Why Not?
24+
25+
If there is risk of causing a stack error or overflow, why would anyone use a recursive strategy to solve a problem?
26+
_Readability, traceability, and intent._
27+
There may be situations where a solution is more readable and/or easier to reason through when expressed through recursion than when expressed through looping.
28+
There may also be program constraints with using/mutating data, managing complexity, delegating responsibility, or organizing workloads.
29+
Problems that lend themselves to recursion include complex but repetitive problems that grow smaller over time, particularly [divide and conquer][divide and conquer] algorithms and [cumulative][cumulative] algorithms.
30+
However, due to Python's limit for how many frames are allowed on the stack, not all problems will benefit from a fully recursive strategy.
31+
Problems less naturally suited to recursion include ones that have a steady state, but need to repeat for a certain number of cycles, problems that need to execute asynchronously, and situations calling for a great number of iterations.
32+
33+
## Looping vs Recursive Strategy: Indira's Insecurity
34+
35+
Indira has her monthly social security auto-deposited in her bank account on the **_second Wednesday_** of every month.
36+
Indira is concerned about balancing her check book.
37+
She is afraid she will write checks before her money is deposited.
38+
She asks her granddaughter Adya to give her a list of dates her money will appear in her account.
39+
40+
Adya, who is just learning how to program in Python, writes a program based on her first thoughts.
41+
She wants to return a `list` of the deposit dates so they can be printed.
42+
She wants to write a function that will work for _any year_.
43+
In case the schedule changes (_or in case other relatives want Adya to calculate their deposit schedules_), she decides the function needs to take an additional parameter for the _weekday_.
44+
Finally, Adya decides that the function needs a parameter for _which weekday_ of the month it is: the first, second, etc.
45+
For all these requirements, she decides to use the `date` class imported from `datetime`.
46+
Putting all of that together, Adya comes up with:
47+
48+
```
49+
from datetime import date
50+
51+
52+
def paydates_for_year(year, weekday, ordinal):
53+
"""Returns a list of the matching weekday dates.
54+
55+
Keyword arguments:
56+
year -- the year, e.g. 2022
57+
weekday -- the weekday, e.g. 3 (for Wednesday)
58+
ordinal -- which weekday of the month, e.g. 2 (for the second)
59+
"""
60+
output = []
61+
62+
for month in range(1, 13):
63+
for day_num in range(1, 8):
64+
if date(year, month, day_num).isoweekday() == weekday:
65+
output.append(date(year, month, day_num + (ordinal - 1) * 7))
66+
break
67+
return output
68+
69+
# find the second Wednesday of the month for all the months in 2022
70+
print(paydates_for_year(2022, 3, 2))
71+
```
72+
73+
This first iteration works, but Adya wonders if she can refactor the code to use fewer lines with less nested looping.
74+
She's also read that it is good to minimize mutating state, so she'd like to see if she can avoid mutating some of her variables such as `output`, `month`, and `day_num` .
75+
76+
She's read about recursion, and thinks about how she might change her program to use a recursive approach.
77+
The variables that are created and mutated in her looping function could be passed in as arguments instead.
78+
Rather than mutating the variables _inside_ her function, she could pass _updated values as arguments_ to the next function call.
79+
With those intentions she arrives at this recursive approach:
80+
81+
```
82+
from datetime import date
83+
84+
85+
86+
def paydates_for_year_rec(year, weekday, ordinal, month, day_num, output):
87+
"""Returns a list of the matching weekday dates
88+
89+
Keyword arguments:
90+
year -- the year, e.g. 2022
91+
weekday -- the weekday, e.g. 3 (for Wednesday)
92+
ordinal -- which weekday of the month, e.g. 2 (for the second)
93+
month -- the month currently being processed
94+
day_num -- the day of the month currently being processed
95+
output -- the list to be returned
96+
"""
97+
if month == 13:
98+
return output
99+
if date(year, month, day_num).isoweekday() == weekday:
100+
return paydates_for_year_rec(year, weekday, ordinal, month + 1, 1, output
101+
+ [date(year, month, day_num + (ordinal - 1) * 7)])
102+
return paydates_for_year_rec(year, weekday, ordinal, month, day_num + 1, output)
103+
104+
# find the second Wednesday of the month for all the months in 2022
105+
print(paydates_for_year_rec(2022, 3, 2, 1, 1, []))
106+
107+
```
108+
109+
Adya is happy that there are no more nested loops, no mutated state, and 2 fewer lines of code!
110+
111+
She is a little concerned that the recursive approach uses more steps than the looping approach, and so is less "performant".
112+
But re-writing the problem using recursion has definitely helped her deal with ugly nested looping (_a performance hazard_), extensive state mutation, and confusion around complex conditional logic.
113+
It also feels more "readable" - she is sure that when she comes back to this code after a break, she will be able to read through and remember what it does more easily.
114+
115+
In the future, Adya may try to work through problems recursively first.
116+
She may find it easier to initially walk through the problem in clear steps when nesting, mutation, and complexity are minimized.
117+
After working out the basic logic, she can then focus on optimizing her initial recursive steps into a more performant looping approach.
118+
119+
Even later, when she learns about `tuples`, Adya could consider further "optimizing" approaches, such as using a `list comprehension` with `Calendar.itermonthdates`, or memoizing certain values.
120+
121+
## Recursive Variation: The Tail Call
122+
123+
A tail call is when the last statement of a function only calls itself and nothing more.
124+
This example is not a tail call, as the function adds 1 to the result of calling itself
125+
126+
```python
127+
def print_increment(step, max_value):
128+
if step > max_value:
129+
return 1
130+
print(f'The step is {step}')
131+
return 1 + print_increment(step + 1, max_value)
132+
133+
134+
def main():
135+
retval = print_increment(1, 2)
136+
print(f'retval is {retval} after recursion')
137+
138+
if __name__ == "__main__":
139+
main()
140+
141+
```
142+
143+
This will print
144+
145+
```
146+
The step is 1
147+
The step is 2
148+
retval is 3 after recursion
149+
```
150+
151+
To refactor it to a tail call, make `retval` a parameter of `print_increment`
152+
153+
```python
154+
def print_increment(step, max_value, retval):
155+
if step > max_value:
156+
return retval
157+
print(f'The step is {step}')
158+
return print_increment(step + 1, max_value, retval + 1)
159+
160+
161+
def main():
162+
retval = print_increment(1, 2, 1)
163+
print(f'retval is {retval} after recursion')
164+
165+
if __name__ == "__main__":
166+
main()
167+
168+
```
169+
170+
You may find a tail call even easier to reason through than a recursive call that is not a tail call.
171+
However, it is always important when using recursion to know that there will not be so many iterations that the stack will overflow.
172+
173+
## Recursion Limits in Python
174+
175+
Some languages are able to optimize tail calls so that each recursive call reuses the stack frame of the first call to the function (_similar to the way a loop reuses a frame_), instead of adding an additional frame to the stack.
176+
Python is not one of those languages.
177+
To guard against stack overflow, Python has a recursion limit that defaults to one thousand frames.
178+
A [RecursionError](https://docs.python.org/3.8/library/exceptions.html#RecursionError) exception is raised when the interpreter detects that the recursion limit has been exceeded.
179+
It is possible to use the [sys.setrecursionlimit](https://docs.python.org/3.8/library/sys.html#sys.setrecursionlimit) method to increase the recursion limit, but doing so runs the risk of having a runtime segmentation fault that will crash the program, and possibly the operating system.
180+
181+
## Resources
182+
183+
To learn more about using recursion in Python you can start with
184+
- [python-programming: recursion][python-programming: recursion]
185+
- [Real Python: python-recursion][Real Python: python-recursion]
186+
- [Real Python: python-thinking-recursively][Real Python: python-thinking-recursively]
187+
188+
[python-programming: recursion]: https://www.programiz.com/python-programming/recursion
189+
[Real Python: python-recursion]: https://realpython.com/python-recursion/
190+
[Real Python: python-thinking-recursively]: https://realpython.com/python-thinking-recursively/
191+
[RecursionError]: https://docs.python.org/3.8/library/exceptions.html#RecursionError
192+
[setrecursionlimit]: https://docs.python.org/3.8/library/sys.html#sys.setrecursionlimit
193+
[divide and conquer]: https://afteracademy.com/blog/divide-and-conquer-approach-in-programming
194+
[cumulative]: https://www.geeksforgeeks.org/sum-of-natural-numbers-using-recursion/

concepts/recursion/introduction.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Introduction
2+
3+
Recursion is a way to repeat code in a function by the function calling itself.
4+
It can be viewed as another way to loop/iterate.
5+
Like looping, a Boolean expression or `True/False` test is used to know when to stop the recursive execution.
6+
_Unlike_ looping, recursion without termination in Python cannot not run infinitely.
7+
Values used in each function call are placed in their own frame on the Python interpreter stack.
8+
If the total amount of function calls takes up more space than the stack has room for, it will result in an error.
9+
10+
```python
11+
def print_increment(step, max_value):
12+
if step > max_value:
13+
return
14+
print(f'The step is {step}')
15+
print_increment(step + 1, max_value)
16+
17+
18+
def main():
19+
print_increment(1, 2)
20+
print("After recursion")
21+
22+
if __name__ == "__main__":
23+
main()
24+
25+
```
26+
27+
This will print
28+
29+
```
30+
The step is 1
31+
The step is 2
32+
After recursion
33+
```
34+
35+
There may be some situations that are more readable and/or easier to reason through when expressed through recursion than when expressed through looping.

concepts/recursion/links.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,6 +2526,11 @@
25262526
"slug": "raising-and-handling-errors",
25272527
"name": "Raising And Handling Errors"
25282528
},
2529+
{
2530+
"uuid": "b77f434a-3127-4dab-b6f6-d04446fed496",
2531+
"slug": "recursion",
2532+
"name": "Recursion"
2533+
},
25292534
{
25302535
"uuid": "d645bd16-81c2-4839-9d54-fdcbe999c342",
25312536
"slug": "regular-expressions",

0 commit comments

Comments
 (0)