[정리] yield 의 동작 방식

yield

yield 는 함수 안에서 값을 반환하지만, 함수의 실행 상태를 유지한 채로 반환을 중단한다.

yield를 만나면, 값을 반환하고, 그 함수는 "일시정지" 한 상태가 되어 나중에 다시 호출되면 그 지점부터 실행을 이어나가게 된다. 

 

yield 는 제너레이터 함수를 정의하거나 이터레러블 객체에 yield를 위임하는 데 사용되며, 제너레이터 함수는 호출될 때 제너레이터 객체를 반환한다. 이 객체는 이터레이터 처럼 next()를 호출할 때마다 값을 하나씩 반환한다. 

 

def yield_test():
    yield 1
    yield 2
    yield 3

gen = yield_test()

print(type(gen))  # <class 'generator'>

print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3
print(next(gen))  # StopIteration...

 

yield 함수는 generator 타입이며, next 함수로 iterable 한 객체를 호출하여 값을 가져올 수 있다. 일반적인 함수의 경우 값을 return 받을 때 함수를 종료하고, 값을 반환하는 형태를 취하지만  yield의 경우 generator 형태로 값을 순차적으로 반환해주는 형태로 선언한다.

return 과의 차이 

  • return : 함수가 실행되고 완료하고 값을 반환하며, 그 후 함수의 상태는 완전히 소멸
  • yield : 함수를 호출할 때마다 값을 하나씩 반환하고, 함수의 실행 상태를 저장한다. 나중에 함수가 다시 호출되면, 저장된 상태에서 이어서 실행한다.

예를 들면 이런 식이다

def data_generator():
    for data in ['A', 'B', 'C', 'D', 'E']:
        yield data  # 데이터 하나씩 생성
        print(f"yield 다음 줄에서 {data}출력")

for data in data_generator():
    print(f"yield 문을 만나면 {data} 는 여기로 옵니다")
    

# yield 문을 만나면 A 는 여기로 옵니다
# yield 다음 줄에서 A출력
# yield 문을 만나면 B 는 여기로 옵니다
# yield 다음 줄에서 B출력
# ...

이는 말 그대로 yield문에서 "일시정지" 된 것이며, 함수가 다시 호출되면 일시정지 된 지점 다음부터 실행한다.

 

 

예제 : 주어진 list 에서 짝수를 반환하는 함수 

 

- yield 를 이용할 때 : 

numbers_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

def sol(nums) : 
    for num in nums :
        if num %2 == 0 :
         yield num


for n in sol(numbers_list) :
   print(n, end = "\n")

 

- list 를 이용할 때 :

numbers_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

def sol(nums):
    result = []
    for num in nums:
        if num % 2 == 0:
            result.append(num)
    return result

for n in sol(numbers_list):
    print(n, end="\n")

 

 

두 방법 모두 시간복잡도는 O(n)으로 동일하나, 제너레이터는 메모리 사용량이 적고, 지연 평가를 통해 필요할 때만 값을 생성하므로 더 효율적이다. 

 

데이터셋의 크기가 작거나, 전체 결과를 한 번에 처리할 필요가 있는 경우에는 일반 리스트를, 

데이터셋의 크기가 크고 메모리 사용량을 최소화 하고자 할 때는 제너레이터가 더 나은 선택임을 알 수 있다.