개인공부/Python

[Python][이게 왜 안돼?] yield 키워드와 Generator

양을 좋아하는 문씨 2025. 2. 24. 19:08

예전에 회사에서 만들었던 프로젝트를 재구성하다가 몰랐던걸 발견해서 기록함.


  1. OpenCV로 캠영상을 읽은 뒤 웹(Flask)으로 스트리밍하는 모듈을 만드는 중이었다.
  2. 캠을 읽는 단위는 클래스 단위로 만들어진다. 즉, 클래스 인스턴스의 개수 == 연결된 캠의 개수.
  3. 여기서 싱글쓰레딩 작업 말고 멀티쓰레딩, 멀티프로세싱를 적용해가며 캠 fps를 줄이는 작업을 했었음.

어쨌든 위와 같은 상황에서 다음과 같은 코드를 작성했다.

# 실행 메서드
def run(self, method: str, test: bool):

    ...
    
    while True:
        reading_success, frame = self.cap.read()
        if not reading_success: # 비디오 송출값 없음
            
            ...
            
            self.cap.release()
            
            ...
            
            break
            
        ...
        
        if test: # 개발 환경에서 테스트시 진입
            cv2.imshow(f"CAM {self.cam_index}", frame_set[self.click_cnt % 3])
            key = cv2.waitKey(50)
            if key == 27:
                self.cap.release()
                
                ...
                
                break
        else: # 웹으로 스트리밍할 때 진입
            encoding_success, buffer = cv2.imencode('.jpg', frame)
            if not encoding_success:
                continue
            frame_bytes = buffer.tobytes()
            yield ...

자잘한 부분은 제외한 대강의 흐름은 이렇다.

  1. Cam 인스턴스를 만든다.
  2. cam 인스턴스의 run 메서드를 통해 실행한다.
  3. while문에 진입하고, test 변수의 값에 따라 분기된다.
  4. 만약 개발환경이면 imshow로 창에 캠영상이 보여진다. 그게아니면 매 이미지마다 인코딩 > 바이트 변환 > yield 되면서 웹에 전달된다.

어쨌든 내 의도는 개발환경과 릴리즈 환경을 조건문으로 나누고 싶었던 것이다.

근데 저렇게 했는데 작동하지 않았다...

삽질하면서 봤는데 어떤 에러가 나는게 아니고, run 메서드에 진입 자체를 하지 않고 있었다.

왜 그런지 찾아봤는데, 원인은 맨 밑의 yield 때문이었다.


파이썬에서는 yield가 포함되면, 그것이 실제로 사용되지 않더라도 그 함수는 자동으로 제너레이터 함수로 취급된다.

즉 아래와 같은 코드가 있다고 하자.

def a():
    print('a')
    return "ret_a"

def b():
    print('b')
    if False:  # 절대 실행되지 않음.
        yield "gen_b"
    return "ret_b"

res_a = a()
print(res_a)
res_b = b()
print(res_b)
print(type(res_a))
print(type(res_b))

>>> a
>>> ret_a
>>> 
>>> <generator object b at 0x00000159AB145B40>
>>> <class 'str'>
>>> <class 'generator'>

위의 코드에서 `res_a`를 보면 a라는 글자가 출력되고 ret_a도 따라서 출력된다.

그런데 `res_b`의 경우는 안에 분명 `print('b')`가 있음에도 아무것도 출력하지 않고, `print(res_b)`를 했더니 ret_b 대신 제너레이터 객체의 메모리 주소를 출력하고 있다. 심지어 타입도 제너레이터가 되어있다.

즉, 함수 b는 yield 키워드가 들어감으로써 자동으로 제너레이터가 되어버렸고, `res_b = b()`를 통해서 함수를 실행하는 것이 아니라 (클래스 인스턴스처럼)새로운 제너레이터 객체가 만들어져 변수에 할당되어버린 것이다...

그래서 `res_b`를 그대로 출력하면 그 제너레이터 객체의 메모리 주소가 나오게 된다.


그렇다면 맨 위의 코드가 동작하지 않았던 이유는 알아냈다. 제너레이터 객체가 되어버렸기 때문에 꺼내 쓰는(소비하는) 코드가 없으면 그냥 안쓰고 반응 없이 지나가버린다.

이와 관련하여, 안쓰고 지나가는 이유에 대해서는 예전에 포스팅했던 지연 평가(lazy evaluation) 개념에서 이유가 나온다.

https://mhd329.tistory.com/53

 

Python : map()에 대한 공부 이것저것.

알고리즘 문제를 풀다가 map을 쓸 일이 생겼다.상황은 이러하다. 1. list1 에 있는 내부 요소들을 list2 로 옮겨야 된다.2. 방법이야 여러 가지가 있겠지만, map을 써서 옮길 수 없을까 생각했다.3. appen

mhd329.tistory.com


그렇다면 왜 이렇게 설계했을까? yield가 들어갔다고 갑자기 제너레이터가 되는것이 조금 특이해서 찾아봤다.

이유는 다음과 같다고 한다.

 

  1. 컴파일 시점에 함수의 성격을 명확히 결정할 수 있음.
  2. 코드 분석을 단순화할 수 있음.
  3. Python의 "명시적인 것이 암시적인 것보다 낫다"는 철학과 부합.

내가 봤을땐 위의 세 가지 이유가 모두 결이 같은 것 같다. `yield` 라는 키워드가 포함되어 있으니 무조건 제너레이터로 생각하면 편하니까 저렇게 설계한 것 같다.

 


그럼 그 값을 어떻게 꺼내 쓸 수 있을까?

제너레이터 객체는 원본 함수의 코드와 현재 자신의 실행 상태를 포함하고 있다.

map과 마찬가지로 지연 평가 객체이기 때문에 실제 결과값을 얻으려면 생성된 객체를 소비해주는 코드가 필요하다.

이때는 아래와 같이 next() 함수를 통해 실제 값을 소비할 수 있다.

# 제너레이터 정의.
def number_generator():
    for i in range(5):
        yield i
    return "generator end"

# 제너레이터 만들기.
gen = number_generator()

# 제너레이터 값을 모두 사용할 때 까지 반복.
while True:
    try:
        value = next(gen)
        print(value)
    except StopIteration as e: # 모두 다 사용했다면 함수의 반환값을 사용.
        print(f"제너레이터 소진됨: {e.value}")
        break
        
>>> 0
>>> 1
>>> 2
>>> 3
>>> 4
>>> 제너레이터 소진됨: generator end