티스토리 뷰

Python

[파이썬] 코루틴

안톨리니 2022. 7. 11. 23:27

Reference

  • Fluent Python

 

함수 안에 yield키워드가 있다면 그 함수는

제너레이터 함수다.

def generator():
    print('My Favorite Food is')
    yield 'Chicken'
    print('My Hobby is')
    yield 'Watching England Premier League'

generator() 함수를 호출하면

제너레이터 객체를 생성하고

호출자가 제너레이터 객체의 흐름을 제어할 수 있다.

 

호출자가 next()함수를 호출하면 제너레이터 객체는 값을 '생산'하고

그 값을 호출자에게 전달한다.

(호출자가 제너레이터객체로부터 값을 꺼내오는 형식)

 

그리고 다음 yield구문까지 함수를 진행하며

다음 yield구문에 도착했을때 호출자의 next() 함수를 다시 대기한다.

import inspect

def generator():
    print('My Favorite Food is')
    yield 'Chicken'
    print('My Hobby is')
    yield 'Watching England Premier League'


x = generator()  # 제너레이터 객체 생성
print(inspect.getgeneratorstate(x))
>>> GEN_CREATED  # 제너레이터가 생성된 상태 next()함수를 사용해야 제너레이터를 가동시킬 수 있음
print(next(x))
>>> My Favorite Food is
>>> Chicken
print(inspect.getgeneratorstate(x))
>>> GEN_SUSPENDED # 제너레이터가 중단된 상태 호출자의 next()함수를 대기

코루틴은 제너레이터와 같이 함수 안에 yield키워드를 가졌다.

 

위에서 설명했듯이 제너레이터 함수는 제너레이터 객체를 생성해서

호출자가 값을 꺼내오는듯한 형식이었지만

코루틴은 호출자가 값을 제너레이터에 보낼 수가 있다.

 

yield의 두가지 의미에 관한 여담

 

※ 참고로 Fluent Python에서 코루틴파트 초반 부분에 이런 문장이 나오는데

영어 사전에서'yield'단어를 찾아보면 '생산한다'와'양보한다'는 두 가지 뜻을 볼 수 있다.

 

Fluent Python에서는 제너레이터가 값을 '생산하고' 그 값을 '양보하는' 개념으로

yield의 두 가지 의미가 모두 적용된다라고 설명하지만

 

필자는 yield라는 키워드의 '생산한다'는 제너레이터에게 어울리고

'양보한다'라는 말은 코루틴에게 어울리는 거 같다.

 

코루틴은 호출자가 send()를 통해 값을 함수에 전달하는데

그런 모습이 마치 자신이 직접 사용하는 것보다

함수에 전달해서 사용하는 게 더 용이해서

호출자가 '양보하는'것처럼 느껴졌다.

 

(사실 별 큰 이유는 없다.)
(그냥 yield'라는 단어의 두 가지 의미가 각각 적용되는 느낌이 들어서 신기했음.)

 

 

 


코루틴
def generator():
    print('What is Your Favorite Food')
    favorite_food = yield
    print('My Favorite Food is', favorite_food)

코루틴으로 쓰이는 제너레이터 함수다.

일반적인 제너레이터 함수와 다른 점은

벌써 눈치챘겠지만

yield키워드 앞에 변수가 있다.

 

일반적인 제너레이터 함수와 같이 호출해서 제너레이터 객체를 만들고

next() 함수를 통해 제너레이터 객체를 가동해야 한다.

import inspect

def generator():
    print('What is Your Favorite Food?')
    favorite_food = yield
    print('My Favorite Food is', favorite_food)


x = generator()  # 제너레이터 객체 생성
print(inspect.getgeneratorstate(x))
>> > GEN_CREATED  # 제너레이터 객체가 생성된 상태

next(x)  # 제너레이터 함수는 next()호출시 yield문 뒤에 있는 값을 전달했지만
         # 위 제너레이터 함수는 yield문 뒤에 아무것도 없어서 None을 반환함

print(inspect.getgeneratorstate(x))
>> > GEN_SUSPENDED  # 제너레이터가 중단된 상태 yield문에서 호출자의 데이터 전송을 대기

inspect모듈의 getgeneratorstate함수를 통해

제너레이터의 상태를 확인해보면

'GEN_SUSPENDED'가 출력된다.

현재 yield문에서 대기하고 있는 상태이고

이제 send()를 통해 값을 전달해보겠다.

import inspect

def generator():
    print('What is Your Favorite Food?')
    favorite_food = yield
    print('My Favorite Food is', favorite_food)

x = generator()
next(x)
>>> What is Your Favorite Food?
x.send('Chicken')
>>> My Favorite Food is Chicken
>>> StopIteration

send()를 통해 'Chicken'이라는 값을 전달했다.

generator() 함수에 있는 yield의 값이 'Chicken'이 되면서

(favorite_food = 'Chicken')

마지막 출력 문장을 성공적으로 실행하고

StopIteration예외가 발생한다.

(다음 yield문이 없기 때문에)

 

조금 더 코루틴의 동작을 명확하게 이해하기 위해

Fluent Python에 있는 코드를 그대로 가져왔다.

def simple_coro2(a):
    print('-> Started: a =', a)
    b = yield a
    print('-> Received: b =', b)
    c = yield a + b
    print('-> Received: c =', c)


my_coro2 = simple_coro2(14)  # 제너레이터 객체 생성
from inspect import getgeneratorstate
print(getgeneratorstate(my_coro2))
>>> GEN_CREATED  # 제너레이터 객체가 생성된 상태
print(next(my_coro2))  # 제너레이터 가동
>>> -> Started: a = 14
>>> 14  # yield a --> 14
print(getgeneratorstate(my_coro2))
>>> GEN_SUSPENDED  # 제너레이터가 중단된 상태 yield문에서 호출자의 데이터 전송을 대기
print(my_coro2.send(28))
>>> -> Received: b = 28
>>> 42  # yield a + b --> 14 + 28 = 42
my_coro2.send(99)
>>> -> Received: c = 99
>>> StopIteration

yield에 할당 문이 있을 때

호출자가 값을 보내기 전에 yield문 뒤에 있는 문장을 먼저 호출자에게 생성해서 전달한다.

 

예시로 위에 있는 코드 중

[ b = yield a ]는

함수에 있는 로컬 변수 a가 먼저 호출자에게 전달되고

send()를 통해 호출자가 값을 전달해주면

b의 값이 결정되고 다시 다음 yield문이 있는 곳까지 실행된다.

(만약 yield문을 마주하게 되면 위 상황과 같이 먼저 yield문 뒤에 있는 값을 생성해서 전달하고 대기한다.)

 

이 그림을 보면

위 코드가 어떻게 동작하는지 이해하는데 큰 도움이 될 거라고 생각한다.

 

 

 


데커레이터를 통해 코루틴을 생성하자마자 바로 가동하기

 

제너레이터 함수로 제너레이터 객체를 만들고

꼭 next()를 한 번 호출해서

제너레이터를 가동해야 한다.(yield문에 도착)

 

코루틴을 편리하게 사용할 수 있도록

제너레이터 객체를 만들자마자 가동할 수 있는 방법은 없을까?

from functools import wraps
import inspect

def coroutine(func):
    """데커레이터: 'func'를 기동해서 첫 번째 'yield'까지 진행한다."""
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer


@coroutine
def simple_coro2(a):
    print('-> Started: a =', a)
    b = yield a
    print('-> Received: b =', b)
    c = yield a + b
    print('-> Received: c =', c)


x = simple_coro2(13)
print(inspect.getgeneratorstate(x))
>>> GEN_SUSPENDED

Fluent Python에서는 데코레이터를 통해서

이러한 문제를 해결했다.

 

제너레이터 함수를 호출했을 뿐인데

데코레이터 덕분에 primer 함수에서 next()를 한 번 호출한 다음에

객체를 반환하기 때문에

 

생성된 제너레이터를 바로 사용할 수 있는 상태로 만들어준다.

 

혹시나 제너레이터에 대해 잘 모른다면

이 코드를 참고하기 바란다.

(클로저는 알아야 이해할 수 있다.)

from functools import wraps
import inspect

def coroutine(func):
    """데커레이터: 'func'를 기동해서 첫 번째 'yield'까지 진행한다."""
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer



def simple_coro2(a):
    print('-> Started: a =', a)
    b = yield a
    print('-> Received: b =', b)
    c = yield a + b
    print('-> Received: c =', c)


x = coroutine(simple_coro2) # coroutine클로저형태 함수 매개변수에 코루틴함수 객체를 전달하면 
print(x)                    # primer함수 객체가 반환된다.
>>> <function simple_coro2 at 0x101739c10>
y = x(3)                    # pirmer함수를 호출하면 coroutine함수의 변수값을(자유변수) 사용하여
>>> -> Started: a = 3       # 다시 simple_coro2객체를 만들고 next()를 호출한 뒤 그 객체를 반환
print(y)
>>> <generator object simple_coro2 at 0x10172d4a0>
print(y.send(5))            
>>> -> Received: b = 5
>>> 8

사실 데코레이터를 응용한 부분은

어쩌면 당연한 내용이기 때문에 넘어가도 되는 부분이지만

필자가 이렇게 남긴 이유는

 

데코레이터를 어떤 부분에서 응용할 수 있는지 감을 잡지 못한

쌩초보자 필자의 시선을 사로잡았던 이유가 큰 거 같다.

👀

 

 

 


코루틴 종료와 예외 처리

 

코루틴 내부에서 예외가 발생하고 만약 그 예외가 처리되어 있는 상태가 아니라면

코루틴을 호출한 호출자에게 예외가 전파된다.

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count

 

 

averager코루틴 yield문에 정수나 실수가 아닌

비수치형값을 보내면

total = total + term 부분에서

total변수에 값을 더할 수 없어

타입 에러가 발생한다.

그리고 다시 코루틴에 정상적인 값을 전달하면

StopIteration예외가 발생한다.

 

코루틴 안에서 처리되지 않은 예외가 발생하면(Type Error) 코루틴이 종료된다.

 

위 코드를 통해
코루틴 내에 예외가 발생하면 코루틴이 종료되는걸 확인할 수 있다.

그렇다면 코루틴 내부에 예외 처리를 한다면

코루틴에 다양한 동작이 가능하도록 할 수 있지 않을까?

 

이번에는 averager코루틴에 TypeError예외를 처리하는 코드를 작성하고

다시 비수치형값을 전달해서 TypeError를 발생시켜보면

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        try:
            term = yield average
        except TypeError:
            print('잘못된 값을 보냈습니다')
        else:
            total += term
            count += 1
            average = total/count

coro_avg = averager()
next(coro_avg)
print(coro_avg.send(50))
>>> 50
print(coro_avg.send('Antoliny'))
>>> TypeError: unsupported operand type(s) for +=: 'float' and 'str'

필자의 예상과는 다르게 TypeError가 발생한다.

(※while True구문 밖에 예외처리를 하면 TypeError예외처리가 성공적으로 동작함으로써

다음 yield문까지 진행되어서 StopIteration예외가 발생)

 

코루틴 안에서 발생한 예외를 처리했음에도 불구하고

next()나 send()로 코루틴을 호출한 호출자에 예외가 전파되었다.

 

파이썬에서 제너레이터 객체에 throw라는 메서드가 있는데

throw메서드는 말 그대로 '던지다'라는 뜻이고

제너레이터에 명시적으로 예외를 던질 수 있다.

 

throw 함수를 통해 yield표현식에 예외를 전달했더니

예외처리 코드가 잘 동작하고 코루틴이 종료되지도 않는다.

def averager():
    total = 0.0
    count = 0
    average = None

    while True:
        try:
            term = yield average
        except TypeError:
            print('잘못된 값을 보냈습니다')
        else:
            total += term
            count += 1
            average = total/count


coro_avg = averager()
next(coro_avg)
print(coro_avg.send(50))
>>> 50.0
coro_avg.throw(TypeError)
>>> 잘못된 값을 보냈습니다
print(coro_avg.send(60))
>>> 55.0

위 예제 코드를 통해 클래스에 Exception상속으로 예외를 만든 뒤

throw메서드를 사용하면

코루틴에 여러 흐름들을 추가할 수 있지 않을까 싶다.

 

참고로 close()라는 메서드도 있는데

이 메서드를 사용하면 코루틴을 종료시킬 수 있다.

 

위에서 코루틴에 예외가 발생하면 코루틴이 종료된다는 걸 확인했듯이

close() 메서드도 사실 코루틴에 GeneratorExit 예외를 발생시켜서 코루틴을 종료시키는 구조다.

def averager():
    total = 0.0
    count = 0
    average = None

    try:
        while True:
            term = yield average
            total += term
            count += 1
            average = total / count
    except GeneratorExit:
        print('코루틴 종료')

coro_avg = averager()
next(coro_avg)
print(coro_avg.send(50))
>>> 50.0
coro_avg.close()
>>> 코루틴 종료
from inspect import getgeneratorstate
print(getgeneratorstate(coro_avg))
>>> GEN_CLOSED

 

 

 


코루틴으로 값 반환하기

 

마지막으로 코루틴을 통해 값을 반환해보겠다.

 

제너레이터 객체에 return문을 통해 값을 전달하면

StopIteration예외의 value속성에 return문 값이 담기고

StopIteration 예외가 발생한다.

 

코루틴도 결국은 제너레이터 객체이기 때문에

return문을 통해 값을 전달할 수 있다.

from collections import namedtuple

Result = namedtuple('Result', 'count average')

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        count += 1
        average = total/count
    return Result(count, average)

coro_avg = averager()
next(coro_avg)
while True:
    try:
        x = input()

        if x.isdigit():
            coro_avg.send(int(x))
        elif x == 'stop':
            coro_avg.send(None)

    except StopIteration as exc:
        result = exc.value
        print(result)
        break


>>> 100  --> x = input()
>>> 200  --> x = input()
>>> 300  --> x = input()
>>> stop  --> x = input()
>>> Result(count=3, average=200.0)

 

 

이전까지 봤던 averager예제와 달리

send()를 호출할 때마다 값을 반환하지 않고

사용자가 stop을 입력하면 yield구문에 None을 보내 코루틴을 빠져나오고 return문을 통해

이전까지 보냈던 누적 값을  namedtuple형으로 반환한다.

 

결국은 반환 값이 StopIteration예외의 value속성에 담긴 채로 반환된다는 것과

StopIteration예외를 처리하는 방식이

다음에 작성할 포스팅의 주제인 yield from구문을 이해하는데 큰 도움이 되는 포인트라고 생각한다.

(yield from의 경우 인터프리터가 StopIteration 예외를 처리할 뿐만 아니라 value 속성이

yield from표현식의 값이 된다.)

 

 

 


후기

 

코루틴은 뭔가 이름이 굉장히 어렵게 생겨서

시작하기도전에 겁을 먹었지만

생각보다 어려운 주제는 아니었던 거 같다.

 

하지만

코루틴은 그저 빙산의 일각이었다.

 

동시성 프로그래밍이라는 큰 주제가

날 기다리고 있었다.

 

설레지만 조금 무섭다.

 

동시성 프로그래밍 관련 포스팅은 조금 늦지 않을까 싶다. ㅠㅠ

C언어를 조금 배우고 할 생각이다.

댓글
공지사항