Python Stack Frames and Tail-Call Optimization | by Reza Bagheri | Towards Data Science

Recursion in computer science is a method of problem-solving in which a function calls itself from within its own code. This method is very useful and can be applied to many types of problems, however, it has a limitation. Functions use the stack to keep their local variables, and the stack has a limited size. So if the recursion is too deep you will eventually run out of stack space which is called a stack overflow. However, some compilers implement tail-call optimization, allowing unlimited recursion to occur without stack overflow. In this article, I will first talk about the Python stack frames, and then I will explain the concept of tail-call optimization and show you how it can be implemented in Python.
Stack Frames
Stack is a data structure with a LIFO (Last In First Out) order which has two principal operations:
push: adds an element to the stack
pop: removes the most recently added element
So the last element added or pushed to the stack is the first element to be removed or popped. The advantage of using the stack to store data is that memory is managed for you. Reading from and writing to stack is very fast, however, the size of the stack is limited. The Python interpreter uses a call stack to run a Python program. When a function is called in Python, a new frame is pushed onto the call stack for its local execution, and every time a function call returns, its frame is popped off the call stack. The module in which the program runs has the bottom-most frame which is called the global frame or the module frame. These frames keep the data that the function needs for its local execution which is the function arguments and its local variables.
For example, consider the following source code:
# Listing 1a = 1def f(x):    b = 2    return b+xy = f(a)print(y)
The module in which this program runs has the bottom-most frame in the call stack which is the module frame. The global variable a is stored in this frame. When we call f(a) a new frame for f is pushed on top of the call stack, and the argument of f and its local variable b are stored in there. When f returns its value, this frame is popped off the call stack (Figure 1).
In Python, you can easily access these frames. Python stores all the information about each frame of the call stack in a frame object. The frame object contains the frame address, the name of the file which it belongs to, the line number in the source code at which this frame was accessed, and the name of the function that it belongs to. We can use the inspect module to inspect frames.
We can use inspect.stack(context=1) to get all the frames that currently exist in the call stack. The optional argument context specifies the number of lines of code context to return, which some are lines of code centered around the current line in the source code. The current line is the line in the source code at which this frame was accessed or inspected, and the current line number is the number of this line in the source code. The frames in the call stack are dynamically changing as the program is being executed, and it is important to know at which line number the stack was inspected. The function:
inspect.stack(context=1)
returns a list of named tuples:
FrameInfo(frame, filename, lineno, function, code_context, index)
where frame is the frame object, filename is the name of the file that this frame belongs to,lineo is the current line number in the source code, function is the name of the function that this frame belongs to, code_context is the list of lines of context from the source code, and index is the index of the current line within that list.
Now let’s try this function. If you run:
>>>import inspect>>>inspect.stack()
in the Python console, you will get:
[FrameInfo(frame=<frame at 0x000001A97248EAD8, file ‘<stdin>’, line 1, code <module>>, filename=’<stdin>’, lineno=1, function=’<module>’, code_context=None, index=None)]
So the returned list only has one element which is a FrameInfo named tuple. Since we are at the module level, we only have the module level on the call stack and this list only has one element which corresponds to the module frame. The first element of the tuple is the frame object:
<frame at 0x000001A97248EAD8, file ‘<stdin>’, line 1, code <module>>
The frame address is there, and the file name is stdin. The frame was accessed at line number 1 since we only had one line of code, and it is clear that the frame belongs to <module>. It is important to note that if you run this code from a Python platform like Anaconda, you may get a much longer output. You will notice that there are more frames below the module frame and these are the modules that are being executed by Anaconda itself, and we don't need to be worried about them. Python console returns None for code_context, and index. To get their values you should run this code as a Python script in the terminal. For example, you can store the following code in Python script:
import inspectprint(inspect.stack())
and run it in the terminal. The output will be:
[FrameInfo(frame=<frame at 0x000002A7EEBFE9F8, file ‘t1.py’, line 2, code <module>>, filename=’t1.py’, lineno=2, function=’<module>’, code_context=[‘print(inspect.stack())\n’], index=0)]
Since the default value of the argument context in stack() is 1, it only shows that line of the code that inspected this frame and its index is also 0.
As mentioned before, the first element of this list is related to the current frame on top of the call stack. We can access it by inspect.stack()[0]. The Inspect module also provides a specific function for that:
inspect.currentframe()
which gives the frame object of the first element of the list (which is the current frame). So it is equivalent to inspect.stack()[0][0].
Now let’s try this function on the source code of Listing 1:
# Listing 2import inspecta = 1print(inspect.stack()[0], "\n")def f(x):    b=2    print(inspect.stack()[0:2], "\n")    return x+by = f(a)print(inspect.stack(3)[0])print(y)
The output will be:
FrameInfo(frame=<frame at 0x000001FDB52AC908, file '<ipython-input-34-3128af912523>', line 3, code <module>>, filename='<ipython-input-34-3128af912523>', lineno=3, function='<module>', code_context=['print(inspect.stack()[0], "\\n")\n'], index=0) [FrameInfo(frame=<frame at 0x000001FDB520E1C8, file '<ipython-input-34-3128af912523>', line 6, code f>, filename='<ipython-input-34-3128af912523>', lineno=6, function='f', code_context=['    print(inspect.stack()[0:2], "\\n")\n'], index=0), FrameInfo(frame=<frame at 0x000001FDB50C4938, file '<ipython-input-34-3128af912523>', line 8, code <module>>, filename='<ipython-input-34-3128af912523>', lineno=8, function='<module>', code_context=['y = f(a)\n'], index=0)] FrameInfo(frame=<frame at 0x000001FDB52AFD88, file '<ipython-input-34-3128af912523>', line 9, code <module>>, filename='<ipython-input-34-3128af912523>', lineno=9, function='<module>', code_context=['y = f(a)\n', 'print(inspect.stack(3)[0])\n', 'print(y)\n'], index=1)3
Initially, we added inspect.stack()[0] at the module level (since I am running it in Anaconda, I only get a slice of the returned list not to show the Anaconda frames which exist below the module frame). The head of the list returned by inspect.stack() shows the FrameInfo tuple for the current frame which is the module frame.
Next, we added print(inspect.stack()[0:2], "\n") inside the function f. When f is called, a new frame for f is pushed on top of the module frame, so the current frame which is shown at the head of the list corresponds to this function:
[FrameInfo(frame=<frame at 0x00000240610678E0, file '<ipython-input-237-6d513e166f46>', line 7, code f>, filename='<ipython-input-237-6d513e166f46>', lineno=7, function='f', code_context=['    print(inspect.stack()[0:2], "\\n")\n'], index=0), FrameInfo(frame=<frame at 0x00000240610B2378, file '<ipython-input-237-6d513e166f46>', line 9, code <module>>, filename='<ipython-input-237-6d513e166f46>', lineno=9, function='<module>', code_context=['y = f(a)\n'], index=0)]
Both the frame object and FrameInfo show the name of f. Below this frame is the module frame, so it is the next element of the list.
Finally, we placed print(inspect.stack(3)[0]) to get the FrameInfo for the current frame after calling the function. Now the function frame is popped off the call stack, so the current frame is the module frame again. Here we used context=3 in stack(). So the output is:
FrameInfo(frame=<frame at 0x000002405E787678, file '<ipython-input-237-6d513e166f46>', line 10, code <module>>, filename='<ipython-input-237-6d513e166f46>', lineno=10, function='<module>', code_context=['y = f(a)\n', 'print(inspect.stack(3)[0])\n', 'print(y)\n'], index=1)
Now we have 3 lines of code for the code context and the middle one with index=1 is the one that inspected the current frame.
Frame object attributes
The frame object has some useful attributes and we only discuss some of them here (you can check the inspect module webpage to see all of them):
f_back: This attributes gives the next outer frame object (which is the caller of this frame and sits below it in the call stack)
f_code: The code object being executed in this frame
f_locals: The local namespace seen by this frame. It is a dictionary that gives all the names and values of all the local variables that exist in this frame. This includes the arguments of the function that this frame belongs to). If this is the module frame, then all the global variables will be included.
For example, if we change Listing 2 to:
# Listing 3a = 1def f(x):    b=2    print(inspect.currentframe().f_code, "\n")    print(inspect.currentframe().f_back, "\n")    print(inspect.currentframe().f_locals)    return x+by = f(a)
We get:
<code object f at 0x00000240610A4D20, file "<ipython-input-236-6a2fc4ce42af>", line 3> <frame at 0x000002405E556C28, file '<ipython-input-236-6a2fc4ce42af>', line 9, code <module>> {'x': 1, 'b': 2}
So f_code returns the code object of f, and f_back returns its caller frame which is the module frame. Finally, f_locals returns a dictionary which contains the argument of the function (x) and its local value b and their values at the time that the frame was inspected.
Now using these attributes, I will define a new function print_frames that prints the local scope of the frames:
This function takes the list of FrameInfo tuples (returned by inspect.stack()) and prints the local scope of the current frame and all the frames below it until it reaches the module frame, and we will use this function to inspect the frames in the next sections. Here is an example of using this function:
# Listing 4a = 1def f(x):    b=2    print_frames(inspect.stack())    return b*xdef g(x):    return 2*f(x)y = g(a)------------------------------------------------------------Output:  [Frame 2 'f': {'x': 1, 'b': 2}]  [Frame 1 'g': {'x': 1}]  [Frame '<module>']
As you can see, when f is called by g, the current frame belongs to f and the frames of g and module are below that.
Stack frames in closures
As you saw in Listing 4, when a function calls another one, its frame remains below the frame of the called function. This is not true for closures. A closure is an inner function returned by its outer function. It also has an extended scope that encompasses the nonlocal variables of the outer function (you can refer to this article to learn more about closures). So when a closure is called, the frame of the caller function has been popped off the call stack. Let me give you an example:
# Listing 5def f(x):    def g(y):        def h(z):            print_frames(inspect.stack())            return x+z        print_frames(inspect.stack())        return h    print_frames(inspect.stack())    return g
Here we have two closure definitions inside f. Now if we write:
f(1)(2)(3)
The output will be:
  [Frame 1 'f': {'g': <function f.<locals>.g at 0x00000240610617B8>, 'x': 1}]  [Frame '<module>']  [Frame 1 'g': {'y': 2, 'h': <function f.<locals>.g.<locals>.h at 0x00000240610F1158>, 'x': 1}]  [Frame '<module>']  [Frame 1 'h': {'z': 3, 'x': 1}]  [Frame '<module>']4
So when f returns g, the frame of f is popped off the call stack and the frame of g is pushed on top of the module frame. The frame of g not only stores its argument (y) but also stores x which is the argument of f and a nonlocal variable to g. Similarly, when h is called, the frame of g has been popped off the call stack. The frame of h also stores the nonlocal variable x.
Inspecting stack frames with sys module
So far we used the inspect module to inspect the stack frames. We can also use the sys module to do the same thing. The function:
 sys._getframe(depth=0)
returns a frame object from the call stack. If optional integer depth is given, it returns the frame object that many calls below the top of the stack. For example, depth=1 returns the next outer frame object which is below the current frame. The default for depth is zero and it returns the current frame which is on top of the call stack. So we can rewrite Listing 3 using this function:
# Listing 6import sysa = 1def f(x):    b=2    print(sys._getframe().f_code, "\n")    print(sys._getframe().f_back, "\n")    print(sys._getframe().f_locals)    return x+by = f(a)
and get the same output of Listing 3. In addition, instead of sys._getframe().f_back we could write sys._getframe(1) to get the next outer frame object.
Recursive functions
In recursive functions, a function calls itself from within its own code. Here is a simple example. The factorial of a number n can be defined as:
factorial(n) = n × (n-1) × (n-2) × ... × 1
We can define a recursive function to calculate the factorial:
# Listing 7def fact(n):    if n==0:        return 1    else:        return n*fact(n-1)
The function fact(n) calculates the factorial of n by multiplying n by the factorial of n-1. When n is equal to 1 it simply returns 1, so n=1 defines the base case of the recursive function. The base case returns a value without making any subsequent recursive calls.
Now we can add print_frames to this function to see how the stack frames are being generated.
# Listing 8def fact(n):    if n==0:        print("fact({}) called:".format(n))        print_frames(inspect.stack())        print("fact({}) returned {}".format(n, 1))        return 1    else:        print("fact({}) called:".format(n))        print_frames(inspect.stack())        result = n*fact(n-1)        print_frames(inspect.stack())        print("fact({}) returned {}".format(n, result))        return result
We also want to see the return value of the function, so we first store the return value in result, print it, and then return it by the function. Let’s try it for n=3:
fact(3)-----------------------------------------------------------------Output:fact(3) called:  [Frame 1 'fact': {'n': 3}]  [Frame '<module>']fact(2) called:  [Frame 2 'fact': {'n': 2}]  [Frame 1 'fact': {'n': 3}]  [Frame '<module>']fact(1) called:  [Frame 3 'fact': {'n': 1}]  [Frame 2 'fact': {'n': 2}]  [Frame 1 'fact': {'n': 3}]  [Frame '<module>']fact(0) called:  [Frame 4 'fact': {'n': 0}]  [Frame 3 'fact': {'n': 1}]  [Frame 2 'fact': {'n': 2}]  [Frame 1 'fact': {'n': 3}]  [Frame '<module>']fact(0) returned 1  [Frame 3 'fact': {'n': 1, 'result': 1}]  [Frame 2 'fact': {'n': 2}]  [Frame 1 'fact': {'n': 3}]  [Frame '<module>']fact(1) returned 1  [Frame 2 'fact': {'n': 2, 'result': 2}]  [Frame 1 'fact': {'n': 3}]  [Frame '<module>']fact(2) returned 2  [Frame 1 'fact': {'n': 3, 'result': 6}]  [Frame '<module>']fact(3) returned 66
As you see, each time that the function calls itself a new frame for f is pushed on top of the previous frames until it reaches the base of the recursion (fact(0)). Then the base case returns 1 and its frame is popped off the call stack. Then fact(1) returns 1×1 and its frame is popped. The other remaining frames of fact are popped one by one until fact(3) returns the final value (6) and is popped off the call stack. So it is clear that for fact(3) we pushed 4 frames on top of the module frame to reach the base case. Generally, to calculate fact(n) a maximum of n+1 frames will be pushed on top of the stack. But what happens if n becomes a big number like 5000?
As mentioned before the size of the call stack is limited. So by increasing the value of n, we finally reach the limit of the call stack, and cannot push any more frames on top of that. This is called a stack overflow, and it means that we run out of memory to hold the frames in the call stack. To avoid the stack overflow, Python sets a limit for the total number of frames that the call stack can hold. The limit is 999 (the maximum number of frames that the call stack can hold). Now remember that fact(n) needs n+1 frames. In addition, the module frame should be included too. So, in fact, we need n+2 frames. As a result, the maximum value of n that we can use is 997 which needs 999 frames. Now let’s try n=998. If we store Listing 9 in a Python script and run it in the terminal:
# Listing 9def fact(n):    if n==0:        return 1    else:        return n*fact(n-1)print(fact(998))
we get this error:
Traceback (most recent call last):  File "fact.py", line 6, in <module>    print(fact(998))  File "fact.py", line 5, in fact    return n*fact(n-1)  File "fact.py", line 5, in fact    return n*fact(n-1)  File "fact.py", line 5, in fact    return n*fact(n-1)  [Previous line repeated 995 more times]  File "fact.py", line 2, in fact    if n==0:RecursionError: maximum recursion depth exceeded in comparison
which means that we have exceeded the limit. In a platform like Anaconda, you may have a higher limit for the total number of frames. For example, in my system, it is 3000.
You can also change this limit to be able to have more frames. The function:
sys.getrecursionlimit()
gives the current limit for recursion and the function
sys.setrecursionlimit(limit) 
is used to set the limit for the maximum number of frames that the call stack can hold.
Please note that that the limit for the maximum recursion depth only prevents the program from getting into stack overflow, and does not change the actual size of the call stack. So changing the limit should be done with care because a too-high limit can lead to the crash of Python.
Another solution is to decrease the number of frames that a recursive function needs to push and make it behave like a non-recursive function. In this way, the function never reaches that limit, and there will be no stack overflow no matter how big is the input data. As you will see in the next section, this is possible by using tail-recursion and tail-call optimization.
Tail-recursion
A recursive function is tail-recursive when the recursive call is the last thing executed by the function. For example, the function fact(n) in Listing 7 is not tail-recursive. In this function, the recursive call is fact(n-1). However, this is not the last thing this function does before returning the result. It multiplies fact(n-1) by n , and then returns the result. So multiplication by n does not allow it to be tail-recursive.
It is possible to modify fact(n) to be tail-recursive. Listing 10 shows a tail-recursive version of fact:
# Listing 10def fact1(n, acc=1):    if n == 0:        return acc    else:        return fact1(n-1, n*acc) fact1(4, 1)
This function has a second argument acc which plays the role of an accumulator. We initialize it with 1 when we call fact1. In each call of fact1, n is multiplied by acc, and when it reaches the base case, it only needs to return acc. Now we can add print_frames to this function to see how the frames are being generated. We also store the return value in result, and print it to see the return value of the function.
# Listing 11def fact1(n, acc=1):    if n == 0:        print("fact1({},{}) called:".format(n, acc))        print_frames(inspect.stack())        print("fact1({0},{1}) returned {1}".format(n, acc))        return acc    else:        print("fact1({},{}) called:".format(n, acc))        print_frames(inspect.stack())        result = fact1(n-1, n*acc)         print_frames(inspect.stack())        print("fact1({},{}) returned {}".format(n, acc, result))        return result
You should notice that this is not truly a tail-recursive function since the recursive call is not the last thing executed by the function. In fact, after executing the recursive call fact1(n-1, n*acc), we assign its returned value to result, then call print_function and print and finally, return result. However, we only add them for debugging purposes to show how this function is working, and we do not change the value of result after the recursion call, so there is no computation on the returned value. As a result, this function gives the same result of the tail-recursive function in Listing 10.
Now if we execute fact1(3,1) the output will be:
fact1(3,1) called:  [Frame 1 'fact1': {'n': 3, 'acc': 1}]  [Frame '<module>']fact1(2,3) called:  [Frame 2 'fact1': {'n': 2, 'acc': 3}]  [Frame 1 'fact1': {'n': 3, 'acc': 1}]  [Frame '<module>']fact1(1,6) called:  [Frame 3 'fact1': {'n': 1, 'acc': 6}]  [Frame 2 'fact1': {'n': 2, 'acc': 3}]  [Frame 1 'fact1': {'n': 3, 'acc': 1}]  [Frame '<module>']fact1(0,6) called:  [Frame 4 'fact1': {'n': 0, 'acc': 6}]  [Frame 3 'fact1': {'n': 1, 'acc': 6}]  [Frame 2 'fact1': {'n': 2, 'acc': 3}]  [Frame 1 'fact1': {'n': 3, 'acc': 1}]  [Frame '<module>']fact1(0,6) returned 6  [Frame 3 'fact1': {'n': 1, 'acc': 6, 'result': 6}]  [Frame 2 'fact1': {'n': 2, 'acc': 3}]  [Frame 1 'fact1': {'n': 3, 'acc': 1}]  [Frame '<module>']fact1(1,6) returned 6  [Frame 2 'fact1': {'n': 2, 'acc': 3, 'result': 6}]  [Frame 1 'fact1': {'n': 3, 'acc': 1}]  [Frame '<module>']fact1(2,3) returned 6  [Frame 1 'fact1': {'n': 3, 'acc': 1, 'result': 6}]  [Frame '<module>']fact1(3,1) returned 66
You can see that when this recursive function reached its base case, acc is equal to factorial(3). Then the same value is returned to the caller functions fact1(1,6), fac1(2, 3) ... Finally fact1(3,1) returns acc as the final result of this function. So when it reaches the base case, it does not need to return the result to the previous callers, and the function execution can be terminated by returning acc.
This is completely different from the output fact(n) in Listing 8. That function wasn’t tail-recursive, so when it reaches the base case (f(0)), it only gets the base case return value which is 1. This value needs to be returned to its caller f(1) to be multiplied by 1 and then to f(2) to be multiplied by 2 and so on. 1,2, ... are the arguments of f and are stored in its frame, so we need the previous frames to be able to access these arguments when we want to multiply them by the returned value of the function.
In fact1 which is tail-recursive, we don’t need to hold the previous frames on the call stack. We only need the current frame until we reach the base case and then we can terminate the function. So a tail-recursive function does not need the frames below its current frame. However, as the output shows Python still executes it like a recursive function and keeps all the frames. We need Python to discard the previous frame when a tail-recursive function calls itself.
Tail-call optimization
Tail-call optimization is a method which allows infinite recursion of tail- recursive functions to occur without stack overflow. Tail-call optimization converts a recursive call into a loop. The recursive call is replaced with a code that:
First replaces the arguments of the function with the values of the corresponding arguments of the recursive call
Jumps back to the beginning of the function.
Tail-call optimization can only be used for tail-recursive functions. The reason is that the new code jumps back to the beginning of the function after the recursive call and ignores any statements after that.
Let me show it using an example. Suppose that we want to apply the tail-call optimization to the fact1 function in Listing 10. We can do it using the pseudo-code in Listing 12.
# Listing 12def fact1(n, acc=1):    START:     if n == 0:        return acc    else:        #  return fact1(n-1, n*acc) is replaced with:        acc = n * acc        n = n -1        goto START
Here we replace the recursive call fact1(n-1, n*acc) with a new code. If n is not equal to zero, the arguments of the function n and acc will be replaced with the values of the corresponding arguments of the recursive call n-1, n*acc, and then it jumps to the beginning of the function. This is a pseudo-code since Python does not have a goto keyword. However, we can implement it using a while loop:
# Listing 13def fact2(n, acc=1):    while True:        if n == 0:            return acc        else:            acc = n * acc            n = n - 1
Tail-call optimization is not supported in Python, but we can apply it by changing the code inside the function, however, we prefer to do it automatically using a decorator and without changing the function’s code. We can do it in two different ways which are explained in the next sections.
Tail-call optimization using stack frames
The first method uses the inspect module and inspects the stack frames to prevent the recursion and creation of new frames. Listing 14 shows a decorator which can apply the tail-call optimization to a target tail-recursive function:
Now we can decorate fact1 using tail_rec:
@tail_recdef fact1(n, acc=1):    if n == 0:        return acc    else:        return fact1(n-1, n*acc)        fact1(4)
Let me explain how this decorator works. When we decorate fact1 with tail_rec, the variables rec_flag, targs and tkwargs are initialized. Now fact1 is referring to the definition of helper. So when we call fact1(4), helper is called (here we call it the first helper). The variables rec_flag, targs and tkwargs are the nonlocal variables of helper, so it can access them. helper first gets the current frame. Then it checks if f.f_code is equal to f.f_back.f_back.f_code or not. As mentioned before, the attribute f_code refers to the code object being executed in the frame, and it indicates which function has created this frame. So f.f_code refers to the code object of the current frame which is the code object of helper. In addition, f.f_back.f_back refers to the frame which is two levels below the current frame (the grandparent of the current frame).
Initially, the frame of helper is on top of the module frame, and the module frame is the bottom-most frame, so the grandparent of the current frame is None and the test in the if block fails. (In a platform like Anaconda the grandparent is not None, but its code object is different from helper, so the test fails again). So it goes into the else block and executes the while loop. Inside the while loop func(*args, **kwargs) is called. func refers to the definition of fact1. So fact1(4) is executed and since it is a recursive function, it calls fact1(3, 4).
However, fact1 is a decorated function, so it is referring to helper. So calling fact1(3, 4) executes helper again as shown in Figure 2 (we call it the second helper). This time the current frame belongs to the second helper and the grandparent of the current frame is the first helper. So both frames have the same code object (belong to the same function) and the condition in the if section is true. Now the arguments of helper are stored in the nonlocal variables targs and tkwargs. In addition, the flag rec_flag is set to True.
In fact, the if statement detects the start of recursion in the target function by checking the code object of the frames. When the start of recursion is detected, the creation of additional frames should be suppressed. So the second helper stores the arguments of the recursive call and changes the rec_flag to signal the presence of a recursive call and then returns None. Its caller is fact1 which returns the same to the first helper function and its frame is also popped off the call stack (Figure 2).
In the first helper, the condition of if statement is true, so it assigns the values of targs and tkwargs to args and kwargs. The while loop calls func(*aregs, **kwargs) again, so fact1(3, 4) is called again with the arguments of its last recursive call. However, this is not a recursive call. In fact when fact1(3, 4) is called, there are no previous fact1 frames on the call stack.
This process continues and each time, when fact1 tries to call itself recursively, helper turns it into a non-recursive call with the same arguments. Finally, when we reach the base case (n=0), there will be no recursive call and fact1 returns acc which is stored in result inside the while loop. Since no recursive call happened, rec_flag remains False. When rec_flag is Fasle, the else block is executed and helper returns result.
Let’s see what happens if we use a non-tail recursive function with this decorator. For example, we can use the factorial function in Listing 7:
@tail_recdef fact(n):    if n==0:        return 1    else:        return n*fact(n-1)print(fact(3))
You will get this exception as the output:
Exception: It is possible that the decorated function is not tail recursive
So if you decorate a function that does some computation on the returned value of the recursive call, it can detect it. The statement result=func(*args,**kwargs) is inside a try block which captures a TypeError exception. If you decorate a non-tail-recursive function with this decorator, a TypeError exception will be raised. The reason is that when helper detects the recursion, it returns None, so in the last line of fact, fact(n-1) also returns None. When the function tries to multiply n by None, a TypeError is raised. Of course, it is possible that you have a TypeError somewhere else in your function, so getting such an exception does not necessarily mean that you have a function which is not tail-recursive.
It is important to note that if you use a function which is not tail-recursive but does no computation on the returned value of the recursive call, the decorator won’t complain about it. For example, it can be used to decorate the following function without error:
# Listing 15def fact2(n, acc=1):    if n == 0:        return acc    else:        result = fact2(n-1, n*acc)        return result
There is a different version of this decorator in which the detection of recursion is signaled by raising an exception, and no nonlocal variables are used inside the decorator. However, in that case, if you use the decorator for a non-tail-recursive function, it won’t detect it and returns the wrong result instead.
Tail-call optimization using bytecode injection
We can also prevent recursion by changing the bytecode of a function (if you are not familiar with bytecode injection, you can refer to this article). Remember the pseudo-code in Listing 12 in which we replaced the recursive call with a new code. The new code first replaces the arguments of the function with the values of the corresponding arguments of the recursive call and then jumps back to the beginning of the function. We can write a decorator to change the bytecode of a target function in a similar way. Let’s see how the bytecode of fact1 in Listing 10 looks like. We use dis module to disassemble the bytecode of this function:
import disdis.dis(fact1)
The output is:
  2           0 LOAD_FAST                0 (n)              2 LOAD_CONST               1 (0)              4 COMPARE_OP               2 (==)              6 POP_JUMP_IF_FALSE       12  3           8 LOAD_FAST                1 (acc)             10 RETURN_VALUE  5     >>   12 LOAD_GLOBAL              0 (fact1)             14 LOAD_FAST                0 (n)             16 LOAD_CONST               2 (1)             18 BINARY_SUBTRACT             20 LOAD_FAST                0 (n)             22 LOAD_FAST                1 (acc)             24 BINARY_MULTIPLY             26 CALL_FUNCTION            2             28 RETURN_VALUE             30 LOAD_CONST               0 (None)             32 RETURN_VALUE
In line 5, the function calls itself. The callable object of fact1 is pushed onto the stack using LOAD_GLOBAL. Then the arguments of the function are pushed onto the stack one by one with the right-most argument on top. Finally, the function is called using CALL_FUNCTION. Its oparg (its argument which is 2) indicates the number of positional arguments. It first pops all the arguments and the callable object off the stack, and then calls the callable object with those arguments, and finally pushes the return value on top of the stack. The instruction RETURN_VALUE returns the top of the stack to the caller of the function.
Now we should replace CALL_FUNCTION and RETURN_VALUE with some new bytecode instructions. The new code should pop all the arguments of the recursive call off the stack and store them in the arguments of the current function and then jump back to the beginning of the function. So it should look like this:
  2     >>    0 LOAD_FAST                0 (n)              2 LOAD_CONST               1 (0)              4 COMPARE_OP               2 (==)              6 POP_JUMP_IF_FALSE       12  3           8 LOAD_FAST                1 (acc)             10 RETURN_VALUE  5     >>   12 LOAD_GLOBAL              0 (fact1)             14 LOAD_FAST                0 (n)             16 LOAD_CONST               2 (1)             18 BINARY_SUBTRACT             20 LOAD_FAST                0 (n)             22 LOAD_FAST                1 (acc)             24 BINARY_MULTIPLY             26 STORE_FAST               1 (acc)             28 STORE_FAST               0 (n)             30 POP_TOP             32 JUMP_ABSOLUTE            0             34 LOAD_CONST               0 (None)             36 RETURN_VALUE
Here CALL_FUNCTION 2 is removed at offset 26. At this point, all the arguments of the recursive call are on top of the stack. So they are popped one by one and stored in the arguments of fact1 using STORE_FAST. Then the callable object of the function which was below them is popped off the stack using POP_TOP since we don’t need it anymore. After that, it jumps back to the beginning of the function at offset 0 using JUMP_ABSOLUTE 0. So the recursive call is converted into a loop.
The decorator in Listing 16, takes a tail-recursive function and changes its bytecode in a similar way.
It uses two functions disassemble_to_list and assemble which are defined in a module named tailrec (these functions have been discussed in detail in this article). disassemble_to_list takes the code object of the function and disassembles the byte code into some human-friendly instructions. It returns a list in which each element shows a bytecode instruction and its argument. This list can be easily manipulated to replace the bytecode instructions with new instructions. After modifying the instructions, assemble converts this list into the bytecode. A new code object with this new bytecode is created and assigned to the target function.
Inside the decorator, there is a while loop that searches the bytecode list to find all the recursive calls. In fact, it searches for a CALL_FUNCTION or CALL_FUNCTION_KW instruction which is followed by a RETURN_VALUE. CALL_FUNCTION_KW is used for functions that have keyword arguments. In that case, the top element on the stack contains a tuple of keyword argument names which have been pushed by an instruction before them. Below that are the keyword arguments and below them are the positional arguments, with the right-most parameter on top. Below the arguments is the callable object to call.
When it finds a recursive call, it adds the STORE_FAST instructions to store the recursive call arguments in the arguments of the functions. It uses the co_varnames attribute of the code object to get the name of the arguments of the function. Then a JUMP_ABSOLUTE 0 will be added to jump back to the beginning of the function. Since we are adding more bytecode instructions to the original functions, we should also update the instructions which have a relative or absolute jump target (their opcode is in dis.hasjrel or dis.hasjabs) and we should correct the offset of their jump target.
This decorator can also detect a function which is not tail-recursive and raise an exception for that. However, unlike the decorator in Listing 14, the function should be truly tail-recursive and no instructions are allowed after the recursive call. So the function in Listing 15 cannot be decorated using this decorator and raises an exception.
The tail-call optimization can be done using either of these decorators, however, the second one is much faster. Let’s compare their performance. We first define two setup functions s1 and s2 to calculate the factorial of 15 using each decorator.
@tail_recdef fact_1(n, acc=1):    if n == 0:        return acc    else:        return fact_1(n-1, n*acc)    @tail_recursiondef fact_2(n, acc=1):    if n == 0:        return acc    else:        return fact_2(n-1, n*acc)def s1():    return fact_1(15)def s2():    return fact_2(15)
Now we can compare the runtime of these setups using the timit module:
import timeitt1 = timeit.timeit(s1, number=100000)t2 = timeit.timeit(s2, number=100000)print("Running time (tail-call optimization using stack frames)=", t1)print("Running time (tail-call optimization using bytecode injection)=", t2)
The output is:
Running time (tail-call optimization using stack frames)= 6.038Running time (tail-call optimization using bytecode injection)= 0.367
As you see the decorator which is using the bytecode injection is more than 10 times faster. The reason is that it eliminates the function calls and replaces them with a loop.
I hope that you enjoyed reading this article. All the code listings of this article are available for download at: