파이썬 | 코루틴

CHOI·2022년 1월 7일
0

Python

목록 보기
31/33
post-thumbnail

지금까지 함수를 호출 한 뒤 함수가 끝나면 원래 함수로 돌아왔다.

def add(a, b):
    c = a + b    # add 함수가 끝나면 변수와 계산식은 사라짐
    print(c)
    print('add 함수')
 
def calc():
    add(1, 2)    # add 함수가 끝나면 다시 calc 함수로 돌아옴
    print('calc 함수')
 
calc()

calc 함수에서 add 함수를 호출하고 add 함수가 끝나면 다시 calc 함수로 돌아온다. 특히, add 함수가 끝나면 이 함수에 있던 변수와 계산식이 모두 사라진다.

이 소스 코드에서 두 함수의 관계를 살펴보자. calc 함수가 메인 루틴(main routine)이면 addcalc 의 서브 루틴(sub routine)이다. 이 메인 루틴과 서브 루틴을 살펴보면 다음과 같다.

메인 루틴에서 서브 루틴을 호출하면 서브 루틴이 실행되고 나서 다시 메인 루틴으로 돌아온다. 이때 서브 루틴이 끝나면 서브 루틴의 모든 내용이 사라진다. 즉, 서브 루틴은 메인 루틴에 종속된 관계이다.

하지만 코루틴의 방식은 다르다. 코루틴(coroutine)은 cooperative routine를 의미하며 서로 협력하는 루틴이라는 뜻이다. 즉, 메인 루틴과 서브 루틴과 같은 종속 관계가 아니라 서로 대등한 관계이며 특정 시점에 상대방의 코드를 실행한다.

코루틴은 함수가 종료되지 않은 상태로 메인 루틴의 코드를 실행하고 다시 돌아와서 코루틴의 코드를 실행한다. 따라서 코루틴은 종료되지 않았으므로 코루틴의 내용도 계속 남아있다.

일반 함수를 호출하면 코드를 한 번만 실행할 수 있지만(한 번만 호출할 수 있다는 것이 아니다), 코루틴은 코드를 여러 번 실행할 수 있다. 참고로 함수의 코드를 실행하는 부분을 진입점(entry point)라고 하며 코루틴은 진입점이 여러 개 인 함수이다.

💡 이번 단원에서 배울 코루틴은 초보자가 이해하기 어려운 내용이다. 코루틴을 모르더라도 파이썬을 하는데 큰 지장이 없다. 이 부분이 어렵게 느껴진다면 넘거가도 괜찮다. 나중에 익숙해지면 다시 와서 봐도 된다.

1. 코루틴에 값 보내기

코루틴은 제너레이터의 특별한 형태이다. 제너레이터는 yield 로 값을 발생시켰지만 코루틴은 yield 로 값을 받아올 수 있다. 다음과 같이 코루틴에 값을 보내면서 코드를 실행할 때는 send 메서드를 사용한다. 그리고 send 메서드로 보낸 값을 받아오려면 (yield) 형식으로 yield 를 괄호로 묶어준 뒤 변수에 저장한다.

  • 코루틴객체.send(값)
  • 변수 = (yield)

그러면 코루틴에 숫자 1, 2, 3을 보내보자.

def number_coroutine():
    while True:        # 코루틴을 계속 유지하기 위해 무한 루프 사용
        x = (yield)    # 코루틴 바깥에서 값을 받아옴, yield를 괄호로 묶어야 함
        print(x)
 
co = number_coroutine()
next(co)      # 코루틴 안의 yield까지 코드 실행(최초 실행)
 
co.send(1)    # 코루틴에 숫자 1을 보냄
co.send(2)    # 코루틴에 숫자 2을 보냄
co.send(3)    # 코루틴에 숫자 3을 보냄

실행 결과

1
2
3

먼저 코루틴 number_coroutinewhile Ture: 로 무한히 반복한다. 왜냐하면 코루틴을 종료하지 않고 계속 유지하기 위해서 무한 루프를 사용한다(코루프를 종료하는 방법은 뒤에서 설명하겠다). 그리고 x = (yield) 와 같이 코루틴 바깥에서 보낸 값을 변수에 저장하고 print(x) 로 출력한다.

def number_coroutine():
    while True:        # 코루틴을 계속 유지하기 위해 무한 루프 사용
        x = (yield)    # 코루틴 바깥에서 값을 받아옴, yield를 괄호로 묶어야 함
        print(x)

코루틴 바깥에서는 co = number_coroutine() 과 같이 코루틴 객체를 생성한 뒤 next(co) 로 코루틴 안의 코드를 최초로 실행하여 yield 까지 코드를 실행한다( co.__next__() 를 호출해도 상관 없다).

  • next(코루틴객체)
co = number_coroutine()
next(co)      # 코루틴 안의 yield까지 코드 실행(최초 실행)

그 다음에 co.send 로 값을 보내면 코루틴 안에서 값을 받은 뒤에 출력한다.

co.send(1)    # 코루틴에 숫자 1을 보냄
co.send(2)    # 코루틴에 숫자 2을 보냄
co.send(3)    # 코루틴에 숫자 3을 보냄

다시 정리해보면 다음과 같다. next(co) 로 코루틴을 실행하면 x = (yield)yield 에서 대기하고 다시 메인 루틴으로 돌아온다.

그리고 co.send(1) 로 1 을 보내면 코루틴은 대기 상태에서 풀린 뒤 x = (yield) 에서 yield 값에 1이 들어가고 print(x) 로 1이 출력된다. 그리고 다시 반복문을 돌아서 x = (yield) 에서 대기 상태가 되고 메인 루틴으로 돌아간다. 그리고 이러한 과정이 반복된다.

next 함수( __next__ 메서드)로 코루틴을 최초로 실행하고 send 메서드로 코루틴에값을 보내면 대기 하고 있던 코루틴이 다시 실행된다.

% send 로 코루틴 최초 실행

지금까지 코루틴을 최초 실행할 떄 next 함수( __next__ 메서드) 를 사용했지만 코루틴 객체.send(None) 과 같이 send 메서드에 None 을 지정하여도 코루틴의 코드를 최초로 실행할 수 있다.

2. 코루틴 바깥으로 값 보내기

지금까지는 코루틴 안으로 값이 보내졌는데 이번에는 코루틴에서 바깥으로 값을 전달해보자. 다음과 같이 ( yield 변수) 형식으로 yield 에 변수를 지정한 뒤에 괄호로 묶어주면 값을 받아오고 바깥으로 값을 전달한다. 그리고 yield 를 통해 바깥으로 전달된 값은 next 함수( __next__ 메서드 ) 와 send 메서드의 반환값으로 나온다.

  • 변수 = (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을 보내고 코루틴에서 나온 값 출력

실행 결과

0
1
3
6

코루틴에서 값을 누적할 변수 total 을 만들고 0을 할당한다. 그리고 x = (yield total) 과 같이 값을 받아오면서 바깥으로 값을 전달하도록 만든다. 즉, 바깥에서 send 로 전달한 값은 x 에 저장되고 total 의 값을 코루틴 바깥으로 전달한다. 그 다음에 total += x 와 같이 받은 값을 누적한다.

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

코루틴 바깥에서는 co = sum_coroutine() 와 같이 코루틴 객체를 생성한 뒤에 next(co) 로 코루틴 안의 코드를 최초로 실행하여 yield 까지 실행하고 printnext(co) 에서 반환된 값을 출력한다. 그 다음에 co.send 로 1, 2, 3을 받고 printco.send 에서 반환된 값을 출력한다.

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을 보내고 코루틴에서 나온 값 출력

nextsend 의 차이점을 보면 next 는 코드를 실행하지만 값을 보내지 않을 때 사용하고 send 는 코드를 실행하고 값을 보낼 때 사용한다.

전체 과정을 다시 한 번 자세히 살펴보면 먼저 next(co) 로 코루틴의 코드가 실행되고 x = (yield total)yield 에서 total 을 메인 루틴으로 전달하고 대기한다.

그 다음에 메인 루틴에서 print(next(co)) 와 같이 코루틴에서 받은 값을 출력하는데 여기에서는 total 에 0이 들어있으므로 0을 받고 출력한다.

그리고 co.send(1) 로 1을 보내면 코루틴은 대기 상태에서 풀리고 x = (yield total)x = 부분이 실행 된 뒤에 total += x 로 숫자를 누적한다. 이 코루틴은 무한루프 이므로 다시 x = (yield total) 에서 total 의 값을 바깥으로 전달하고 대기한다. 그리고 print(co.send(1)) 와 같이 코루틴에서 나온 값을 출력하는데 total 에는 1이 들어 있기 때문에 1을 출력한다.

이러한 과정으로 (yield total) 에서 전달된 값을 nextsend 의 반환값으로 받고, send 에서 전달된 값을 x = (yield total)x 가 받는다.

코루틴과 제너레이터의 차이점

  • 제너레이터는 next 함수(next 메서드)를 반복 호출하여 값을 얻어내는 방식
  • 코루틴은 next 함수(next 메서드)를 한 번만 호출한 뒤 send로 값을 주고 받는 방식

3. 코루틴 종료 및 예외처리

보통 코루틴은 실행 상태를 유지하기 위해서 while True: 와 같이 무한 루프 반복문으로 실행한다. 그렇다면 코루틴을 종료하고 싶다면 어떻게 할까? close 메서드를 사용하면 된다.

  • 코루틴객체.close()

다음은 코루틴에 숫자 20개를 보낸 뒤에 종료한다.

def number_coroutine():
    while True:
        x = (yield)
        print(x, end=' ')
 
co = number_coroutine()
next(co)
 
for i in range(20):
    co.send(i)
 
co.close()    # 코루틴 종료

실행 결과

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

코루틴 객체에서 close 메서드를 사용하면 코루틴이 종료된다. 사실 파이썬 스크립트가 끝나면 코루틴도 종료되기 때문에 close 를 사용하지 않을 때와 별 차이가 없다. 하지만 close 는 코루틴의 종료 시점을 알아야 할 때 사용하면 유용하다.

GeneratorExit 예외

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

def number_coroutine():
    try:
        while True:
            x = (yield)
            print(x, end=' ')
    except GeneratorExit:    # 코루틴이 종료 될 때 GeneratorExit 예외 발생
        print()
        print('코루틴 종료')
 
co = number_coroutine()
next(co)
 
for i in range(20):
    co.send(i)
 
co.close()
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
코루틴 종료

코루틴 안에서 예외 발생시키기

코루틴 안에서 특정한 예외를 발생시킬 때는 어떻게 할까? throw 메서드를 사용하면 된다. throw 는 말 그대로 던지다 라는 뜻인데 코루틴 안으로 예외를 던지는 것이다. 이때 throw 메서드에 지정한 에러 메시지는 except as 의 변수에 들어간다.

  • 코루틴객체.throw(예외이름, 에러메시지)
def sum_coroutine():
    try:
        total = 0
        while True:
            x = (yield)
            total += x
    except RuntimeError as e:
        print(e)
        yield total    # 코루틴 바깥으로 값 전달
 
co = sum_coroutine()
next(co)
 
for i in range(20):
    co.send(i)
 
print(co.throw(RuntimeError, '예외로 코루틴 끝내기')) # 190
                                                      # 코루틴의 except에서 yield로 전달받은 값

실행 결과

예외로 코루틴 끝내기
190

위와 같이 코루틴 바깥에서 throwRuntimeError 예외를 던지면 코루틴 안에서 예외가 발생한다 그리고 except 안에서 total 을 바깥으로 전달한 값은 throw 메서드의 반환값으로 나온다.

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

제너레이터에서 yield from 을 사용하면 값을 여러 번 바깥으로 전달할 수 있다고 했다(제너레이터- yield from 참조). 하지만 코루틴에서는 조금 다르게 사용한다. yield from 에서 코루틴을 지정하면 해당 코루틴에서 return 으로 반환한 값을 가져온다.

  • 변수 = yield from 코루틴()

다음은 코루틴에서 숫자를 누적한 뒤에 합계를 yield from 으로 가져온다.

def accumulate():
    total = 0
    while True:
        x = (yield)         # 코루틴 바깥에서 값을 받아옴
        if x is None:       # 받아온 값이 None이면
            return total    # 합계 total을 반환
        total += x
 
def sum_coroutine():
    while True:
        total = yield from accumulate()    # accumulate의 반환값을 가져옴
        print(total)
 
co = sum_coroutine()
next(co)
 
for i in range(1, 11):    # 1부터 10까지 반복
    co.send(i)            # 코루틴 accumulate에 숫자를 보냄
co.send(None)             # 코루틴 accumulate에 None을 보내서 숫자 누적을 끝냄
 
for i in range(1, 101):   # 1부터 100까지 반복
    co.send(i)            # 코루틴 accumulate에 숫자를 보냄
co.send(None)             # 코루틴 accumulate에 None을 보내서 숫자 누적을 끝냄

실행 결과

55
5050

코루틴에서 1부터 11까지 보내서 합계 55를 구하고 1부터 101 까지 보내서 합계 5050을 구했다.

먼저 숫자를 받아서 누적할 코루틴을 만든다. x = (yield) 와 같이 코루틴 바깥에서 숫자를 받아서 total 에 누적을 한다. while 문을 반복하면 계속 누적하는데 만약 코루틴 바깥에서 None 을 받으면 total 을 반환하면서 코루틴이 끝나게 된다.

def accumulate():
    total = 0
    while True:
        x = (yield)         # 코루틴 바깥에서 값을 받아옴
        if x is None:       # 받아온 값이 None이면
            return total    # 합계 total을 반환, 코루틴을 끝냄
        total += x

이제 합계를 출력할 코루틴을 만든다. 먼저 while True 로 무한히 반복한다. yield from 으로 코루틴 accumulate 의 반환값을 가져온다.

def sum_coroutine():
    while True:
        total = yield from accumulate()    # accumulate의 반환값을 가져옴
        print(total)

코루틴에서 yield from 을 사용하면 코루틴 바깥에서 send 로 하위 코루틴까지 값을 보낼 수 있다. 따라서 co = sum_coroutine() 으로 코루틴 객체를 받은 뒤에 co.send 로 값을 보내면 accumulate 에서 값을 받는다.

co = sum_coroutine()
next(co)
 
for i in range(1, 11):    # 1부터 10까지 반복
    co.send(i)            # 코루틴 accumulate에 숫자를 보냄

co.send 로 계속 숫자를 보내다가 누적을 끝내고 싶으면 None 을 보내면 된다.

co.send(None)             # 코루틴 accumulate에 None을 보내서 숫자 누적을 끝냄

이때 accumulateNone 을 받으면 코루틴이 완전히 끝나게 된다. 그러나 sum_coroutine 에서 무한 루트로 계속 돌고 있으므로 print(total) 을 한 다음에 다시 yield form accumulate()accumulate 가 다시 실행되게 된다.

def sum_coroutine():
    while True:
        total = yield from accumulate()    # accumulate가 끝나면 yield from으로 다시 실행
        print(total)

코루틴에서 StopIteration 예외

코루틴도 제너레이터이므로 return 을 하면 StopIteration 예외가 발생한다. 그래서 코루틴에서 return 값raise StopIteration(값) 형태로 사용할 수도 있다(파이썬 3.6 이하). 아래의 코드와 같이 raiseStopIteration 예외를 발생시킨 다음에 값을 지정해주면 yield from 으로 값을 가져올 수 있다(단, 파이썬 3.7 이상부터는 제너레이터 안에서 rasie StopIteration 예외를 직접 발생시켜면 자동으로 RuntimeError 로 바뀌므로 이 방법을 사용할 수 없다 파이썬 3.7 이상부터는 그냥 return 을 하자).

def accumulate():
    total = 0
    while True:
        x = (yield)                       # 코루틴 바깥에서 값을 받아옴
        if x is None:                     # 받아온 값이 None이면
            raise StopIteration(total)    # StopIteration에 반환할 값을 지정(파이썬 3.6 이하)
        total += x
 
def sum_coroutine():
    while True:
        total = yield from accumulate()    # accumulate의 반환값을 가져옴
        print(total)
 
co = sum_coroutine()
next(co)
 
for i in range(1, 11):    # 1부터 10까지 반복
    co.send(i)            # 코루틴 accumulate에 숫자를 보냄
co.send(None)             # 코루틴 accumulate에 None을 보내서 숫자 누적을 끝냄
 
for i in range(1, 101):   # 1부터 100까지 반복
    co.send(i)            # 코루틴 accumulate에 숫자를 보냄
co.send(None)             # 코루틴 accumulate에 None을 보내서 숫자 누적을 끝냄

실행 결과

55
5050
💡 지금까지 코루틴에 대해서 알아보았다. 코루틴은 함수가 종료되지 않은 상태로 값을 주고 받을 수 있는 함수이며 이 과정에서 현재 코드의 실행을 대기하고 다른 코드를 실행한다는점이 중요하다. 보통 코루틴은 시간이 오래 걸리는 작업을 분할하여 처리할 때 사용하는데 주로 파일 처리, 네트워크 처리 등에서 사용한다. 코루틴이 당장 이해가 안된다 하여도 걱정할 필요없다. 현직 프로그래머들도 다소 어려워 하는 내용이다. 나중에 코루틴이 필요할 때 다시 돌아와서 학습하자.

% 코루틴의 yield from 으로 값 발생시키기

이전 예제에서는 x = (yield) 로 코루틴 바깥에서 보낸 값만 받았다. 하지만 코루틴에서 yield 로 값을 지정해서 바깥으로 값을 전달했다면 yield from 은 해당 값을 다시 바깥으로 전달한다.

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)    # number_coroutine으로 1을 보냄
print(x)          # 1: number_coroutine의 yield에서 바깥으로 전달한 값
x = co.send(2)    # number_coroutine으로 2를 보냄
print(x)          # 2: number_coroutine의 yield에서 바깥으로 전달한 값
co.send(3)        # 3을 보내서 반환값을 출력하도록 만듦

실행 결과

1
2
print_coroutine: 3
profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

0개의 댓글