betterway33 yield from을 사용해 여러 제너레이터를 합성하라

김승환·2021년 7월 11일

코딩의 기술

목록 보기
23/36

yield from을 사용해 여러 제너레이터를 합성하라

  • 제너레이터에는 여러 장점이 있고, 제너레이터에서 발생할 수 있는 일반적인 문제를 해결할 방법도 있다.
  • 제너레이터가 아주 유용하기 때문에 다양한 곳에 제너레이터가 쓰이고 있으며, 이로인해 제너레이터를 여러 단계에 걸쳐 한줄기로 연결한 것처럼 보이는 프로그램도 많다.
  • 예를들어 이미지를 움직이는 프로그램이 있다고 하자. 원하는 시각적인 효과를 얻으려면 처음에는 이미지가 빠르게 이동하고, 잠시 멈춘 다음, 다시 이미지가 느리게 이동해야한다.
  • 다음 이 애니메이션의 각 부분에서 필요한 화면상 이동 변위(delta)를 만들어 낼 때 사용할 두 가지 제너레이터를 정의한 코드이다.
def move(period, speed):
    for _ in range(period):
        yield speed

#
def pause(delay):
    for _ in range(delay):
        yield 0
  • 최종 애니메이션을 만들려면 move와 pause를 합성해서 변위 시퀀스를 하나만 만들어야 한다.
  • 애니메이션의 각 단계마다 제너레이터를 호출해서 차례로 이터레이션하고 각 이터레이션에서 나오는 변위를 순서대로 내보내는 방식으로 다음과 같이 만든다.
def animate():
    for delta in move(4, 5.0):
        yield delta
    for delta in pause(3):
        yield delta
    for delta in move(2, 3.0):
        yield delta

이렇게 만든 화면상 변위를 단일 animation 제너레이터에서 만들어진 것처럼 화면에 표시한다.

def render(delta):
    print(f'Delta: {delta:.1f}')# :.1f는 자릿수
    # 화면에서 이미지를 이동시킨다

def run(func):
    for delta in func():
        render(delta)

run(animate)

Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 0.0
Delta: 0.0
Delta: 0.0
Delta: 3.0
Delta: 3.0

문제점

  • animate가 너무 반복적이다.
  • for문과 yield식이 반복되면서 잡음이 늘고 가독성이 줄어든다.
  • 이 예제는 제너레이터를 겨우 세 개만 내포시켰는데도 벌써 코드가 명확하지 못하다.
  • 열 단계가 넘어가는 복잡한 애니메이션을 표현하는 코드는 따라가기 훨씬 더 어려울 것이다.

해법

  • yield from 식을 사용하라
  • 고급 제너레이터기능으로, 제어를 부모 제너레이터에게 전달하기 전에 내포된 제너레이터가 모든 값을 내보낸다.
# 다음 코드는 animation 함수를 yield from을 사용해 다시 작성,
#2번을 통합
def animate_composed():
    yield from move(4, 5.0)
    yield from pause(3)
    yield from move(2, 3.0)


run(animate_composed)

Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 5.0
Delta: 0.0
Delta: 0.0
Delta: 0.0
Delta: 3.0
Delta: 3.0

  • 이전 프로그램과 같지만 지금 코드가 더 명확하고 더 직관적이다.
  • yield from은 근본적으로 파이썬 인터프리터가 여러분 대신 for 루프를 내보시키고 yield식을 처리하도록 만든다.
  • 다음 코드에서 timeit내장 모듈을 통해 마이크로 벤치마크를 실행함으로써 성능이 개선되는지 살펴봤다.
import timeit

def child():
    for i in range(1_000_000):
        yield i

def slow():
    for i in child():
        yield i

def fast():
    yield from child()

baseline = timeit.timeit(
    stmt='for _ in slow(): pass',
    globals=globals(),
    number=50)
print(f'수동 내포: {baseline:.2f}s')

comparison = timeit.timeit(
    stmt='for _ in fast(): pass',
    globals=globals(),
    number=50)
print(f'합성 사용: {comparison:.2f}s')

수동 내포: 6.88s
합성 사용: 6.16s

profile
인공지능 파이팅!

0개의 댓글