[ python ] 07. 제너레이터, 이터레이터 및 비동기 프로그래밍_(2)

박찬영·2024년 5월 15일

파이썬 클린 코드

목록 보기
17/19
post-thumbnail

07. 제너레이터, 이터레이터 및 비동기 프로그래밍

코루틴(coroutine)

코루틴의 핵심은 특정 시점에 실행을 일시 중단했다가 나중에 재시작할 수 있는 함수를 만드는 것이다. 이런 기능 덕분에 프로그램은 다른 코드를 디스패치하기 위해 기존 코드를 중지했다가, 나중에 다시 원래의 위치에서 재시작할 수 있다.

이전에 공부했던 제너레이터는 파이썬에서 자동으로 제공하는 기능에 의해 코루틴으로 활용할 수도 있다. 코루틴을 지원하기 위한 기본 메서드는 다음과 같다.

  • .close()
  • .throw(ex_type[, ex_value][, ex_traceback]])
  • .send(value)

파이썬은 코루틴을 생성하기 위해 제너레이터를 활용한다. 제너레이터는 중지 가능한 객체이므로, 자연스럽게 코루틴이 되기 위한 좋은 성질을 가지고 있다. 그러나 제너레이터만으로 코루틴을 만들기에 충분하지 않아서 위 메서드가 추가되었다. (코드를 일시 중단하는 것만으로는 충분하지 않고 그것과 통신하는 수단이 필요하기 때문)

이제 각 메서드에 대해서 알아보자

제너레이터 인터페이스의 메서드

close()

이 메서드를 호출하면 제너레이터에서 GeneratorExit예외가 발생한다. 이 예외를 따로 처리하지 않으면 제너레이터가 더 이상 값을 생성하지 않으며 반복이 중지된다.

이 예외는 종료 상태를 지정하는데 사용할 수 있다. 다음 예제에서 코루틴을 사용하여 데이터베이스 연결을 유지한 상태에서 한 번에 모든 레코드를 읽는 대신에 특정 크기의 페이지를 스트리밍하는 것을 보자

def stream_db_records(db_handler):
  try:
    while True:
      yield db_handler.read_n_records(10)
  except GeneratorExit:
    db_handler.close()

제너레이터를 호출할 때마다 데이터베이스 핸들러에서 얻은 10개의 레코드를 반환하고, 명시적으로 반복을 끝내기 위해 close()를 호출하면 데이터베이스 연결도 함께 종료한다.

제너레이터에서 작업을 종료할 때는 close() 메서드를 사용한다.

이 메서드는 리소스를 정리하기 위해 사용하는 것으로, 컨텍스트 관리자를 사용하지 않았거나 하여 자동으로 정리가 어려운 경우에 수동으로 리소스를 해제하기 위해 호출한다.

throw(ex_type[, ex_value[, ex_traceback]])

이 메서드는 제너레이터가 중단된 현재 위치에서 예외를 던진다. 제너레이터가 예외를 처리했으면 해당 except절에 있는 코드가 호출되고, 예외를 처리하지 않았으면 예외가 호출자에게 전파된다.

코루틴이 예외를 처리했을 때와 그렇지 않을 때의 차이를 살펴보자.

class CustomException(Exception):
  """처리하려는 에러 유형"""
  def stream_data(db_handler):
    while True:
      try:
        yield db_handler.read_n_records(10) # 1
      except CustomException:
        logger.warning("%r 에러 발생 후 계속 진행", e)
      except Exception as e: # 2
        logger.error("%r 에러 발생 후 중단", e)
        db_handler.close()
        break

이제 CustomException을 처리하고 있으며 이 예외가 발생한 경우 제너레이터는 WARNING 레벨의 메시지를 기록한다.

이 예제는 모든 예외를 처리하고 있는데, 만약 마지막 블록(2)이 없다면, 제너레이터가 중지된 라인(1)에서 발생한 예외가 호출자에게 전파되고 제너레이터가 중지될 것이다.

send(value)

현재 제너레이터의 주요 기능은 고정된 수의 레코드를 읽는 것이다. 이제 읽어올 개수를 파라미터로 받도록 수정해보자. 안타깝게도 next() 함수는 이러한 옵션을 제공하지 않기 때문에 send() 메서드를 사용하면 된다.

def stream_db_records(db_handler):
  retrieved_data = None
  previous_page_size = 10
  try:
    while True:
      page_size = yield retrieved_data
      if page_size is None:
        page_size = previous_page_size

      previous_page_size = page_size
      retrieved_data = db_handler.read_n_records(page_size)

  except GeneratorExit:
    db_handler.close()

이제 send() 메서드를 통해 인자 값을 전달할 수 있다. 사실 이 메서드는 제너레이터와 코루틴을 구분하는 기준이 된다. send() 메서드를 사용했다는 것은 yield 키워드가 할당 구문의 오른쪽에 나오게 되고 인자 값을 받아서 다른 곳에 할당할 수 있음을 뜻한다.

코루틴에서는 일반적으로 다음과 같은 형태로 yield 키워드를 사용한다.

receive = yield produced

이 경우 yield 키워드는 두 가지 일을 한다. 하나는 produced 값을 호출자에게 보내고 그 곳에 멈추는 것이다. 호출자는 next() 메서드를 호출하여 다음 라운드가 되었을 때 값을 가져올 수 있다. 다른 하나는 거꾸로 호출자로부터 send() 메서드를 통해 전달된 produced 값을 받는 것이다. 이렇게 입력된 값은 receive 변수에 할당된다.

코루틴에서 send() 메서드를 호출하려면 항상 next()를 먼저 호출해야 한다.

profile
안녕하세요 박찬영입니다.

0개의 댓글