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.0
출력: -3.0
출력: -2.1
기본 파형을 생성하는 한 이 코드는 잘 작동한다. 하지만 별도의 입력을 사용해 진폭을 지속적으로 변경해야 한다면 이 코드는 쓸모가 없다. 제너레이터를 이터레이션할 때마다 진폭을 변조할 수 있는 방법이 필요하다.
파이썬 제너레이터는 send
메서드를 사용하면 입력을 제너레이터에 스트리밍하는 동시에 출력을 내보낼 수 있다. 일반적으로 제너레이터를 이터레이션할 때 yield
식이 반환하는 값은 None
이다.
def my_generator():
received = yield 1
print(f'받은값 = {received}')
it = iter(my_generator())
output = next(it) # 첫 번째 제너레이터의 출력을 얻음 (send 실행 (n=1))
print(f'출력값 = {output}')
try:
next(it) # 종료될 때까지 제너레이터를 실행한다. (send 실행 (n>1))
except StopIteration:
pass
>>>
출력값 = 1
받은값 = None
for
루프나 next
내장 함수로 제너레이터를 이터레이션하지 않고 send
메서드를 호출하면, 제너레이터가 재개(resume)될 때 yield
가 send
에 전달된 파라미터 값을 반환한다. 하지만 위 제너레이터는 아직 yield
식에 도달하지 못했기 때문에 최초로 send
를 호출할 때 인자로 전달할 수 있는 유일한 값은 None
뿐이다.
위의 경우 첫번째
next(it)
호출에서None
값이 전달되며, 두번째next(it)
이 실행될 때(resume)received = yield 1
의yield
가None
값을 반환한다.
다른 값을 전달하면 실행 시점에 TypeError: can't send non-None value to a just-started generator 예외가 발생
it = iter(my_generator())
output = it.send(None) # 첫 번째 제너레이터의 출력을 얻음
print(f'출력값 : {output}')
try:
it.send('안녕!')
except StopIteration:
pass
>>>
출력값 = 1
받은값 = 안녕!
이런 동작을 활용해 입력 시그널을 바탕으로 사인 파의 진폭을 변조할 수 있다.
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_modulating
제너레이터에게 스트리밍하도록 만든다. 아직 제너레이터가 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_modulatin(wave_modulating(12))
>>>
출력: None
출력: 0.0
출력: 3.5
출력: 6.1
출력: 2.0
...
출력: -10.0
출력: -8.7
출력: -5.0
제너레이터가 첫 번째 yield 식에 도달할 때까지는 amplitude
값을 받지 못하므로, 첫 번째 출력은 None
이다.
이 코드의 문제점은 코드를 처음 봤을 때 이해하기 어렵다는 것이다. 대입문의 오른쪽에 yield
를 사용하는 것은 직관적이지 않다. 그리고 제너레이터 고급 기능을 잘 모를 경우에는 send
와 yield
사이의 연결을 알아보기 어렵다.
이제 여러 신호의 시퀀스로 이뤄진 복잡한 파형을 사용해야 한다면, yield from
식을 사용해 여러 제너레이터를 합성하는 것이다.
def complex_wave():
yield from wave(7.0, 3)
yield from wave(2.0, 4)
yield from wave(10.0, 5)
run(complex_wave())
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)
run_modulating(complex_wave_modulating())
>>>
출력: None
...
출력: None
...
출력: None
...
이 코드는 어느 정도 잘 작동한다. 하지만 출력에 None
이 여럿 보인다.
내포된 제너레이터에 대한 yield from
식이 끝날 때마다 다음 yield from
식이 실행된다. 각각의 내포된 제너레이터는 send
메서드 호출로부터 값을 받기 위해 아무런 값도 만들어내지 않는 단순한 yield
식으로 시작한다. 이로 인해 부모 제너레이터가 자식 제너레이터를 옮겨갈 때마다 None
이 출력된다.
이는 yield from
과 send
를 따로 사용할 때는 제대로 작용하던 특성이 두 기능을 함께 사용할 때는 깨지기 때문이다. 이 문제를 해결하기 위해 run_modulating
함수의 복잡도를 증가시켜 우회할 수는 있겠지만, 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)
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()
다만 이 코드는 입력 제너레이터가 완전이 스레드 안정(thread-safe)하다고 가정한다는 단점이 있다. 하지만 제너레이터가 항상 스레드 안전하지는 않다. 따라서 스레드 경계를 넘나들면서 제너레이터를 사용해야 한다면 async
함수가 더 나은 해법일 수 있다.
기억해야 할 내용
send
메서드를 사용해 데이터를 제너레이터에 주입할 수 있다. 제너레이터는send
로 주입된 값을yield
식이 반환하는 값을 통해 받으며, 이 값을 변수에 저장해 활용할 수 있다.send
와yield from
식을 함께 사용하면 제너레이터의 출력에None
이 중간중간 나타나는 결과를 얻을 수 있다.- 합성할 제너레이터들의 입력으로 이터레이터를 전달하는 방식이
send
를 사용하는 방식보다 더 낫다.