본문 바로가기

Python/Python

[Python] 제네레이터 기반 Coroutine(코루틴)

반응형

이번 포스팅에서는 제네레이터 기반 코루틴에 대해서 알아보자. 코루틴은 Python 3.5 이상부터는 aysnc와 await 키워드를 사용하는 Native 코루틴과 def 와 yield 또는 yield from을 함께 사용하는 제네레이터 기반 코루틴이 존재한다. 여기서는 후자에 대해 알아보자.

 


1. 코루틴이란 무엇일까?

우선 코루틴에 대해 개념부터 살펴보자. 코루틴에 대해서 알아보기 전에 메인루틴과 서브루틴에 대해서도 잠깐 짚어보고 넘어가자. 우선 다음과 같은 함수가 있다고 가정해보자.

 

def calc_func(x):
	# 서브 루틴 영역
	sum_val = 0
	for _ in range(5):
    	sum_val += x
    return sum_val
    
# 메인 루틴 영역
result = calc_func(100)
print(result)

 

위 함수는 특정 숫자를 5번 더한 결과값인 sum_val을 리턴하는 일반적인 함수이다. 예시에서 메인 루틴, 서브 루틴을 구분지어주면 위와 같다. 즉, 특정 숫자를 5번 더해주는 과정을 하는 함수 내부가 서브 루틴, 그리고 그 함수를 직접적으로 호출하는 부분을 메인 루틴이라고 볼 수 있다. 보통은 위 예시 코드처럼 메인 루틴이 호출하면 서브 루틴이 정해진 일을 수행하고 결과값만을 전달해주고 종단된다. 결국 서브 루틴이 메인 루틴에 종속적인 관계를 갖는다고 볼 수 있다.

 

하지만 코루틴은 위와 같은 메인과 서브 루틴 간의 종속적인 관계가 아닌 대등한 관계로 메인 루틴과 서브 루틴 간에 서로 반복적으로 호출할 수 있게 해주는 것이다. 여기서 서브 루틴을 코루틴 함수라고 보고 메인 루틴이 코루틴 함수를 호출하는 것이라고 보면 되겠다. 단, 어쨌건 코루틴을 수행해주기 위해서는 최초에 코루틴 함수를 호출하긴 해야 하므로 코루틴의 최초 시작은 메인 루틴에서 시작한다고 볼 수 있겠다. 그리고 최초 호출이 진행되면 계속적으로 메인과 서브 루틴 간에 서로를 호출하면서 제어권을 주고 받게 된다.

 

그러면 이제 코루틴을 사용한 예제 소스코드를 보고 한줄씩 해석해보자.

 

def coroutine2(x):
    print('>> Coroutine2 started: {}'.format(x))
    y = yield x
    print('>> Coroutine2 Y received: {}'.format(y))
    z = yield x + y  
    print('>> Coroutine2 Z received: {}'.format(z))
    yield z + x + y
    
    
cr3 = coroutine2(10)
next1 = next(cr3)
print('next1:', next1)

send_y = cr3.send(100)
print('send_y:', send_y)

send_z = cr3.send(777)
print('send_z:', send_z)

 

문법이 좀 특이하다. 먼저 코루틴 함수인 coroutine2 라는 내부 스코프에서 한 줄씩 살펴보자.  방금 코루틴을 수행하기 위해서는 메인 루틴에서 최초로 코루틴 함수를 호출해주어야 한다고 했다. 그래서 처음에 cr3 = coroutine2(10) 이라는 문법으로 x에 10이라는 숫자를 넣어주고 next() 함수롤 호출해서 코루틴 함수를 최초로 호출한다.

 

그렇게 되면 코루틴 함수 내부에서는 첫 번째 print 문이 실행되고 y = yield x 부분이 실행된다. 이 때, y = yield x 를 예시로 들면, 등호(=)를 기준으로 오른쪽 부분인 yield x 는 코루틴 함수(서브 루틴)가 메인 루틴으로 x값을 return 해주는 역할을 한다. 그리고 왼쪽 부분인 y는 이제 서브 -> 메인이 아닌 메인 -> 서브로 전달할 입력값을 의미한다. 그리고 실질적으로 서브 루틴이 하는 일은 yield x로 메인 루틴으로 x 값을 리턴해주고 메인 루틴이 y값을 입력시킬 때까지 잠시 대기하게 된다.

 

그리고 다음에는 send() 함수로 이제 메인 루틴에서 서브 루틴으로 입력 데이터를 전달해준다. 위 코드에서는 100을 전달했다. 즉, 코루틴 내부 함수에서 y값에 100을 전달한 것이다. 이렇게 메인 루틴에서 send() 함수를 호출해서 y값을 전달해주면 다시 서브 루틴은 두번째 print 문을 출력하고 z = yield x + y 라인까지 실행한다. 이 때도 마찬가지로 실질적으로 x+y값을 해서 메인 루틴으로 값을 전달해주고 메인 루틴에서 z라는 입력값을 입력해줄 때까지 대기한다.

 

이렇게 메인루틴과 서브루틴이 yield 구문을 통해 반복적으로 데이터를 주고  받는다. 다시 말해 메인루틴에서 입력값을 요청하고 서브루틴에서 입력값을 넣어주는 반복 형태를 지닌다. 그렇기 때문에 입력을 주고 받는 지점을 기준으로 제어권이 메인,서브 루틴 간에 오고 가고 하는 것이다. 참고로 필자도 처음에 y = yield x 구문을 보고 반사적으로 등호(=)라는 표시 때문에 y랑 x 값이 같다라고만 생각해서 이해가 잘 가지 않았다. 그럴 때마다 계속 등호(=)를 기준으로 왼쪽은 메인루틴에서 입력할 값, 오른쪽은 서브 루틴에서 반환하는 값이라고 생각했다.

 

위 예시 코드에 주석을 달아서 코루틴을 단계별로 설명하면 아래와 같다.

 

def coroutine2(x):
    print('>> Coroutine2 started: {}'.format(x))
    y = yield x  # y는 메인루틴에서 입력할 값, yield x는 서브루틴에서 메인루틴으로 보내는 값
    print('>> Coroutine2 Y received: {}'.format(y))
    z = yield x + y  # z는 메인루틴에서 입력할 값, yield x+y는 서브루틴에서 메인루틴으로 보내는 값
    print('>> Coroutine2 Z received: {}'.format(z))
    yield z + x + y

    ## z 와 yield x+y 등식이라는 것이 아니라 그냥 왼쪽은 메인루틴에서 입력시킬 값, 오른쪽은 서브->메인으로 보낼 값만을 의미하는 것임 오해X

cr3 = coroutine2(10)
next1 = next(cr3)  # -> 호출 후 메인루틴에서 y입력할 때까지 대기 상태
print('next1:', next1)

send_y = cr3.send(100)   # -> 메인->서브로 y입력 후 서브루틴에서 yield x+y를 수행 후 반환한 다음 z입력 때까지 대기 상태
print('send_y:', send_y)

send_z = cr3.send(777)   # 메인->서브로 z값 입력 후 서브루틴에서 z+x+y 수행 후 반환한 상태
print('send_z:', send_z)

2. yield 와 yield from

yield와 yield from의 기능성 차이는 없지만 yield from을 사용하면 iterable한 객체를 사용할 때 코드가 좀 더 간결화 된다. 아래 비교 코드를 보면 이해가 갈 것이다.

 

# 중첩 코루틴
def generator1():
    for x in 'ABC':
        yield x
    for i in range(0, 4):
        yield i

t1 = generator1()
print(list(t1))


# 위와 같은 방식을 yield from 방식으로 변경 가능
def generator2():
    yield from 'ABC'
    yield from range(0, 4)

t2 = generator2()
print(list(t2))

 

# Reference

- 파이썬의 코루틴

 

반응형