Effective Python Item 34. send로 제네레이터에 데이터를 주입하지 마라.

    들어가기 전

     


    요약

    • 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만 사용한 단방향 제네레이터 코드다.

    1. wave()는 Sin, Cos 파형을 출력하는 제네레이터 함수다.
    2. 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))
    1. 앞선 코드에서는 고정된 Amplitude를 사용하기 때문에 일정한 진폭의 Sin, Cos 파형만 출력할 수 있었다. 
    2. 동적인 Amplitude를 사용하기 위해서는 제네레이터에 Amplitude 값을 전송할 방법이 필요하다.

    제네레이터에 동적으로 Amplitude 값을 전송하기 위해서 send() 메서드를 사용할 수 있다. send() 메서드는 다음과 같이 동작한다.

    1. 제네레이터가 yield 키워드에서 값을 반환한 시점에 send()로 데이터를 전송하면 다음과 같이 동작함. 
      • 제네레이터는 yield 키워드에서 전송한 값을 받을 수 있다. amplitude = yield output 부분에서 send()로 전송된 값이 전달되고, amplitude에 그 값이 바인딩 된다. 
      • 제네레이터는 값을 바인딩 한 후에 제네레이터 코드 내에서 다음 블록을 수행함. 
    2. 아직 시작하지 않은 제네레이터에는 send() 메서드를 사용할 수 없다.
      • 제네레이터 시작 전에 보내는 send()는 반드시 None이 보내져야 함.
      • 이후 send()를 호출할 때, 원하는 값을 전송할 수 있음. 

    send()를 이용해서 제네레이터에 동적으로 값을 전송하는 방법은 위에서 설명했다. 설명을 읽어보면 알겠지만, 알아야 할 부분이 제법 있다. 또한 제네레이터를 사용할 때, 처음과 나머지 부분을 다르게 사용해야 한다는 부분도 꽤 번거롭다. 

    결론적으로 제네레이터에게 send()로 값을 전송하는 기능을 사용한다면 다음 문제가 존재한다.

    1. 제네레이터 시작 하기 전에 send()를 사용하려면 반드시 None을 전송해야 함. 
    2. 따라서 제네레이터의 시점에 따라 send()의 사용 방법이 달라짐. 
    3. 일반적인 이터레이팅 문법과 다름. 따라서 고려해야 할 부분이 많아짐. 

    위 문제는 가독성이 나쁘고, 알아야 할 것이 많고, 사용하기 어렵다는 것으로 귀결되게 된다. 

     


    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을 함께 사용하면 가독성이 나쁘고, 어떻게 동작할지 예측하기 어려운 코드가 만들어진다. 위 코드에서 입력이 어떻게 사용되는지를 살펴보자. (이상하게 사용됨)

    1. None → 첫번째 자식 제네레이터의 yield까지 간다. 
    2. 7 → 첫번째 자식 제네레이터의 첫번째 amplitude로 사용됨.
    3. 6 → 첫번째 자식 제네레이터의 두번째 amplitude로 사용됨.
    4. 5 → 첫번째 자식 제네레이터의 세번째 amplitude로 사용됨.
    5. 4 → 두번째 자식 제네레이터의 yield까지 간다. 즉 버려지는 값이 된다.
    6. 3 → 두번째 자식 제네레이터의 첫번째 amplitude로 사용됨. 

    여기서 주목해야 할 부분은 send()로 제네레이터에 값을 전송할 때, 제네레이터가 어떤 상태인지 정확하게 알아야만 한다는 점이다. 그렇지 않으면 5번처럼 '의도치 않게 버려지는 값'이 생길 수 밖에 없다. 그런데 우리가 코드를 작성할 때, 이 값들을 정확하게 알고 입력 값을 작성한다는 것은 '사실상 하드코딩'과 다름 없다. 따라서 send()와 yield from을 함께 사용해서 잘 동작하는 코드를 만든다는 것은 다음을 의미한다.

    1. 매우 읽기 어려운 코드를 작성해야함. 
    2. 하드코딩을 해야할 수도 있음. 
    3. 하드코딩을 하지 않기 위해서 다른 함수의 복잡성이 증가할 수 있음. 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()를 이용해서 제네레이터에 값을 전송하는 코드를 작성하면 다음 문제점이 있었다.

    1. send() + yield from을 함께 사용하면 아주 읽기 어려워진다. 
    2. send() 자체의 문법도 공부할 것이 있고, send()를 사용할 때 고려해야 할 부분도 많음. 

    이런 단점들 때문에 send()는 사용하지 말고, 꼭 필요한 경우 이터레이터를 전달하는 방식을 사용해야한다. 위 코드에서는 가변 진동을 표현하기 위해 amplitude 컨테이너의 이터레이터를 complex_wave_modulating()에 전달해서 가변 진동을 표현할 수 있었다. 

    이터레이터를 사용하면서, 가독성이 좋아졌고 문법적으로 고려할 부분도 많이 줄어들었으며, 덕분에 함수들의 복잡성도 많이 감소하게 되었다. 

    댓글

    Designed by JB FACTORY