들어가기 전
- 이 글은 이펙티브 파이썬을 공부하며 작성한 글입니다.
- 코드 : https://github.com/chickenchickenlove/effective-python/tree/master/item34
요약
- send()를 이용하면 제네레이터에 데이터를 전송할 수 있음. 즉, 제네레이터와 양방향 소통이 가능함.
- send() 메서드는 일반적인 이터레이션(For, next())와 다른 방식으로 동작함.
- 제네레이터가 시작하기 전에 send()를 사용하면, 반드시 None을 입력값으로 전송해야 함. (시작되지 않았기 때문임)
- 그 이후 send()를 사용할 때는, 원하는 값을 전송할 수 있음.
- 여러 제네레이터를 합성해서 사용하는 yield from과 send()를 함께 사용하면, 서로 다른 용법 때문에 가독성이 나빠짐.
- send()는 가독성을 나쁘게 하므로 가급적 사용하지 말고, 데이터 전송이 필요하면 이터레이터를 입력으로 전달하자.
Item 34. send로 제네레이터에 데이터를 주입하지 마라.
일반적으로 제네레이터는 단방향으로 소통한다. 무슨 말이냐면, 클라이언트가 호출했을 때 yield 키워드를 이용해 값을 반환하지만 클라이언에게서 값을 전송받지는 않는다. 그러나 send() 메서드를 이용하면, 동작중인 제네레이터에게도 값을 전달할 수 있다.
단방향 제네레이터
def wave(amplitude, steps):
step_size = 2 * math.pi / steps
for step in range(steps):
radians = step * step_size
fraction = math.sin(radians)
output = amplitude * fraction
yield output
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))
위 코드는 yield만 사용한 단방향 제네레이터 코드다.
- wave()는 Sin, Cos 파형을 출력하는 제네레이터 함수다.
- wave() 함수는 고정된 진폭(amplitude)를 가지는 함수다.
wave() 함수는 시작할 때 고정된 진폭(amplitude)를 입력받아 사용하기 때문에 고정된 진폭을 가진 Sin, Cos 파형은 잘 출력할 수 있다. 그러나 유동적인 진폭을 가진 Sin, Cos 파형은 출력할 수 없다. 현재 코드에서는 동작중인 제네레이터에게 값을 전달해 줄 수 있는 방법이 없기 때문이다.
양방향 제네레이터
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 # 다음 진폭을 받음.
def transmit(output):
if output is None:
print(f'출력: None')
else:
print(f'출력: {output:>5.1f}')
def run_modulating(it):
amplitudes = [None, 7, 7, 7, 2, 2, 2, 10, 10, 10, 10, 10]
for amplitude in amplitudes:
output = it.send(amplitude)
transmit(output)
run_modulating(wave_modulating(12))
- 앞선 코드에서는 고정된 Amplitude를 사용하기 때문에 일정한 진폭의 Sin, Cos 파형만 출력할 수 있었다.
- 동적인 Amplitude를 사용하기 위해서는 제네레이터에 Amplitude 값을 전송할 방법이 필요하다.
제네레이터에 동적으로 Amplitude 값을 전송하기 위해서 send() 메서드를 사용할 수 있다. send() 메서드는 다음과 같이 동작한다.
- 제네레이터가 yield 키워드에서 값을 반환한 시점에 send()로 데이터를 전송하면 다음과 같이 동작함.
- 제네레이터는 yield 키워드에서 전송한 값을 받을 수 있다. amplitude = yield output 부분에서 send()로 전송된 값이 전달되고, amplitude에 그 값이 바인딩 된다.
- 제네레이터는 값을 바인딩 한 후에 제네레이터 코드 내에서 다음 블록을 수행함.
- 아직 시작하지 않은 제네레이터에는 send() 메서드를 사용할 수 없다.
- 제네레이터 시작 전에 보내는 send()는 반드시 None이 보내져야 함.
- 이후 send()를 호출할 때, 원하는 값을 전송할 수 있음.
send()를 이용해서 제네레이터에 동적으로 값을 전송하는 방법은 위에서 설명했다. 설명을 읽어보면 알겠지만, 알아야 할 부분이 제법 있다. 또한 제네레이터를 사용할 때, 처음과 나머지 부분을 다르게 사용해야 한다는 부분도 꽤 번거롭다.
결론적으로 제네레이터에게 send()로 값을 전송하는 기능을 사용한다면 다음 문제가 존재한다.
- 제네레이터 시작 하기 전에 send()를 사용하려면 반드시 None을 전송해야 함.
- 따라서 제네레이터의 시점에 따라 send()의 사용 방법이 달라짐.
- 일반적인 이터레이팅 문법과 다름. 따라서 고려해야 할 부분이 많아짐.
위 문제는 가독성이 나쁘고, 알아야 할 것이 많고, 사용하기 어렵다는 것으로 귀결되게 된다.
send()와 yield from을 함께 쓰면 대환장 파티
import math
def wave_modulating(steps):
step_size = 2 * math.pi / steps
print('here')
amplitude = yield # 초기 진폭을 받음.
print(f'init amplitude = {amplitude}')
for step in range(steps):
radians = step * step_size
fraction = math.sin(radians)
output = amplitude * fraction
print(f'{amplitude=}')
amplitude = yield output # 다음 진폭을 받음.
print(f'last amplitude = {amplitude}')
def transmit(output):
if output is None:
print(f'출력: None')
else:
print(f'출력: {output:>5.1f}')
def run_modulating(it):
amplitudes = [None, 7, 6, 5, 4, 3, 2, 1, 100, 90, 80, 70]
for amplitude in amplitudes:
output = it.send(amplitude)
transmit(output)
def complex_wave_modulating():
yield from wave_modulating(3)
print(1)
yield from wave_modulating(4)
print(2)
yield from wave_modulating(5)
print(3)
run_modulating(complex_wave_modulating())
>>>> # 압축한 출력임. (몇개는 생략함)
출력: None
init amplitude = 7
amplitude=7
amplitude=6
amplitude=5
last amplitude = 4
...
출력: None
init amplitude = 3
amplitude=3
amplitude=2
...
yield from 키워드를 이용해 여러 제네레이터를 합성해서 사용하는 경우도 있다. 이럴 때, send()와 yield from을 함께 사용하면 가독성이 나쁘고, 어떻게 동작할지 예측하기 어려운 코드가 만들어진다. 위 코드에서 입력이 어떻게 사용되는지를 살펴보자. (이상하게 사용됨)
- None → 첫번째 자식 제네레이터의 yield까지 간다.
- 7 → 첫번째 자식 제네레이터의 첫번째 amplitude로 사용됨.
- 6 → 첫번째 자식 제네레이터의 두번째 amplitude로 사용됨.
- 5 → 첫번째 자식 제네레이터의 세번째 amplitude로 사용됨.
- 4 → 두번째 자식 제네레이터의 yield까지 간다. 즉 버려지는 값이 된다.
- 3 → 두번째 자식 제네레이터의 첫번째 amplitude로 사용됨.
여기서 주목해야 할 부분은 send()로 제네레이터에 값을 전송할 때, 제네레이터가 어떤 상태인지 정확하게 알아야만 한다는 점이다. 그렇지 않으면 5번처럼 '의도치 않게 버려지는 값'이 생길 수 밖에 없다. 그런데 우리가 코드를 작성할 때, 이 값들을 정확하게 알고 입력 값을 작성한다는 것은 '사실상 하드코딩'과 다름 없다. 따라서 send()와 yield from을 함께 사용해서 잘 동작하는 코드를 만든다는 것은 다음을 의미한다.
- 매우 읽기 어려운 코드를 작성해야함.
- 하드코딩을 해야할 수도 있음.
- 하드코딩을 하지 않기 위해서 다른 함수의 복잡성이 증가할 수 있음. send() 메세지의 특수성을 처리하기 위해..
결론적으로 send()를 이용해 제네레이터에 값을 전송하는 것은 절대로 하지 않아야 할 작업이 될 것이다.
send() 대신 이터레이터를 전달하자.
import math
def wave_modulating(amplitude_it, steps):
step_size = 2 * math.pi / steps
for step in range(steps):
radians = step * step_size
fraction = math.sin(radians)
yield next(amplitude_it) * fraction
def transmit(output):
if output is None:
print(f'출력: None')
else:
print(f'출력: {output:>5.1f}')
def run_modulating():
amplitudes = [7, 6, 5, 4, 3, 2, 1, 100, 90, 80, 70, 1000]
it = iter(amplitudes)
for v in complex_wave_modulating(it):
transmit(v)
def complex_wave_modulating(it):
yield from wave_modulating(it, 3)
yield from wave_modulating(it, 4)
yield from wave_modulating(it, 5)
run_modulating()
send()를 이용해서 제네레이터에 값을 전송하는 코드를 작성하면 다음 문제점이 있었다.
- send() + yield from을 함께 사용하면 아주 읽기 어려워진다.
- send() 자체의 문법도 공부할 것이 있고, send()를 사용할 때 고려해야 할 부분도 많음.
이런 단점들 때문에 send()는 사용하지 말고, 꼭 필요한 경우 이터레이터를 전달하는 방식을 사용해야한다. 위 코드에서는 가변 진동을 표현하기 위해 amplitude 컨테이너의 이터레이터를 complex_wave_modulating()에 전달해서 가변 진동을 표현할 수 있었다.
이터레이터를 사용하면서, 가독성이 좋아졌고 문법적으로 고려할 부분도 많이 줄어들었으며, 덕분에 함수들의 복잡성도 많이 감소하게 되었다.
'프로그래밍 언어 > 파이썬' 카테고리의 다른 글
Effective Python Item 38. 간단한 인터페이스의 경우 클래스 대신 함수를 받아라. (0) | 2024.02.04 |
---|---|
Effective Python Item 35. 제네레이터 안에서 throw로 상태를 변화시키지 마라. (0) | 2024.01.21 |
Effective Python Item 33. yield from을 사용해 여러 제네레이터를 합성하라. (0) | 2024.01.20 |
Effective Python Item 32. 긴 리스트 컴프리헨션보다는 제네레이터 식을 사용하라. (0) | 2024.01.20 |
Effective Python Item 30. 리스트를 반환하기보다는 제너레이터를 사용하라 (0) | 2024.01.15 |