제너레이터를 사용해 화면의 이미지를 움직이게 하는 그래픽 프로그램이 있다고 하자.
def move(period, speed):
for _ in range(period):
yield speed
def pause(delay):
for _ in range(delay):
yield 0
최종 애니메이션을 만드려면 move
와 pause
를 합성해서 변위(delta) 시퀀스를 하나만 만들어야 한다.
애니메이션의 각 단계마다 제너레이터를 호출해서 차례로 이터레이션하고 각 이터레이션에서 나오는 변위를 순서대로 내보내는 방식으로 다음과 같이 시퀀스를 만든다.
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}')
# 화면에서 이미지를 이동시킨다.
...
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
식을 사용하는 것이다. 이는 제어를 부모 제너레이터에게 전달하기 전에 내포된 제너레이터가 모든 값을 내보낸다.
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(), # 코드를 실행할 namespace
number=50) # 선언한 stmt의 수행 횟수, default 10000000
print(f'수동 내포: {baseline:.2f}s')
comparison = timeit.timeit(
stmt='for _ in fast(): pass',
globals=globals(),
number=50)
print(f'합성 사용: {comparison:.2f}s')
>>>
수동 내포: 2.81s
합성 사용: 2.56s
# 약 8.8% 시간이 적게 듦
timeit
내장 모듈timeit.timeit( stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None ) #지정된 문장, setup 코드 및 timer 함수로 Timer 인스턴스를 만들고, # number 실행으로 timeit() 메서드를 실행합니다. # 선택적 globals 인자는 코드를 실행할 이름 공간을 지정합니다.
timeit.repeat( stmt='pass', setup='pass', timer=<default timer>, repeat=5, number=1000000, globals=None ) # 주어진 문장, setup 코드 및 timer 함수로 Timer 인스턴스를 생성하고, # 주어진 repeat 카운트와 number 실행으로 repeat() 메서드를 실행합니다. # 선택적 globals 인자는 코드를 실행할 이름 공간을 지정합니다.
timeit.default_timer() # 기본 타이머, 항상 time.perf_counter()입니다.
만약 제너레이터를 합성한다면 가급적 yield from
을 사용하라.
기억해야 할 내용
yield from
식을 사용하면 여러 내장 제너레이터를 모아서 제너레이터 하나로 합성할 수 있다.- 직접 내포된 제너레이터를 이터레이션하면서 각 제너레이터의 출력을 내보내는 것보다
yield from
을 사용하는 것이 성능 면에서 더 좋다.