5

I am trying to understand call stack for the below python recursive function. The function gives all the combinations for a given set.

def subsets(A):
    if not A:
        yield []
    else:
        for s in subsets(A[1:]):
            yield s
            yield [A[0]] + s
l1=[1,2,3]
print(list(subsets(l1)))

The program is generating the output as desired:

[[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]

However I am not able to visualize the call stack. How is it able to print [1,2,3]? To my understanding as follows

call stack 1 : values are : for s in [2,3], a[0] = 1, a = [1,2,3]
call stack 2 : values are : for s in [3], a[0] = 2, a = [2,3]
call stack 3 : values are : for s in [], a[0] = 3, a = [3]

Can some help me to understand call stack flow with s and a values?

1

3 Answers 3

1

When you call susbsets initially, argument A is [1, 2, 3]. The important thing to note is that the function will recurse repeatedly calling itself with arguments [2, 3], [3] and [] before it starts yielding values for the current argument, [1, 2, 3]. Then all these recursive calls return and we execute the the following code with A having the value [1, 2, 3]:

for s in subsets(A[1:]):
    yield s # produces [2, 3]
    yield [A[0]] + s # produces [1, 2, 3]

So we would expect the last two values yielded to be [2, 3], [1, 2, 3] within the containing list.

Sign up to request clarification or add additional context in comments.

Comments

1

This is a direct implementation of the mathematical definition of the powerset

An empty A has no subsets (yield []).

For the non-empty, you have two options

  1. keep the first element, and concatenate all possible subsets of the rest (yield [A[0]] + s)

  2. do not keep the first element, and return the possible subsets of the rest (yield s)

So, with A = [1, 2, 3] you have the union of [1] + subsets([2, 3]) and subsets([2, 3]). Similarly, subsets([2, 3]) is union of [2] + subsets([3]) and subsets([3]). finally, subsets([3]) is [] or [3]. That gives 3 steps, with 2 possible outcomes on each, giving the 8 possible combinations.

Comments

1

You call list() on a generator. This will call the generator again and again, until it is exhausted. Let's track the flow of execution. This can be a good exercise to understand generators. I'll format everything as a code block, so that I can use proper indentation to clarify the hierarchy of generator calls.

subsets([1, 2, 3]) is called, 
    so A is [1, 2, 3]. 
    This list is not empty, so the else block is executed.
    A[1:] is [2, 3], so to determine the first s,
    subsets([2, 3]) is called.
        Now A is [2, 3], so A[1:] is [3], so to determine s,
        subsets([3]) is called.
            Now A is [3], so A[1:] is [], so to determine s,
            subsets([]) is called.
                Now A is [], so the if block is executed.
                This yields [].
            The loop starts with s = [].
            This yields [] again.
        Now this loop starts, also with s = [],
        because this is what subsets([3]) has just yielded.
        So this yields [] as well.
    So subsets([2, 3]) has yielded [], 
    so this loop also starts with s = [].
    This yields [] yet again.
So subsets([1, 2, 3]) has yielded [], 
and now this generator is called again (because of list()), 
    picking up the action after the previously executed yield statement.
    So we reach the next statement: yield [A[0]] + s.
    This yields [1].
subsets([1, 2, 3]) is called again,
    picking up at the end of the first run through the for loop,
    so to determine the next s, 
    subsets([2, 3]) is called again, 
        picking up at yield [A[0]] + s.
        This yields [2].
    So the loop starts again, with s = [2].
    This yields [2].
subsets([1, 2, 3]) is called again,
    picking up at yield [A[0]] + s, with s = [2].
    This yields [1, 2].
subsets([1, 2, 3]) is called again,
    picking up at the end of the for loop,
    so to determine the next s, 
    subsets([2, 3]) is called again,
        picking up at the end of the for loop,
        so to determine the next s,
        subsets([3]) is called again, 
            picking up at yield [A[0]] + s.
            This yields [3].
        So the loop starts again, with s = [3].
        This yields [3].
    So the loop starts again, with s = [3].
    This yields [3].
subsets([1, 2, 3]) is called again,
    picking up at yield [A[0]] + s, with s = [3].
    This yields [1, 3].
subsets([1, 2, 3]) is called again,
    picking up at the end of the for loop,
    so to determine the next s, 
    subsets([2, 3]) is called again,
        picking up at yield [A[0]] + s, with s = [3].
        This yields [2, 3].
    So the loop starts again, with s = [2, 3].
    This yields [2, 3]. 
subsets([1, 2, 3]) is called again,
    picking up at yield [A[0]] + s, with s = [2, 3].
    This yields [1, 2, 3].
subsets([1, 2, 3]) is called again,
    picking up at the end of the for loop,
    so to determine the next s, 
    subsets([2, 3]) is called again,
        picking up at the end of the for loop,
        so to determine the next s, 
        subsets([3]) is called again, 
            picking up at the end of the for loop,
            so to determine the next s, 
            subsets([]) is called again, 
                picking up at the end of the if block,
                so we reach the end of the generator, 
                which means it is exhausted and yields nothing anymore.
            So there is no further iteration of the for loop,
            hence subsets([3]) is also exhausted.
        So subsets([2, 3]) is also exhausted.
    So subsets([1, 2, 3]) is also exhausted.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.