파이썬 코딩의 기술 - 33

JinWooHyun·2021년 7월 7일
0

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

제너레이터를 사용해 화면의 이미지를 움직이게 하는 그래픽 프로그램이 있다고 하자.

def move(period, speed):
    for _ in range(period):
    	yield speed

def pause(delay):
    for _ in range(delay):
    	yield 0

최종 애니메이션을 만드려면 movepause를 합성해서 변위(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을 사용하는 것이 성능 면에서 더 좋다.
profile
Unicorn Developer

0개의 댓글