Article 447C8 Removing a recursion in Python, part 1

Removing a recursion in Python, part 1

by
ericlippert
from Fabulous adventures in coding on (#447C8)
Story Image

For the last two decades or so I've admired the simplicity and power of the Python language without ever actually doing any work in it or learning about the details. I've been taking a closer look lately and having a lot of fun with it; it's a very pleasant language indeed.

A recent question on Stack Overflow got me thinking about how to turn a recursive algorithm into an iterative one, and it turns out that Python is a pretty decent language for this. The problem that the poster faced was:

  • A player is on a positive-integer numbered "space".
  • The aim is to get back to space 1.
  • If you're on an even numbered space, you must pay one coin and jump backwards half that many spaces.
  • If you're on an odd numbered space, you have two choices: pay five coins to go directly to space 1, or pay one coin and jump back one space (to an even numbered space, obviously) and go from there.

The problem is: given the space the player is on, what is the lowest cost in coins to get home? The recursive solution is straightforward:

def cost(s): if s <= 1: return 0 if s % 2 == 0: return 1 + cost(s // 2) return min(1 + cost(s - 1), 5)

However, apparently the user was experimenting with values so large that the program was crashing from exceeding the maximum recursion depth! Those must have been some big numbers. The question then is: how do we transform this recursive algorithm into an iterative algorithm in Python?

Before we get into it, of course there are fast ways of solving this specific problem; I'm not interested in this problem per se. Rather, it was just a jumping-off point for the question of how to in general remove a single recursion from a Python program. The point is that we can refactor any simple recursive method to remove the recursion; this is just the example that was at hand.

Of course, the technique that I'm going to show you is not necessarily "Pythonic". There are probably more Pythonic solutions using generators and so on. What I'd like to show here is that to remove this sort of recursion, you can do so by re-organizing the code using a series of small, careful refactorings until the program is in a form where removing the recursion is easy. Let's see first how to get the program into that form.

The first step of our transformation is: I want the thing that precedes every recursive call to be a computation of the argument, and the thing that follows every recursive call to be a return of a method call that takes the recursive result:

def add_one(n): return n + 1def get_min(n): return min(n + 1, 5)def cost(s): if s <= 1: return 0 if s % 2 == 0: argument = s // 2 result = cost(argument) return add_one(result) argument = s - 1 result = cost(argument) return get_min(result)

The second step is: I want to compute the argument in a helper function:

...def get_argument(s): if s % 2 == 0: return s // 2 return s - 1def cost(s): if s <= 1: return 0 argument = get_argument(s) result = cost(argument) if s % 2 == 0: return add_one(result) return get_min(result)

The third step is: I want to decide which function to call afterwards in a helper function. Notice that we have a function which returns a function here!

...def get_after(s): if s % 2 == 0: return add_one return get_mindef cost(s): if s <= 1: return 0 argument = get_argument(s) after = get_after(s) # after is a function! result = cost(argument) return after(result) 

And you know, let's make this a little bit more general, and make the recursive case a bit more concise:

...def is_base_case(s): return s <= 1def base_case_value(s): return 0def cost(s): if is_base_case(s): return base_case_value(s) argument = get_argument(s) after = get_after(s) return after(cost(argument)) 

I hope it is clear that at every small refactoring we have maintained the meaning of the program. We're now doing a small amount of work twice; we have two tests for even space number per recursion, whereas before we had just one, but we could solve that problem if we wanted by combining our two helpers into one function that returned a tuple. Let's not worry about it for the sake of this exercise.

We've reduced our recursive method to an extremely general form:

  • If we are in the base case:
    • compute the base case value to be returned
    • return it
  • If we are not in the base case:
    • get the recursive argument
    • make the recursive call
    • compute the value to be returned
    • return it

Something important to notice at this stage is that none of the "afters" must themselves contain any calls to cost; the technique that I'm showing today only removes a single recursion. If you're recursing two or more times, well then, we'll need more special techniques for that.

Once we've got our recursive algorithm in this form, turning it into an iterative algorithm is straightforward. The trick is to think about what happens in the recursive program. As we do the recursive descent, we call get_argument before every recursion, and we call whatever is in after, well, after every recursion. That is, all of the calls to get_argument happen before all of the calls to every after. Therefore we can turn that into two loops: one calls all the get_arguments and makes a list of all the afters, and the other calls all the afters:

...def cost(s): # Let's make a stack of afters. Remember, these are functions # that take the value returned by the "recursive" call, and # return the value to be returned by the "recursive" method. afters = [ ] while not is_base_case(s): argument = get_argument(s) after = get_after(s) afters.append(after) s = argument # Now we have a stack of afters: result = base_case_value(s) while len(afters) != 0: after = afters.pop() result = after(result) return result

No more recursion! It looks like magic, but of course all we are doing here is exactly what the recursive version of the program did, in the same order.

This illustrates a point I make often about the call stack: its purpose is to tell you what is coming next, not what happened before! The only relevant information on the call stack in the recursive version of our program is what the value of after is, since that is the function that is going to be called next; nothing else matters. Instead of using the call stack as an inefficient and bulky mechanism for storing a stack of afters, we can just, you know, store a stack of afters.

Next time on FAIC, we'll look at a more advanced technique for removing a recursion in Python that very-long-time readers of my blog will recognize.

External Content
Source RSS or Atom Feed
Feed Location http://ericlippert.com/feed
Feed Title Fabulous adventures in coding
Feed Link https://ericlippert.com/
Reply 0 comments