안녕하세요. 휴먼스케이프의 개발자 Bruno입니다.
이 번 포스트에서는 파이썬의 제너레이터와 코루틴에 대해 다루겠습니다.
제너레이터
제너레이터 함수
제너레이터(Generator) 함수란, yield 키워드를 가진 함수입니다. 이 함수는 제너레이터를 만들어서 반환합니다. 즉 제너레이터 함수는 제너레이터 팩토리라고 할 수 있습니다.
그럼 제너레이터는 또 뭘까요?
제너레이터는 외부에서 실행을 관리할 수 있는 객체입니다.
간단한 제너레이터 예시를 보여드리겠습니다.
>>> def gen_123(): ... yield 1 ... yield 2 ... yield 3 ... >>> gen_123 # 제너레이터 함수입니다>>> gen_123() # 제너레이터 생성 >>> for i in gen_123(): # 제너레이터는 반복자입니다 ... print(i) ... 1 2 3 >>> g = gen_123() # 제너레이터를 생성하여 변수 g에 할당 >>> next(g) 1 >>> next(g) 2 >>> next(g) 3 >>> next(g) Traceback (most recent call last): File ' ', line 1, in StopIteration
위 예시에서 볼 수 있듯, 제너레이터는 반복할 수 있으므로 반복자입니다. 그러나 또 다른 특징을 가지고 있습니다. 각 반복을 외부(호출자)에서 관리할 수 있다는 점입니다.
위 예시를 보면 gen_123() 제너레이터 함수를 이용해서 만든 제너레이터 객체를 변수 g에 할당하고 있습니다. 그 후엔 next() 를 이용해서 호출자가 각 반복의 실행을 조절할 수 있죠.
제너레이터: 느긋한 실행
제너레이터를 이용하면 반복의 실행을 호출자가 관리할 수 있게 됩니다. 이는 많은 이득을 가져오는데요. 그 중 하나가 느긋한 실행입니다.
한 문장에서 단어들을 추출하는 클래스 Sentence를 만들어서 예를 들어보겠습니다.
아래는 제너레이터를 사용하지 않는 Sentence입니다.
generator를 사용하지 않는 Sentence 클래스
위 예시는 아주 잘 동작합니다. 주어진 문장을 단어 별로 나눠서 words 리스트에 저장하고, __getitem__() 메소드의 동작을 리스트에 위임해서 반복자로 동작할 수 있게 합니다.
하지만 입력된 text가 아주 큰 경우, words 속성이 너무 많은 메모리를 차지하게 될 수 있다는 단점이 있습니다. 최악의 경우 허용된 메모리를 초과하여 프로그램이 정지할 수도 있습니다.
제너레이터를 이용해서 느긋한 실행, 즉 요청이 있을 때 다음 words를 반환하도록 구현한다면 메모리를 크게 아낄 수 있습니다.
다음은 제너레이터를 사용해서 느긋하게 동작하는 Sentence 클래스입니다.
__iter__() 안에서 yield를 사용해서 제너레이터로 만들었습니다. 이제 next() 혹은 반복문을 이용해서 차례차례 각 단어에 접근할 수 있게 되었습니다.
>>> from sentence_gen2 import Sentence >>> text = 'Hello Python, Hello Humanscape' >>> for word in Sentence(text): ... print(word) ... Hello Python Hello Humanscape
yield from
yield from은 파이썬 3.3에서 새로 추가된 키워드로, 제너레이터 내부에서 다른 제너레이터를 호출할 때 유용합니다.
yield from을 사용하면 제너레이터 안에 있는 제너레이터의흐름도 호출자가 제어할 수 있게 됩니다.
다음은 yield from을 사용하지 않고, 호출자가 제너레이터 안의 제너레이터의 흐름을 제어할 수 있도록 하는 예시입니다.
>>> def chain(*iterables): ... for it in iterables: ... for i in it: ... yield i ... >>> s = 'ABC' >>> t = tuple(range(3)) >>> list(chain(s, t)) ['A', 'B', 'C', 0, 1, 2]
이렇듯 호출자에서 제너레이터 내부의 제너레이터의 흐름을 제어할 수 있기 위해서는, 처음 제너레이터 안에 다음 제너레이터 흐름을 제어하는 로직을 추가해야 합니다. 즉 호출자에서 흐름을 제어할 수 없습니다.
다음은 yield from을 사용한 예제입니다.
>>> def chain2(*iterables): ... for i in iterables: ... yield from i ... >>> list(chain(s, t)) ['A', 'B', 'C', 0, 1, 2]
위 예시를 보면, yield from을 사용해서 간단하게 제어 흐름을 호출자에서 할 수 있도록 만들 수 있습니다.
이 외에도 yield from은 호출자와 하위 제너레이터 간의 데이터를 매개해주는 역할도 할 수 있습니다. 관련 내용은 아래 코루틴(corouine)을 설명하면서 추가하겠습니다.
코루틴
코루틴(coroutine)은 문법상으로는 제너레이터와 다르지 않습니다. 내부에 yield 키워드가 있는 함수입니다.
제너레이터와 다른 점이라면 호출자가 실행을 컨트롤할 뿐만 아니라, 내부에 데이터를 전달할 수 있다는 점입니다. 코루틴은 호출자가 next() 대신 값을 전송하는 send()를 호출하면 코루틴이 호출자로부터 데이터를 받을 수 있습니다.
다음은 간단한 코루틴 예시입니다.
>>> def simple_coroutine(): ... print('-> 코루틴 시작') ... x = yield # 호출자에서 주는 데이터를 받을 수 있음 ... print(f'-> 코루틴 이 받은 데이터:{x}') ... >>> my_coro = simple_coroutine() >>> my_coro>>> next(my_coro) # 코루틴 기동(priming) -> 코루틴 시작 >>> my_coro.send(42) # 코루틴에 데이터 전달 -> 코루틴 이 받은 데이터:42 Traceback (most recent call last): File ' ', line 1, in StopIteration
simple_coroutine()을 보면 내부 yield가 위에 제너레이터 예시들과는 다르게 = 연산자 오른쪽에 위치합니다. 이 코루틴은 호출자가 send()를 이용해서 데이터를 전달하면 yield 문에서 그 데이터를 받아서 x 변수에 할당하고 그 다음 연산을 진행합니다.
코루틴 기동
코루틴을 사용하기 위해선 기동(priming)하는 단계가 필요합니다. 위 예시 중간에 next(my_coro)가 바로 기동하는 단계입니다. 마치 클래스에서 __init__이 호출되듯, 코루틴의 기본 값을 세팅하는 과정을 거친다고 생각하셔도 될 것 같습니다.
처음 next()를 호출하면, 코루틴은 None을 반환하며 첫 yield 문 전까지 실행하고 멈춥니다.
코루틴 종료
마지막에 발생한 StopIteration 예외는 제너레이터에서도 동일하게 발생하는 예외입니다. 바로 모든 yield가 끝나고 코루틴이 종료했을 때 raise 해주는 예외입니다.
코루틴의 상태
코루틴은 다음과 같은 총 네 가지 상태를 가집니다.
GEN_CREATED: 실행을 시작하기 위해 대기하고 있는 상태 GEN_RUNNING: 현재 실행하고 있는 상태. 다중스레드에서만 볼 수 있다 GEN_SUSPENDED: 현재 yield 문에서 대기하고 있는 상태 GEN_CLOSED: 실행이 완료된 상태
코루틴 예제: 이동 평균 계산
이동 평균을 계산하는 코루틴을 만들어서 예를 들어 보겠습니다.
사용예
>>> coro_avg = averager() >>> next(coro_avg) # 코루틴 기동 >>> coro_avg.send(10) 10.0 >>> coro_avg.send(30) 20.0 >>> coro_avg.send(5) 15.0
이렇게 매 번 값을 전달 받아서, 값들의 이동평균을 반환하는 코루틴을 만들어봤습니다.
코루틴을 사용하면 total과 count를 지역변수로 사용할 수 있는 장점이 있습니다. 객체 속성이나 별도의 클로저 없이 평균을 구하는 데 필요한 값을 유지할 수 있습니다.
코루틴을 기동하기 위한 테커레이터
코루틴을 사용하기 위해서는 기동(priming) 과정을 거쳐야 합니다. 위 예시에서는 next(coro_avg)에 해당하는 과정입니다.
이를 단순화하기 위해서 기동하는 데커레이터가 종종 사용됩니다.
위는 코루틴을 기동하는 데커레이터의 구현입니다. 이를 데커레이터로 사용해서 코루틴을 구현하면 기동 과정없이 바로 코루틴을 사용할 수 있게 됩니다.
위처럼 위에서 정의한 코루틴을 기동하는 데커레이터 corouine을 사용해보겠습니다.
>>> coro_avg = averager() >>> from inspect import getgeneratorstate >>> getgeneratorstate(coro_avg) # 바로 GEN_SUSPENDED 상태 돌입 'GEN_SUSPENDED' >>> coro_avg.send(10) # 기동 과정을 거칠 필요가 없다 10.0 >>> coro_avg.send(30) 20.0 >>> coro_avg.send(5) 15.0
기동 과정 없이 편리하게 사용할 수 있게 되었습니다.
코루틴 종료와 예외처리
코루틴 역시, 제너레이터와 같이, 모든 yield문이 끝나게 되면 StopIteration 예외를 발생시키며 종료합니다. 이 외에도 코루틴을 종료하는 방법이 있습니다.
처리되지 않은 예외 발생
generator.thow(exc_type[, exc_value[, traceback]]) 을 이용해서 제너레이터가 중단한 곳의 yield 문에 예외 전달. 이 때, 예외를 처리되지 않은 예외라면 코루틴 또는 제너레이터가 종료됩니다.
generator.close() 을 이용하면 현재 suspend된 yield 문에서 GeneratorExit 예외를 발생시킵니다. 제너레이터가 예외를 처리하지 않거나 StopIteration 예외를 발생시키면, 아무런 에러도 호출자에게 전달되지 않습니다.
코루틴에서 값 반환하기
코루틴은 완료시에 값을 반환하게 할 수 있습니다. 이런 특성을 이용해서, 중간에는 의미 있는 값을 생성하지는 않지만, 최후에 어떤 의미 있는 값을 반환하는 코루틴도 가능합니다.
다음은 값을 반환하는 코루틴 예시입니다.
위 코루틴은 호출자에서 전달 받은 값들의 개수와 평균을 namedtuple 형태로 반환합니다.
>>> coro_avg = averager() >>> next(coro_avg) # 코루틴 기동 >>> coro_avg.send(10) # 값을 반환하지 않음 >>> coro_avg.send(30) >>> coro_avg.send(6.5) >>> coro_avg.send(None) # 종료 조건 Traceback (most recent call last): File '', line 1, in StopIteration: Result(count=3, average=15.5)
StopIteration 예외와 함께 값을 반환합니다. 아래처럼 이 예외를 잡아서 값을 처리하는 방법이 코루틴이 반환하는 값을 사용하는 정석입니다.
>>> try: ... coro_avg.send(None) ... except StopIteration as exc: ... result = exc.value ... >>> result Result(count=3, average=15.5)
코루틴과 yield from
코루틴에서 yield from은 호출자와 하위 제너레이터 간의 양방향 채널을 열어주는 역할을 합니다.
상위 제너레이터에서 yield from을 사용하여 하위 제너레이터를 호출하면 호출자에서 하위 제너레이터의 흐름을 제어하면서 데이터를 전달하고 또 받을 수 있습니다.
다음은 yield를 이용한 averager() 코루틴 예제입니다.
위처럼 grouper()라는 상위 코루틴에서 yield from을 이용해서 averager()라는 하위 코루틴을 호출하게 되면, 호출자에서 상위 코루틴을 통해서 값을 전달할 수 있고, 또 하위 코루틴에서 값을 호출자로 전달할 수 있게 됩니다.
아래는 위 코드를 실행하는 예제입니다.
>>> data = { ... 'girls;kg': ... [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5], ... 'girls;m': ... [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43], ... 'boys;kg': ... [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3], ... 'boys;m': ... [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], ... } >>> main(data) 9 boys averaging 40.42kg 9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m
감사합니다.
이 포스트는 루시아누 하말류의 책 [Fluent Python]을 참고해서 작성했습니다.
Get to know us better! Join our official channels below.
Telegram(EN) : t.me/Humanscape KakaoTalk(KR) : open.kakao.com/o/gqbUQEM Website : humanscape.io Medium : medium.com/humanscape-ico Facebook : www.facebook.com/humanscape Twitter : twitter.com/Humanscape_io Reddit : https://www.reddit.com/r/Humanscape_official Bitcointalk announcement : https://bit.ly/2rVsP4T Email : support@humanscape.io