Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 54 additions & 36 deletions concepts/recursion/about.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# About

Recursion is a way to repeatedly execute code inside a function through the function calling itself.
Functions that call themselves are know as _recursive_ functions.
Functions that call themselves are known as _recursive_ functions.
Recursion can be viewed as another way to loop/iterate.
And like looping, a Boolean expression or `True/False` test is used to know when to stop the recursive execution.
And like looping, a Boolean expression or `True`/`False` test is used to determine when to stop the recursive execution.

_Unlike_ looping, recursion without termination in Python cannot not run infinitely.
Values used in each function call are placed in their own frame on the Python interpreter stack.
If the total number of function calls takes up more space than the stack has room for, it will result in an error.
Expand All @@ -12,12 +13,12 @@ If the total number of function calls takes up more space than the stack has roo

Looping and recursion may _feel_ similar in that they are both iterative.
However, they _look_ different, both at the code level and at the implementation level.
Looping can take place within the same frame on the call stack.
Looping can take place within the same frame on the [call stack][what-is-the-call-stack].
This is usually managed by updating one or more variable values to progressively maintain state for each iteration.
This is an efficient implementation, but it can be somewhat cluttered when looking at the code.

Recursion, rather than updating _variable state_, can pass _updated values_ directly as arguments to the next call (iteration) of the same function.
This declutters the body of the function and can clarify how each update happens.
This de-clutters the body of the function and can clarify how each update happens.
However, it is also a less efficient implementation, as each call to the same function adds another frame to the stack.

## Recursion: Why and Why Not?
Expand All @@ -26,6 +27,7 @@ If there is risk of causing a stack error or overflow, why would anyone use a re
_Readability, traceability, and intent._
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.
There may also be program constraints with using/mutating data, managing complexity, delegating responsibility, or organizing workloads.

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.
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.
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.
Expand All @@ -45,18 +47,22 @@ Finally, Adya decides that the function needs a parameter for _which weekday_ of
For all these requirements, she decides to use the `date` class imported from `datetime`.
Putting all of that together, Adya comes up with:

```
```python
from datetime import date


def paydates_for_year(year, weekday, ordinal):
"""Returns a list of the matching weekday dates.

Keyword arguments:
year -- the year, e.g. 2022
weekday -- the weekday, e.g. 3 (for Wednesday)
ordinal -- which weekday of the month, e.g. 2 (for the second)
Arguments:
year (int): The year (e.g. 2022).
weekday (int): The weekday number (e.g. 3 for Wednesday).
ordinal (int): Which weekday of the month (e.g. 2 for the second day).

Returns:
output (list): Matching weekday dates.
"""

output = []

for month in range(1, 13):
Expand All @@ -70,58 +76,64 @@ def paydates_for_year(year, weekday, ordinal):
print(paydates_for_year(2022, 3, 2))
```

This first iteration works, but Adya wonders if she can refactor the code to use fewer lines with less nested looping.
This first iteration works, but Adya wonders if she can refactor the code to use fewer lines and less nested looping.
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` .

She's read about recursion, and thinks about how she might change her program to use a recursive approach.
She also knows about recursion, and thinks about how she might change her program to use a recursive approach.
The variables that are created and mutated in her looping function could be passed in as arguments instead.
Rather than mutating the variables _inside_ her function, she could pass _updated values as arguments_ to the next function call.
With those intentions she arrives at this recursive approach:

```
```python
from datetime import date



def paydates_for_year_rec(year, weekday, ordinal, month, day_num, output):
"""Returns a list of the matching weekday dates

Keyword arguments:
year -- the year, e.g. 2022
weekday -- the weekday, e.g. 3 (for Wednesday)
ordinal -- which weekday of the month, e.g. 2 (for the second)
month -- the month currently being processed
day_num -- the day of the month currently being processed
output -- the list to be returned
Arguments:
year (int): The year (e.g. 2022).
weekday (int): The weekday number (e.g. 3 for Wednesday).
ordinal (int): Which weekday of the month (e.g. 2 for the second day).
month (int): The month number currently being processed.
day_num (int): The day number of the month currently being processed.

Returns:
output (list): Matching weekday dates.
"""

if month == 13:
return output

if date(year, month, day_num).isoweekday() == weekday:
return paydates_for_year_rec(year, weekday, ordinal, month + 1, 1, output
+ [date(year, month, day_num + (ordinal - 1) * 7)])
return paydates_for_year_rec(
year, weekday, ordinal, month + 1, 1, output
+ [date(year, month, day_num + (ordinal - 1) * 7)]
)

return paydates_for_year_rec(year, weekday, ordinal, month, day_num + 1, output)

# find the second Wednesday of the month for all the months in 2022
print(paydates_for_year_rec(2022, 3, 2, 1, 1, []))

# find the second Wednesday of the month for all the months in 2022
print(paydates_for_year_rec(2022, 3, 2, 1, 1, []))
```

Adya is happy that there are no more nested loops, no mutated state, and 2 fewer lines of code!

She is a little concerned that the recursive approach uses more steps than the looping approach, and so is less "performant".
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.
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.
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.

In the future, Adya may try to work through problems recursively first.
She may find it easier to initially walk through the problem in clear steps when nesting, mutation, and complexity are minimized.
After working out the basic logic, she can then focus on optimizing her initial recursive steps into a more performant looping approach.

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.
Even later, when she learns about [concept:python/tuples](), Adya could consider further "optimizing" approaches, such as using a [`list comprehension`][list-comprehension] with [`Calendar.itermonthdates`][itermonthdates], or [memoizing][memoization] certain values.


## Recursive Variation: The Tail Call

A tail call is when the last statement of a function only calls itself and nothing more.
This example is not a tail call, as the function adds 1 to the result of calling itself
This example is not a tail call, as the function adds 1 to the result of calling itself:

```python
def print_increment(step, max_value):
Expand All @@ -140,15 +152,15 @@ if __name__ == "__main__":

```

This will print
This will print:

```
The step is 1
The step is 2
retval is 3 after recursion
```

To refactor it to a tail call, make `retval` a parameter of `print_increment`
To refactor it to a tail call, make `retval` a parameter of `print_increment`.

```python
def print_increment(step, max_value, retval):
Expand All @@ -172,11 +184,11 @@ However, it is always important when using recursion to know that there will not

## Recursion Limits in Python

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.
Some languages are able to optimize tail calls so that each recursive call reuses the [stack frame][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.
Python is not one of those languages.
To guard against stack overflow, Python has a recursion limit that defaults to one thousand frames.
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.
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.
A [RecursionError][RecursionError] exception is raised when the interpreter detects that the recursion limit has been exceeded.
It is possible to use the [sys.setrecursionlimit][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.

## Resources

Expand All @@ -185,10 +197,16 @@ To learn more about using recursion in Python you can start with
- [Real Python: python-recursion][Real Python: python-recursion]
- [Real Python: python-thinking-recursively][Real Python: python-thinking-recursively]

[python-programming: recursion]: https://www.programiz.com/python-programming/recursion

[Real Python: python-recursion]: https://realpython.com/python-recursion/
[Real Python: python-thinking-recursively]: https://realpython.com/python-thinking-recursively/
[RecursionError]: https://docs.python.org/3.8/library/exceptions.html#RecursionError
[setrecursionlimit]: https://docs.python.org/3.8/library/sys.html#sys.setrecursionlimit
[divide and conquer]: https://afteracademy.com/blog/divide-and-conquer-approach-in-programming
[cumulative]: https://www.geeksforgeeks.org/sum-of-natural-numbers-using-recursion/
[divide and conquer]: https://afteracademy.com/blog/divide-and-conquer-approach-in-programming
[itermonthdates]: https://docs.python.org/3/library/calendar.html#calendar.Calendar.itermonthdates
[list-comprehension]: https://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/
[memoization]: https://dbader.org/blog/python-memoization
[python-programming: recursion]: https://www.programiz.com/python-programming/recursion
[stack-frame]: https://shanechang.com/p/python-frames-systems-programming-connection/
[sys.setrecursionlimit]: https://docs.python.org/3.8/library/sys.html#sys.setrecursionlimit
[what-is-the-call-stack]: https://en.wikipedia.org/wiki/Call_stack
8 changes: 6 additions & 2 deletions concepts/recursion/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

Recursion is a way to repeat code in a function by the function calling itself.
It can be viewed as another way to loop/iterate.
Like looping, a Boolean expression or `True/False` test is used to know when to stop the recursive execution.
Like looping, a Boolean expression or `True`/`False` test is used to determine when to stop the recursive execution.
_Unlike_ looping, recursion without termination in Python cannot not run infinitely.
Values used in each function call are placed in their own frame on the Python interpreter stack.
Values used in each function call are placed in their own [frame][stack-frame] on the Python [interpreter stack][what-is-the-call-stack].
If the total number of function calls takes up more space than the stack has room for, it will result in an error.

```python
Expand Down Expand Up @@ -33,3 +33,7 @@ After recursion
```

There may be some situations that are more readable and/or easier to reason through when expressed through recursion than when expressed through looping.


[stack-frame]: https://shanechang.com/p/python-frames-systems-programming-connection/
[what-is-the-call-stack]: https://en.wikipedia.org/wiki/Call_stack
Loading