함수 안에서 yield를 본 적이 있을 겁니다. 함수 안에서 이것을 쓰게 되면, 제너레이터가 됩니다. 이 문서에서는 어떻게 제너레이터를 쓰고 왜 쓰는지에 대해서 언급하지는 않습니다. 함수 안에서 yield를 썼을 때, 실행 문맥에 어떤 것을 저장하고 있길래, yield가 나타나면, 실행을 양보하고, 다시 foo를 호출하면, 그 다음 위치부터 실행시킬 수 있는지에 대해서 간단하게 고찰해 보겠습니다.

 


 main에서 foo를 호출했을 때를 생각해 봅시다. 이 때 foo(a)의 위치를 2, 그 다음 줄에 위치해 있는 a = a + 1을 위치 3이라고 해 보겠습니다. foo(a)를 호출하고, 리턴이 되면, 위치 3에 있는 a = a + 1이 수행됩니다.

 

 

 그림으로 그려 보면 위와 같습니다. 이는, 함수를 호출할 때 복귀할 수 있는 정보 (예를 들자면 복귀 주소)를 가지고 있기 때문입니다. 예를 들자면, 위 그림에서는 복귀할 때, 2번 다음에 있는 3번으로 가라는 정보가 저장 될 겁니다. generator의 yield가, 실행 문맥이 보존된다는 이야기도 똑같이 적용할 수 있어요.

 

 

 단지, 문맥이 보존되는 대상이 caller였는지, yield가 있는 함수인지가 다를 뿐입니다.

 


 예제 하나를 보도록 하겠습니다. foo 함수가 있는데요. yield i와 yield 2*i가 있어요. 제너레이터라 할 수 있어요.

 

 

 11번째 줄에서 foo의 byte 코드를 출력합니다. 다음에, 12번째 줄을 보면 for x in gen이 보이는데요. 순회 가능하다는 이야기입니다.저는 13번째 줄에 브레이크 포인트를 걸면서 gen 안에 있는 어떤 값들이 변하는지 확인해 보도록 하겠습니다.

 


 특정 시점에, gen 내부 구조를 보면 아래와 같습니다.

 

 파란색으로 변한 것들을 보시면 되는데요. gi_frame에 stack이 있습니다. 뭔지 모르겠지만, frame: foo [main.py:7]을 담고 있습니다. 7번째 줄까지 실행 되고 frozen이 된 상황을 저장하고 있어요. 여기까지만 봐도, 어? 실행 문맥을 저장할 때, 어느 줄까지 실행했는지 저장하는 구나 정도를 알 수 있습니다.

 

 그런데, 밑에 보면, f_lasti랑 f_lineno가 있어요. 그리고 f_locals가 있어요. 이 중에, 위의 2개는 어디까지 실행 되었는지 알려주는 역할을 합니다. 그리고 나머지 하나는 지역 변수를 의미하는데요. 5번째 줄에 for i in range(5)가 있었어요.

 

 

 i값이 0이다라는 정보 또한 저장하고 있는데요. 이것도 함수가 실행하는 데 필요한 요소 중 하나입니다. i가 0일 때와 3일 때, 4일 때, 5일 때 상황이 달라질 수 있기 때문입니다. pc 값 뿐만이 아니라 지역 변수들도 저장을 하고 있는 셈이 됩니다.

 

 

 파이참에서는 F9를 누르면 다음 break point까지 실행시킵니다. 다음 break point는 for loop를 한 번 더 돌리고 print문을 실행하기 직전입니다. 이 때 어떻게 변했는지 보도록 하겠습니다.

 

 그러면, i값에 대한 정보도 바뀌었고, stack frame 정보도 바뀌었습니다. 그리고 f_lasti와 f_lineno도 바뀌었는데요. f_lasti가 14, 24를 왔다갔다 함을 눈치챌 수 있습니다. foo를 disassmble한 결과를 봅시다.

 

 

 14와 24를 찾아보시면, YIELD_VALUE를 볼 수 있는데요. lasti가 14와 24를 왔다갔다 한 상황과 match를 시켜 봅시다. 그러면, lasti가 무엇을 의미했는지 단번에 아실 수 있을 겁니다. 바이트 코드를 어디까지 실행했는지를 나타냅니다. 즉, 제너레이터가 yield를 만나서, 함수의 바깥으로 값을 전달하고 frozen 상태가 될 때, 지역 변수, stack frame 뿐만이 아니라, 어디까지 실행이 되었는지에 대한 것도 저장하고 있습니다.

 

 그렇기에, 다시 next 같은 것이 실행되었을 때, 다음 위치부터 실행을 이어갈 수 있게 됩니다.

 

 제너레이터를 돌면서 뽑은 값들은 위와 같습니다.