Search code examples
pythonreturn

Why doesn't execution stop when 'return' statement is encountered?


While I was reading this code:

def factorial(number):
    if not isinstance(number, int):
        raise TypeError("Sorry. 'number' must be an integer.")
    if number < 0:
        raise ValueError("Sorry. 'number' must be zero or positive.")
    
    def inner_factorial(number):
        if number <= 1:
            return 1
        return number * inner_factorial(number - 1)
    return inner_factorial(number)

I couldn't understand this part of the code:

    def inner_factorial(number):
        if number <= 1:
            return 1
        return number * inner_factorial(number - 1)
    return inner_factorial(number)

I mean, doesn't Python stop the code, when it sees the statement return? I want to know how the return number * inner_factorial(number - 1) code line works.


Solution

  • The line you have highlighted belongs to inner function. If it helps, you can think them separately, as follows.

    def inner_factorial(number):
        if number <= 1:
            return 1
        return number * inner_factorial(number - 1)  # 1
    
    def factorial(number):
        if not isinstance(number, int):
            raise TypeError("Sorry. 'number' must be an integer.")
        if number < 0:
            raise ValueError("Sorry. 'number' must be zero or positive.")
        
        return inner_factorial(number)  # 2
    

    The factorial function just returns inner_factorial with the same value passed in (Line #2). Then, inner_factorial is called and it returns itself, which means it is a recursive call. inner_factorial will be calling itself until the number becomes less than or equal to 1.


    How does the recursion work?

    When we run factorial(4), the values are being pushed onto the Call Stack (The Call Stack is the stack data structure that stores information about the active subroutines of a computer program. The Call Stack is important because each task can have it’s own separate stack. This means that each function called can be active simultaneously for different tasks doing different things. Another advantage is that a Call Stack supports recursive calls automatically.) in the program and there will be a new inner_factorial function pushed onto the Call Stack until inner_factorial returns 1. To see that, one can use debugger (I use PyCharm's) with break point in the return statement of inner_factorial function and use step into button. So, with factorial(4), we populate 4 new inner_factorials.

    enter image description here

    If I continue clicking in step into button, these inner_factorials are being removed one by one. (Being popped off the Call Stack which is why it is happening in reverse order from when it was pushed onto the Call Stack)

    Another thing is whether or not this multiplication will be done at once, or sequentially. To test this, instead of using multiplication with *, I create a function multiply to reveal what is going on under the hood. It appears that multiplication is done as follows: 2x1 → 3x2 → 4x6. This behavior can be illustrated by running the script below:

    import time
    
    
    def inner_factorial(number):
        if number <= 1:
            return 1
        time.sleep(1)
    
    
        def multiply(a, b):
            print(f"multiplying {a} with {b}.")
            time.sleep(1)
            return a * b
        return multiply(number, inner_factorial(number - 1))  # <- change is in here.
    # where multiply (number being popped of the call stack, return a*b)
    
    # multiplying 2 with 1.
    # multiplying 3 with 2.
    # multiplying 4 with 6.
    # 24
    
    inner_factorial(4)
    

    If I misinterpret any part and explained wrong, please let me know.