betterway34send로 제너레이터 데이터 주입말라

김승환·2021년 7월 11일

코딩의 기술

목록 보기
24/36

send로 제너레이터에 데이터를 주입하지 말라

  • yield식을 사용하면 제너레이터 함수가 간단하게 이터레이션이 가능한 출력 값을 만들어낼 수 있다.
  • 하지만 이렇게 만들어내는 채널은 단방향이다.
  • 제너레이터가 데이터를 내보내면서 다른 데이터를 받아들일 때 직접 쓸 수 있는 방법이 없는 것처럼 보인다.
  • 하지만 이런 양방향 통신이 있다면 많은 경우에 도움이 될 것이다.
# 예를 들어 소프트웨어 라디오를 사용해 신호를 내보낸다고 하자.
# 다음 코드는 주어진 간격과 진폭에 따른 사인파값을 생성한다.

import math

def wave(amplitude, steps):
    step_size = 2 * math.pi / steps   # 2라디안/단계 수
    for step in range(steps):
        radians = step * step_size
        fraction = math.sin(radians)
        output = amplitude * fraction
        yield output
# wave 제너레이터를 이터레이션하면서 진폭이 고정된 파형 신호를 송신할 수 있다.
def transmit(output):
    if output is None:
        print(f'출력: None')
    else:
        print(f'출력: {output:>5.1f}')

def run(it):
    for output in it:
        transmit(output)

run(wave(3.0, 8))

출력: 0.0
출력: 2.1
출력: 3.0
출력: 2.1
출력: 0.0
출력: -2.1
출력: -3.0
출력: -2.1

  • 하지만 별도의 입력을 사용해 진폭을 지속적으로 변경해야 한다면 이 코드는 쓸모없다.
  • 제너레이터를 이터레이션할 때마다 진폭을 변조할 수 있는 방법이 필요하다.

제너레이터는 send 메서드를 지원한다.

  • 이 메서드는 yield 식을 양방향 채널로 격상시켜준다.
  • send 메서드를 사용하면 입력을 제너레이터에 스트리밍하는 동시에 출력을 내보낼 수 있다.
  • 일반적으로 제너레이터를 이터레이션할 때 yield 식이 반환하는 값은 None이다.
def my_generator():
    received = yield 1
    print(f'받은 값 = {received}')

it = iter(my_generator())



output = next(it)  # 첫 번째 제너레이터 출력을 얻는다
print(f'출력값 = {output}')

try:
    next(it)  # 종료될 때까지 제너레이터를 실행한다
except StopIteration:
    pass

출력값 = 1
받은 값 = None

def my_generator():
    received = yield 1
    
    print(f'받은 값 = {received}')

it = iter(my_generator())
try:
    print(next(it))  # 종료될 때까지 제너레이터를 실행한다
except StopIteration:
    pass

try:
    print(next(it)) # 첫 번째 제너레이터 출력을 얻는다

except StopIteration:
    pass

1
받은 값 = None

  • 하지만 for 루프나 next 내장 함수로 제너레이터를 이터레이션하지 않고 send 메서드를 호출하면, 제너레이터가 재개될 때 yield가 send에 전달된 파라미터 값을 반환한다.
  • 방금 시작한 제너레이터는 아직 yield식에 도달하지 못했기 때문에 최초로 send를 호출할 때 인자로 전달할 수 있는 유일한 값은 None뿐이다.
#
it = iter(my_generator())
output = it.send(None)     # 첫 번째 제너레이터 출력을 얻는다
print(f'출력값 = {output}')

try:
    it.send('안녕!')    # 값을 제너레이터에 넣는다
except StopIteration:
    pass
 
try:
    it.send(None)
except StopIteration:
    print('stop')

출력값 = 1
받은 값 = 안녕!
stop

  • 위 동작을 이용해 입력 시그널을 바탕으로 사인 파의 진폭을 변조할 수 있다.
  • yield식이 반환한 진폭 값을 amplitude에 저장하고, 다음 yield출력 시 이 진폭 값을 활용하도록 wave 제너레이터를 변경해야한다.
import math
def wave_modulating(steps):
    step_size = 2 * math.pi / steps
    amplitude = yield                # 초기 진폭을 받는다
    for step in range(steps):
        radians = step * step_size
        fraction = math.sin(radians)
        output = amplitude * fraction
        amplitude = yield output     # 다음 진폭을 받는다
  • 이후 run 함수를 변경해서 매 이터레이션마다 변조에 사용할 진폭을 wave_modularting 제너레이터에게 스트리밍하도록 만든다.
  • 아직 제너레이터가 yield식에 도달하지 못했으므로 send에 보내는 첫 번째 입력은 None이어야 한다.
def run_modulating(it):
    amplitudes = [
        None, 7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10]
    for amplitude in amplitudes:
        output = it.send(amplitude)
        transmit(output)

run_modulating(wave_modulating(12))

출력: None
출력: 0.0
출력: 3.5
출력: 6.1
출력: 2.0
출력: 1.7
출력: 1.0
출력: 0.0
출력: -5.0
출력: -8.7
출력: -10.0
출력: -8.7
출력: -5.0

  • 제너레이터가 첫 번째 yield식에 도달할 때까지는 amplitude 값을 받지 못하므로 예상대로 None

문제점

  • 코드를 처음 봤을 때 이해하기 어려움,

  • 대입문의 오른쪽에 yield 를 사용하는 것은 직관적이지 않다.

  • 제너레이터 고급 기능을 잘 모를 경우 send와 yield 사이 연결을 알아보기 어렵다.

  • 프로그램 요구 사항이 더 복잡해졌다고 가정하자.

  • 단순 사인파를 반송파로 사용하는 대신, 여러 신호의 시퀀스로 이뤄진 복잡한 파형을 사용해야 한다.

  • 이런 동작을 구현하는 한 가지 방법은 yield from식을 사용해 여러 제너레이터를 합성하는 것이다.

# 다음은 진폭이 고정된 더 단순한 경우에 yield from이 잘 작동하는 지 확인하기 위한 코드
def complex_wave():
    yield from wave(7.0, 3)
    yield from wave(2.0, 4)
    yield from wave(10.0, 5)
def wave(amplitude, steps):
    step_size = 2 * math.pi / steps   # 2라디안/단계 수
    for step in range(steps):
        radians = step * step_size
        fraction = math.sin(radians)
        output = amplitude * fraction
        yield output
def run(it):
    for output in it:
        transmit(output)

run(complex_wave())

출력: 0.0
출력: 6.1
출력: -6.1
출력: 0.0
출력: 2.0
출력: 0.0
출력: -2.0
출력: 0.0
출력: 9.5
출력: 5.9
출력: -5.9
출력: -9.5

  • yield from 식이 단순한 경우를 잘 처리하므로, 이 제너레이터에 send 메서드를 사용해도 잘 작동할 것으로 예상할 수 있다.
  • wave_modulating 제너레이터에 대한 여러 호출을 yield from으로 합성해보자.
def complex_wave_modulating():
    yield from wave_modulating(3)
    yield from wave_modulating(4)
    yield from wave_modulating(5)

def wave_modulating(steps):
    step_size = 2 * math.pi / steps
    amplitude = yield                # 초기 진폭을 받는다
    for step in range(steps):
        radians = step * step_size
        fraction = math.sin(radians)
        output = amplitude * fraction
        amplitude = yield output   
def run_modulating(it):
    amplitudes = [
        None, 7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10]
    for amplitude in amplitudes:
        output = it.send(amplitude)
        transmit(output)
run_modulating(complex_wave_modulating())

출력: None
출력: 0.0
출력: 6.1
출력: -6.1
출력: None
출력: 0.0
출력: 2.0
출력: 0.0
출력: -10.0
출력: None
출력: 0.0
출력: 9.5
출력: 5.9

  • 여러 none값이 출력되었다.

  • yield from 식이 끝날 때마다 다음 yield from 식이 실행 된다.

  • 각 내포된 제너레이터는 send 메서드 호출로부처 값을 받기 위해 아무런 값도 만들어내지 않는 단순한 yield식으로 시작한다.

  • 이로 인해 부모 제너레이터가 자식 제너레이터를 옮겨갈 때마다 None이 출력된다.

  • yield from과 send를 따로 사용할 때는 제대로 작용하던 특성이 두 기능을 함께 사용할 때는 깨지기 때문이다.

  • run-modulation함수의 복잡도를 증가시켜서 이런 None 문제를 우회하는 방법이 있다.

  • 하지만 이 문제를 해결하기 위해 그런 노력을 기울일 만한 가치는 없다.

  • send가 어떻게 작동하는지 배우는 것도 어려운데 yield from의 함정까지 이해해야 한다면 악화된다.

  • send를 사용하지말고 더 단순한 접근 방법을 택할 것을 권한다.

해결법

  • wave 함수에 이터레이터를 전달하는 것이다.
  • 이터레이터는 자신에 대해 next 내장 함수가 호출될 때마다 입력으로 받은 진폭을 하나씩 돌려준다.
  • 이런식으로 이전 제너레이터를 다음 제너레이터의 입력으로 연쇄시켜 연결하면 입력과 출력이 차례로 처리되게 만들 수 있다.
def wave_cascading(amplitude_it, steps):
    step_size = 2 * math.pi / steps
    for step in range(steps):
        radians = step * step_size
        fraction = math.sin(radians)
        amplitude = next(amplitude_it) # 다음 입력 받기
        output = amplitude * fraction
        yield output
  • 합성에 사용할 여러 제너레이터 함수에 같은 이터레이터를 넘길 수도 있다
  • 이터레이터는 상태가 있기 때문에 내포된 각각의 제너레이터는 앞에 있는 제너레이터가 처리를 끝낸 시점부터 데이터를 가져와 처리한다.
def complex_wave_cascading(amplitude_it):
    yield from wave_cascading(amplitude_it, 3)
    yield from wave_cascading(amplitude_it, 4)
    yield from wave_cascading(amplitude_it, 5)
  • 이제 amplitutdes 리스트에서 얻은 이터레이터를 합성한 제너레이터에 넘기기만 하면 전체를 실행할 수 있다
def run_cascading():
    amplitudes = [7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10]
    it = complex_wave_cascading(iter(amplitudes))
    for amplitude in amplitudes:
        output = next(it)
        transmit(output)

run_cascading()

출력: 0.0
출력: 6.1
출력: -6.1
출력: 0.0
출력: 2.0
출력: 0.0
출력: -2.0
출력: 0.0
출력: 9.5
출력: 5.9
출력: -5.9
출력: -9.5

  • 이 접근 방법에서 가장 멋진 부분은 아무 데서나 이터레이터를 가져올 수 있다.
  • 이터레이터가 완전히 동적인 경우에도 잘 작동한다는 점이다.
  • 다만 이 코드는 입력 제너레이터가 완전히 스레드 안전하다고 가정한다는 단점이 있다.
  • 하지만 제너레이터가 항상 스레드 안전하지는 않다.
  • 따라서 스레드 경계를 넘나들면서 제너레이터를 사용해야 한다면 async 함수가 더 나은 해법일 수도 있다.
profile
인공지능 파이팅!

0개의 댓글