29. 코루틴(Coroutine)
1. 키워드
- 코루틴(Coroutine)
- 메인 루틴(Main Routine)과 서브 루틴(Sub Routine)
2. 코루틴 사용하기
- 지금까지 함수를 호출한 뒤 함수가 끝나면 현재 코드로 다시 돌아왔다.
- 예를 들어, 다음과 같이
calc
함수 안에서add
함수를 호출했을 때add
함수가 끝나면 다시calc
함수로 돌아온다. - 그리고
add
함수가 끝나면 이 함수에 들어있던 변수와 계산식은 모두 사라진다.
def add(a, b):
c = a + b
print(c)
print("add 함수")
def calc():
add(1, 2)
print("calc 함수")
calc()
# 3
# add 함수
# calc 함수
- 이 소스 코드에서
calc
함수와add
함수의 관계를 살펴보자. calc
가 메인 루틴이면add
는calc
의 서브 루틴이다.
- 이 메인 루틴과 서브 루틴의 동작 과정을 그림으로 나타내면 다음과 같은 모양이 된다.
- 메인 루틴에서 서브 루틴을 호출하면 서브 루틴의 코드를 실행한 뒤 다시 메인 루틴으로 돌아온다.
- 특히 서브 루틴이 끝나면 서브 루틴의 내용은 모두 사라진다.
- 즉, 서브 루틴은 메인 루틴에 종속된 관계이다.
- 하지만 코루틴은 방식이 조금 다르다.
- 코루틴은 Cooperative Routine을 의미하는데, 서로 협력하는 루틴이라는 뜻이다.
- 즉, 메인 루틴과 서브 루틴처럼 종속된 관계가 아니라 서로 대등한 관계이며 특정 시점에 상대방의 코드를 실행한다.
- 이처럼 코루틴은 함수가 종료되지 않은 상태에서 메인 루틴의 코드를 실행한 뒤 다시 돌아와서 코루틴의 코드를 실행한다.
- 따라서 코루틴이 종료되지 않았으므로 코루틴의 내용도 계속 유지된다.
- 일반 함수를 호출하면 코드를 한 번만 실행할 수 있지만, 코루틴은 코드를 여러 번 실행할 수 있다.
- 참고로 함수의 코드를 실행하는 지점을 진입점(Entry Point)이라고 하는데, 코루틴은 진입점이 여러 개인 함수이다.
3. 코루틴에 값 보내기
- 코루틴은 제너레이터의 특별한 형태이다.
- 제너레이터는
yield
로 값을 발생시켰지만 코루틴은yield
로 값을 받아올 수 있다.
- 다음과 같이 코루틴에 값을 보내면서 코드를 실행할 때는
send
메서드를 사용한다. - 그리고
send
메서드가 보낸 값을 받아오려면(yield)
형식으로yield
를 괄호로 묶어준 뒤 변수에 저장한다.
- 그럼 코루틴에 숫자
1
,2
,3
을 보내서 출력해 보자.
def number_coroutine():
while True:
x = (yield)
print(x)
co = number_coroutine()
next(co)
co.send(1) # 1
co.send(2) # 2
co.send(3) # 3
- 먼저 코루틴
number_coroutine
은while True:
로 무한히 반복하도록 만든다. - 왜냐하면 코루틴을 종료하지 않고 계속 유지시키기 위해 무한 루프를 사용한다.
- 그리고
x = (yield)
와 같이 코루틴 바깥에서 보낸 값을 받아서x
에 저장하고,print
로x
의 값을 출력한다.
- 코루틴 바깥에서는
co = number_coroutine()
과 같이 코루틴 객체를 생성한 뒤next(co)
(co.__next__()
를 호출해도 상관없다.)로 코루틴 안의 코드를 최초로 실행하여yield
까지 코드를 실행한다.
- 그다음에
co.send
로 숫자1
,2
,3
을 보내면 코루틴 안에서 숫자를 받은 뒤print
로 출력한다.
- 이 코루틴의 동작 과정을 그림으로 살펴보자.
- 먼저
next(co)
로 코루틴의 코드를 최초로 실행하면x = (yield)
의yield
에서 대기하고 다시 메인 루틴으로 돌아온다.
- 그다음에 메인 루틴에서
co.send(1)
로1
을 보내면 코루틴은 대기 상태에서 풀리고x = (yield)
의x =
부분이 실행된 뒤print(x)
로 숫자를 출력한다. - 이 코루틴은
while True:
로 반복하는 구조이므로 다시x = (yield)
의yield
에서 대기한다. - 그러고 나서 메인 루틴으로 돌아온다.
- 이런 과정으로
send
가 보낸 값을(yield)
가 받게 된다.
- 계속 같은 과정으로
send
를 사용하여 값을 보내면 코루틴에서 값을 받아서 출력한다. - 정리하자면
next
함수(__next__
메서드)로 코루틴의 코드를 최초로 실행하고,send
메서드로 코루틴에 값을 보내면서 대기하고 있던 코루틴의 코드를 다시 실행한다.
send
로 코루틴의 코드를 최초로 실행하기
- 지금까지 코루틴의 코드를 최초로 실행할 때
next
함수(__next__
메서드)를 사용했지만,코루틴객체.send(None)
과 같이send
메서드에None
을 지정해도 코루틴의 코드를 최초로 실행할 수 있다.
4. 코루틴 바깥으로 값 전달하기
- 지금까지 코루틴 안에 값을 보내기만 했는데 이번에는 코루틴에서 바깥으로 값을 전달해 보자.
- 다음과 같이
(yield 변수)
형식으로yield
에 변수를 지정한 뒤 괄호로 묶어주면 값을 받아오면서 바깥으로 값을 전달한다. - 그리고
yield
를 사용하여 바깥으로 전달한 값은next
함수(__next__
메서드)와send
메서드의 반환값으로 나온다.
- 그럼 코루틴에 숫자를 보내고, 코루틴은 받은 숫자를 누적해서 바깥에 전달해 보자.
def sum_coroutine():
total = 0
while True:
x = (yield total)
total += x
co = sum_coroutine()
print(next(co)) # 0
print(co.send(1)) # 1
print(co.send(2)) # 3
print(co.send(3)) # 6
- 코루틴에서 값을 누적할 변수
total
을 만들고0
을 할당한다. - 그리고
x = (yield total)
과 같이 값을 받아오면서 바깥으로 전달하도록 만든다. - 즉, 바깥에서
send
가 보낸 값은x
에 저장되고, 코루틴 바깥으로 보낼 값은total
이다. - 그다음에
total += x
와 같이 받은 값을 누적해 준다.
- 코루틴 바깥에서는
co = sum_coroutine()
과 같이 코루틴 객체를 생성한 뒤next(co)
로 코루틴 안의 코드를 최초로 실행하여yield
까지 코드를 실행하고,print
로next(co)
에서 반화된 값을 출력한다. - 그다음에
co.send
로 숫자1
,2
,3
을 보내고,print
로co.send
에서 반환된 값을 출력한다.
co = sum_coroutine()
print(next(co)) # 0
print(co.send(1)) # 1
print(co.send(2)) # 3
print(co.send(3)) # 6
- 참고로
next
와send
의 차이를 살펴보면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
로 숫자를 누적한다. - 이 코루틴은
while True:
로 반복하는 구조이므로 다시x = (yield total)
의yield
에서total
을 메인 루틴으로 전달하고 대기한다. - 그다음에 메인 루틴에서
print(co.send(1))
과 같이 코루틴에서 나온 값을 출력한다. - 여기서는
total
에1
이 들어있으므로1
을 받아와서 출력한다.
- 이런 과정으로
(yield total)
이 바깥으로 전달한 값을next
와send
의 반환값으로 받고,send
가 보낸 값을x = (yield total)
의x
가 받게 된다. - 여기서는
yield
를 사용하여 코루틴 바깥으로 값을 전달하면next
와send
의 반환값으로 받는다는 점만 기억하면 된다. - 마지막으로 제너레이터와 코루틴의 차이점을 정리해 보자.
1] 제너레이터
next
함수(__next__
메서드)를 반복 호출하여 값을 얻어내는 방식
2] 코루틴
next
함수(__next__
메서드)를 한 번만 호출한 뒤send
로 값을 주고 받는 방식
값을 보내지 않고 코루틴의 코드 실행하기
- 값을 보내지 않으면서 코루틴의 코드를 실행할 때는
next
함수(__next__
메서드)만 사용하면 된다. - 잘 생각해 보면 이 방식이 일반적인 제너레이터이다.
5. 코루틴을 종료하고 예외 처리하기
- 보통 코루틴은 실행 상태를 유지하기 위해
while True:
를 사용하여 끝나지 않는 무한 루프로 동작한다.
- 만약 코루틴을 강제로 종료하고 싶다면 다음과 같이
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
메서드는 코루틴의 종료 시점을 알아야 할 때 사용하면 편리하다.
1) GeneratorExit
예외 처리하기
- 코루틴 객체에서
close
메서드를 호출하면 코루틴이 종료될 때GeneratorExit
예외가 발생한다. - 따라서 이 예외를 처리하면 코루틴의 종료 시점을 알 수 있다.
def number_coroutine():
try:
while True:
x = (yield)
print(x, end=" ")
except 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
# 코루틴 종료
- 코루틴 안에서
try except
로GeneratorExit
예외가 발생하면"코루틴 종료"
가 출력되도록 만들었다. - 이렇게 하면
close
메서드로 코루틴을 종료할 때 원하는 코드를 실행할 수 있다.
2) 코루틴 안에서 예외 발생시키기
- 이번에는 코루틴 안에 특정 예외를 발생시켜서 코루틴을 종료해 보자.
- 코루틴 안에 예외를 발생시킬 때는 다음와 같이
throw
메서드를 사용한다. throw
는 말 그대로 던지다라는 뜻인데 예외를 코루틴 안으로 던진다.- 이때
throw
메서드에 지정한 에러 메시지는except as
의 변수에 들어간다.
- 다음은 코루틴에 숫자를 보내서 누적하다가
RuntimeError
예외가 발생하면 에러 메시지를 출력하고 누적된 값을 코루틴 바깥으로 전달한다.
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
- 코루틴 안에서
try except
로RuntimeError
예외가 발생하면print
로 에러 메시지를 출력하고yield
를 사용하여total
을 바깥으로 전달하도록 만들었다.
- 코루틴 바깥에서는
co.throw(RuntimeError, "예외로 코루틴 끝내기")
와 같이throw
메서드에RuntimeError
예외와 에러 메시지를 지정하면 코루틴 안에서 예외가 발생한다. - 그리고 코루틴 안의
except
에서yield
를 사용하여 바깥으로 전달한 값은throw
메서드의 반환값으로 나온다. - 여기서는 코루틴 안에서 예외 처리를 했으므로
"예외로 코루틴 끝내기"
가 출력되고, 코루틴 바깥에서는 누적된 값190
이 출력된다.
6. 하위 코루틴의 반환값 가져오기
- 제너레이터에서
yield from
을 사용하면 값을 바깥으로 여러 번 전달할 수 있었다. - 하지만 코루틴에서는 조금 다르게 사용하는데,
yield from
에 코루틴을 지정하면 해당 코루틴에서return
으로 반환한 값을 가져온다.
- 다음은 코루틴에서 숫자를 누적한 뒤 합계를
yield from
으로 가져온다.
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(1, 11):
co.send(i)
co.send(None) # 55
for i in range(1, 101):
co.send(i)
co.send(None) # 5050
- 코루틴에
1
부터10
까지 보내서 합계55
를 구하고, 다시1
부터100
까지 보내서 합계5050
을 구했다.
- 먼저 다음과 같이 숫자를 받아서 누적할 코루틴을 만든다.
x = (yield)
와 같이 코루틴 바깥에서 값을 받아온 뒤total
에 계속 더한다.- 특히 이 코루틴은
while True:
로 무한히 반복하지만 코루틴을 끝낼 방법이 필요하다. - 여기서는 바깥에서 받아온 값이
None
이면return
으로total
을 반환하고 코루틴을 끝낸다.
- 이제 합계를 출력할 코루틴을 만든다.
- 먼저
while True:
로 무한히 반복한다. - 그리고
total = yield from accumulate()
와 같이yield from
을 사용하여 코루틴accumulate
의 반환값을 가져온다.
- 코루틴에서
yield from
을 사용하면 코루틴 바깥에서send
로 하위 코루틴까지 값을 보낼 수 있다. - 따라서
co = sum_coroutine()
으로 코루틴 객체를 만든 뒤co.send
로 값을 보내면accumulate
에서 값을 받는다.
co.send
로 숫자를 계속 보내다가 누적을 끝내고 싶으면None
을 보내면 된다.
- 이때
accumulate
는None
을 받으면 코루틴이 완전히 끝나지만sum_coroutine
에서 무한 루프로 반복하고 있으므로print
로total
을 출력한 뒤 다시yield from accumulate()
로accumulate
를 실행하게 된다.
코루틴의 yield from
으로 값을 발생시키기
- 이번 예제에서 하위 코루틴은
x = (yield)
와 같이 코루틴 바깥에서 보낸 값만 받아왔다. - 하지만 코루틴에서
yield
에 값을 지정해서 바깥으로 전달했다면yield from
은 해당 값을 다시 바깥으로 전달한다.