[python] 코루틴 사용하기

코루틴에 값 보내기

여태는 함수를 호출한 뒤 함수가 끝나면 현재 코드로 다시 돌아왔다.
예를 들어서, 아래처럼 calc 안에서 add를 호출하면, add가 끝나면
add에 들어있던 변수와 계산식이 모두 사라지고 calc 함수로 돌아오게 된다.

def add(a, b):
    c = a + b    # add 함수가 끝나면 변수와 계산식은 사라짐
    print(c)
    print('add 함수')

def calc():
    add(1, 2)    # add 함수가 끝나면 다시 calc 함수로 돌아옴
    print('calc 함수')

calc()  # 3
        # add 함수
        # calc 함수

위 코드에서 calc가 메인 루틴이라면 add는 서브 루틴이다.
동작 방식을 그림으로 나타내면 아래와 같다.

메인 루틴에서 서브 루틴을 호출하면 서브 루틴의 코드를 실행한 뒤
서브 루틴의 내용을 모두 없애고 메인 루틴으로 돌아온다.
즉, 서브 루틴은 메인 루틴에 종속된 관계라고 볼 수 있다.

하지만 코루틴(coroutine)은 그 방식이 조금 다른데,
메인 루틴과 서브 루틴처럼 어느 한 쪽이 종속된 관계가 아니라
서로 대등한 관계이며 특정 시점에 각각의 코드를 실행한다.
그 동작 방식을 그림으로 나타낸다면 아래와 같다.

이처럼 코루틴은 함수가 종료되지 않은 상태에서 메인 루틴의 코드를 실행한 후
다시 돌아와서 코루틴의 코드를 실행한다.
코루틴 함수는 종료된 것이 아니므로 코루틴의 내용도 계속 유지된다.
일반 함수를 호출하면 코드를 한 번만 실행할 수 있지만, 코루틴은 여러번 실행할 수 있다.

코루틴은 제너레이터의 특별한 형태이다.
제너레이터는 yield로 값을 발생시켰지만, 코루틴은 yield로 값을 받아올 수 있다.
아래와 같이 코루틴에 값을 보내면서 코드를 실행할 때는
코루틴객체.send(값) 형태로 코드를 실행해주고,
send된 값을 코루틴에서 받으려면
변수 = (yield) 로 받아올 수 있다.
아래 예제 코드를 보자.

def number_coroutine():
    while True:  # 코루틴을 계속 유지하기 위해 무한 루프 사용
        x = (yield)
        print(x)

co = number_coroutine()
next(co)  # 코루틴 안의 yield까지 코드 실행(최초 실행), 코루틴은 yield에서 대기
            # co.send(None) 으로도 가능하다.
co.send(1)  # 1
co.send(2)  # 2
co.send(3)  # 3

코루틴 바깥으로 값 전달하기

이번에는 코루틴 바깥으로 값을 전달해보자
(yield 변수) 형식으로 변수를 지정한 뒤 괄호로 묶어주면 값을 받아오며 바깥으로 값을 전달한다.
이렇게 바깥으로 전달한 값은 next 함수와 send 메소드와 반환값으로 나온다.

def sum_coroutine():
    total = 0
    while True:
        x = (yield total)  # 코루틴 바깥에서 값을 받아오고 코드 실행 후 바깥으로 값 전달
        total += x


co = sum_coroutine()
print(next(co))  # 0: 코루틴 안의 yield까지 코드를 실행하고 코루틴에서 나온 값 출력

print(co.send(1))  # 1: 코루틴에 숫자 1을 보내고 코루틴에서 나온 값 출력
print(co.send(2))  # 3: 코루틴에 숫자 2를 보내고 코루틴에서 나온 값 출력
print(co.send(3))  # 6: 코루틴에 숫자 3을 보내고 코루틴에서 나온 값 출력

코루틴을 종료하고 예외 처리하기

보통 코루틴은 실행 상태를 유지하기 위해 무한 루프로 동작하는데,
코루틴을 강제로 종료하고 싶다면 close 메소드를 사용하면 된다.
코루틴객체.close()

def sum_coroutine():
    total = 0
    while True:
        x = (yield total)  # 코루틴 바깥에서 값을 받아오고 코드 실행 후 바깥으로 값 전달
        total += x


co = sum_coroutine()
next(co)
for i in range(10):
    print(co.send(i), end=' ')  # 0 1 3 6 10 15 21 28 36 45
co.close()
print()
# print(co.send(1))  # 이미 종료된 코루틴에 send를 하면 StopIteration 예외가 발생한다.

코루틴 객체에서 close 메소드를 호출한다면 GeneratorExit 예외가 발생한다.
따라서 이 예외를 처리하면 코루틴의 종료 시점을 알 수 있다.

def number_coroutine():
    try:  # 코루틴으로 실행할 코드
        while True:
            x = (yield)
            print(x, end=' ')
    except GeneratorExit as e:  # 코루틴이 종료 될 때 GeneratorExit 예외 발생
        print(e)
        print('코루틴 종료')


co = number_coroutine()
next(co)

for i in range(10):
    co.send(i)  # 0 1 2 3 4 5 6 7 8 9

co.close()  # 코루틴 종료

코루틴 안에 특정 예외를 발생시키고 싶다면 throw 메소드를 사용한다.
이 때 throw 메소드에 지정한 에러 메시지는 except as 의 변수에 들어간다.
코루틴객체.throw(예외, 에러메시지)

def sum_coroutine2():
    try:
        total = 0
        while True:
            x = (yield)
            total += x
    except RuntimeError as e:
        print(e)
        yield total  # 코루틴 바깥으로 값 전달


co = sum_coroutine2()
next(co)

for i in range(10):
    co.send(i)

print(co.throw(RuntimeError, '예외로 코루틴 끝내기'))  # 예외로 코루틴 끝내기
                                                    # 45 (코루틴의 except에서 yield로 전달받은 값)

하위 코루틴의 반환 값 가져오기

제너레이터에서 yield from을 사용하면 값을 바깥으로 여러 번 전달한다.
코루틴에서는 yield from을 사용하면 해당 코루틴에서 return으로 반환한 값을 가져온다.
변수 = yield from 코루틴()
코루틴에서 yield from을 사용하면 코루틴 바깥에서 send로 하위 코루틴까지 값을 보낼 수 있다.
따라서 아래 예제 코드에서 send한 값들이 sum_coroutine을 거쳐 accumulate까지 전달된다.

def accumulate():
    total = 0
    while True:
        x = (yield)
        if x is None:
            return total
        total += x


def sum_coroutine():
    while True:
        total = yield from accumulate()
        print(total)


co = sum_coroutine()
next(co)

for i in range(0, 11):
    co.send(i)  # 1부터 10을 코루틴 accumulate에 보낸다.
co.send(None)  # 55, None을 accumulate에 보내서 누적 끝낸다.

코루틴도 제너레이터라 return을 사용하면 StopIteration이 발생한다.
그래서 코루틴에서 return 값은 raise StopIteration(값) 으로 대체할 수 있다.(파이썬 3.6이하에서만)
파이썬 3.7부터는 raise로 StopIteration 예외를 직접 발생시키면 RuntimeError로 바뀐다.
3.7부터는 그냥 return 값 을 쓸 것

코루틴의 yield from으로 하위 코루틴에서 yield한 값을 바깥으로 전달할 수 있다.

def number_coroutine():
    x = None
    while True:
        x = (yield x)  # 코루틴 바깥에서 값을 받아오면서 바깥으로 값을 전달
        if x == 3:
            return x


def print_coroutine():
    while True:
        x = yield from number_coroutine()  # 하위 코루틴의 yield에 지정된 값을 다시 바깥으로 전달
        print('print_coroutine:', x)


co = print_coroutine()
next(co)

x = co.send(1)
print(x)  # 1 number_coroutine의 yield에서 바깥으로 전달한 값
x = co.send(2)
print(x)  # 2 number_coroutine의 yield에서 바깥으로 전달한 값
co.send(3)  # 3을 보내서 반환값을 출력하도록 만듦

Updated:

Leave a comment