본문 바로가기
BASIC

코루틴

by 자동매매 2023. 11. 28.

1. 코루틴 사용하기

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

 

이 소스 코드에서 calc 함수와 add 함수의 관계를 살펴보겠습니다. calc가 메인 루틴(main routine)이면 add calc의 서브 루틴(sub routine)입니다. 이 메인 루틴과 서브 루틴의 동작 과정을 그림으로 나타내면 다음과 같은 모양이 됩니다.


메인 루틴에서 서브 루틴을 호출하면 서브 루틴의 코드를 실행한 뒤 다시 메인 루틴으로 돌아옵니다. 특히 서브 루틴이 끝나면 서브 루틴의 내용은 모두 사라집니다. 즉, 서브 루틴은 메인 루틴에 종속된 관계입니다.

 

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

 

 

이처럼 코루틴은 함수가 종료되지 않은 상태에서 메인 루틴의 코드를 실행한 뒤 다시 돌아와서 코루틴의 코드를 실행합니다. 따라서 코루틴이 종료되지 않았으므로 코루틴의 내용도 계속 유지됩니다.

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

 

1) 코루틴에 값 보내기

코루틴은 제너레이터의 특별한 형태입니다.

제너레이터는 yield로 값을 발생시켰지만 코루틴은 yield로 값을 받아올 수 있습니다. 다음과 같이 코루틴에 값을 보내면서 코드를 실행할 때는 send 메서드를 사용합니다.

그리고 send 메서드가 보낸 값을 받아오려면 (yield) 형식으로 yield를 괄호로 묶어준 뒤 변수에 저장합니다.

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

 

먼저 next(co)로 코루틴의 코드를 최초로 실행하면 x = (yield) yield에서 대기하고 다시 메인 루틴으로 돌아옵니다.

 

 

 

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

 

 

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

 

값을 보내지 않고 코루틴의 코드 실행하기

값을 보내지 않으면서 코루틴의 코드를 실행할 때는 next 함수(__next__ 메서드)만 사용하면 됩니다. 잘 생각해보면 이 방식이 일반적인 제너레이터입니다.

 

코루틴

def number_coroutine():
    i=0
    while True:
        x = (yield i)
        i +=1


co = number_coroutine()
for i in range(10):
    print(next(co))

 

제너레이터

def number_coroutine():
    i = 0
    while True:
        yield i
        i +=1
        

co = number_coroutine()
for i in range(10):
    print(next(co))

 

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 예외가 발생합니다.

따라서 이 예외를 처리하면 코루틴의 종료 시점을 알 수 있습니다.

코루틴 안에서 try except GeneratorExit 예외가 발생하면 '코루틴 종료'가 출력되도록 만들었습니다. 이렇게 하면 close 메서드로 코루틴을 종료할 때 원하는 코드를 실행할 수 있습니다.

 

def number_coroutine():
    try:
        while True:
            x = (yield)
            print(x, end=' ')
    except GeneratorExit:    # close 메서드를 호출하면 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(예외이름, 에러메시지)

다음은 코루틴에 숫자를 보내서 누적하다가 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
                                                      # 코루틴의 except에서 yield로 전달받은 값
예외로 코루틴 끝내기
190
 

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

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

2) 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

 

 

 

5) 코루틴의 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

 

6) 코루틴 초기화를 자동화할 수 없나요?

코루틴 객체를 생성한 뒤 next를 호출하는 모양이 마음에 들지 않는다면 다음과 같이 코루틴 초기화 데코레이터를 만들어서 사용하면 됩니다.

즉, 데코레이터 안에서 코루틴 객체를 만들고 next를 호출한 뒤 객체를 반환합니다

def coroutine(func):  # 코루틴 초기화 데코레이터
    def init(*args, **kwargs):
        co = func(*args, **kwargs)  # 코루틴 객체 생성
        next(co)  # next 호출
        return co  # 코루틴 객체 반환

    return init


@coroutine  # 코루틴 초기화 데코레이터 지정
def sum_coroutine():
    total = 0
    while True:
        x = (yield total)
        total += x


co = sum_coroutine()  # 코루틴 객체를 생성한 뒤 바로 사용

print(co.send(1))  # 1
print(co.send(2))  # 3
print(co.send(3))  # 6

 

이터레이터와 제너레이터의 차이는 무엇인가요?

제너레이터는 이터레이터를 반환하는 함수입니다. 따라서 모든 제너레이터 객체는 이터레이터입니다. 그러나, 이터레이터가 항상 제너레이터 객체인 것은 아닙니다.

 

코루틴의 이점은 무엇인가요?

보통 두 루틴이 동시에 실행되는 멀티태스킹은 하나의 변수에 값을 동시에 쓰면(write) 동기화 문제가 발생합니다. 따라서 크리티컬 섹션, 세마포어, 뮤텍스 등 동기화를 위한 락(lock)이 필요합니다. 하지만, 코루틴은 시분할 방식 멀티태스킹이라 동기화를 위한 락이 필요하지 않습니다.

 

 

 

[ 연습문제 ]

다음 소스 코드를 완성하여 문자열에서 특정 단어가 있으면 True, 없으면 False가 출력되게 만드세요. find 함수는 코루틴으로 작성해야 합니다.

def find(word):
    result = False
    while True:
        line = (yield result)
        result = word in line      

f = find('Python')
next(f)

print(f.send('Hello, Python!'))
print(f.send('Hello, world!'))
print(f.send('Python Script'))

f.close()
True
False
True

 

 

[ 연습문제 ]

표준 입력으로 사칙연산 계산식이 여러 개 입력됩니다.

다음 소스 코드에서 각 계산식의 결과를 구하는 코루틴을 만드세요.

계산식은 문자열 형태이며 값과 연산자는 공백으로 구분됩니다. 그리고 값은 정수로 변환하여 사용하고, 나눗셈은 / 연산자를 사용하세요.

def calc():
    result = 0
    while True:
        ex = yield result
        target = ex.split()
        x = int(target[0])
        y = int(target[2])
        if target[1] == "+":
            result = x + y
        if target[1] == "-":
            result = x - y
        if target[1] == "*":
            result = x * y
        if target[1] == "/":
            result = x / y


expressions = input().split(", ")
c = calc()
next(c)
for e in expressions:
    print(c.send(e))

c.close()

 

[ 입력 ]
1 + 2, 4 - 9
[ 결과 ]
3
-5

[ 입력 ]
3 * 4, 10 / 5, 20 + 39
[ 결과 ]
12
2.0
59

'BASIC' 카테고리의 다른 글

정규표현식  (0) 2023.11.28
데코레이터  (1) 2023.11.28
제너레이터  (0) 2023.11.28
이터레이터  (0) 2023.11.28
회문 판별과 N-gram 만들기  (1) 2023.11.28

댓글