본문 바로가기
언어 정리/python_lib,일급함수

3_yield기반의 coroutine 코루틴

by 알 수 없는 사용자 2022. 6. 14.

 

참고

https://blog.humminglab.io/posts/python-coroutine-programming-1/

 

Python 비동기 프로그래밍 제대로 이해하기(1/2) - Asyncio, Coroutine

Python2 와 비교하여 python3의 가장 돋보이는 killer feature 는 비동기 프로그래밍 지원이라고 할 수 있다. 이를 위하여 python 3.4에 asyncio 모듈이 추가되었고, python 3.5 에는 native coroutine 지원을 위한 async

blog.humminglab.io

- 이 블로그 참조, 내가 더 궁금한건 더 추가해서 설명함. 

- 거의 따라 쓴 부분이 많은 정도 


목차 

  • iterator
  • yield 키워드와 generator
  • yield from
  • asyncio
  • async, await
  • future

coroutine 은 2가지가 있음

Yield 기반의 coroutine

await 기반의 coroutine

차이는 python 버전 따라 바뀜


Yield 기반의 coroutine

위의 generator의 yield는 generator 함수내의 코드에서 호출하는 곳으로 데이타를 전달하는 것으로 볼수 있다. 즉 yield가 실행될 때 마다 해당 함수는 멈추고, 값을 next(x)를 호출한 함수로 전달하는 셈이다.

참고: 이 부분부터 generator와 coroutine이 혼용되어 설명하되는데, yield 기반의 coroutine은 generator와 동일하다고 보면 된다. Python이 버전업 되면서 generator의 기능이 yield 기반의 generator로 확장되었다고 이해하면 된다. 나중에 설명하겠지만 python 3.5 에서는 async로 coroutine을 지원하는데, 이는 native coroutine이라고 한다.

 

next() 기반의 코루틴

def callee():
	"처음 동작"
    yield 1
    "next() 함수 후 두 번째 동작 부분"
    yield 2
    "next() 함수 후 세 번째 동작 부분"
		
x = callee()
i = next(x)
i = next(x)

 

위에 코드를 도식화하면 이런 형상임

 

여기서 포인트는 
"""결과적으로 보면 두개의 함수가 제어권을 핑퐁하면서 값을 전달하는 셈이다. 하나 빠진 것이 오른쪽에서 왼쪽으로 값은 전달할 수 있지만 반대 방향으로는 값을 전달하지 못한다."""

 

 

 

 

 

 

 

 

 

 

이와 같은 부분(상호전달이 안되는 부분)을 보완하여 상호간에 데이타와 함께 제어권을 전달하는 방법이 python 2.5에 yield 기반으로 확장된 coroutine 이다(PEP 342 – Coroutines via Enhanced Generators)

여기서 부터 python은 단일 thread로 다수의 작업(coroutine)을 concurrent(병행) 하게 실행 할 수 있는 coroutine이 갖추어진 셈이다.

 

next() + send() 기반의 코루틴

def coroutine1():
    print("sub_routine's callee start 1")
    x = yield 1
    print("sub_routine's callee 2: %d \t" % x)
    x = yield 2
    print("sub_routine's callee 3: %d \t" % x)

task = coroutine1()
i = next(task)
print("main's i : ",i)
i = task.send(10)
print("main's i : ",i)
task.send(20)
print("main's end")		#출력안됨. "task.send(20)"이 후 __iter__로 뽑아낼 다음이 없기 때문에

출력값 :

 

간단히 말하자면

메인루틴과 서브루틴이 있는데

"서브루틴 -> 메인루틴"으로 보내는 경우엔
- "x = yield 1"   --> "next(task)"와 "task.send(?)의 retrun 값이 1이 됨

 

"메인루틴 -> 서브루틴"으로 보내는 경우엔 
"task.send(10)"  -->  "yield ?"의 return 값이 10이 됨 
하지만 처음 시작은 "next(task)"로 해줘야함. 메인루틴에서는 send를 하든 next를 하든 서브루틴의 yield 부분은 넘어간다.

위에 코드를 도식화하면 이런 형상임

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Coroutine은 generator와 yield 에서 값을 받을 수 있다는 것을 제외하고는 모든 것이 동일하다. coroutine1()을 실행하면 동일하게 코드가 실행되는 것이 아니라 generator 객체가 리턴된다. 이를 실행 시키려면 마찬가지로 처음에 next(task) 처럼 실행해 주어야 한다. 이렇게 되면 yield 를 만날때 까지 실행된다. 이때 ‘callee 1’ 문장이 출력되고, next()로 값 1을 반환한다. 이후 부터가 generator와 사용방법이 다른 부분이다. 처음 한번 next()를 호출한 이후로는 coroutine.send()를 이용하여 next() 와 유사하게 coroutine의 동작을 재개한다. next()와의 차이점은 값을 coroutine으로 전달 할 수 있다는 것이다. 위 예에서는 10을 coroutine에 전달하여 ‘callee 2: 10’이 출력되고 다음 yield를 만나서 다시 2를 리턴한다.

처음에 next()를 호출하여야 한다는 부분이 조금 맘에 들지 않지만, 어쨋든 비동기 프로그램이 가능한 구조가 갖추어 졌다.

하지만 이것만으로는 부족하다. Generator의 경우 generator 에서 caller로는 try-finally 제한은 있지만 exception을 전달할 수 있다. 또한 coroutine의 마지막 라인까지 실행되면 StopIteration exception으로 종료를 알릴 수 도 있다. 그렇지만 반대로 caller에서 coroutine으로 exception을 전달할 방법이 없고, caller를 parent task라는 개념으로 봤을 때 child인 coroutine을 종료시킬 방법도 없다. 이를 지원하기 위하여 다음과 같은 사항이 추가되었다.

  • generator에서 yield문은 try-finally로 감쌀 수 없었으나, python 2.5부터는 이를 지원한다. 이렇게 되면 coroutine->caller 로의 예외 전달은 모두 지원하게 된다.
  • Caller에서 yield(또는 생성 직후)로 멈춰있는 coroutine에 excetion 전달 지원. send()와 비슷한 방법으로 throw(type, value, traceback) 처럼 exception을 coroutine에 전달할 수 있다. Parameter는 raise의 parameter와 동일하다.
  • Caller에서 coroutine을 종료 시킬 수 있는 close() 도 추가하였다. 이를 위하여 GeneratorExit exception이 추가되었고, close()는 throw()를 이용하여 GeneratorExit exception을 coroutine에 전달한다.

위와 같이 exception과 종료가 추가되어서, 비동기 방식의 프로그래밍을 위한 부분이 완료된 셈이다.

위에말이 이해가 잘안됌..ㅎㅅㅎ

 

 

Yield from 

이제 부터는 본격적으로 python3에서 지원되는 기능이다.

일반 함수나 task도 1:1 통신을 하는 경우 말고, a <-> b <-> c 와 같이 통신을 하는 경우도 많다(task가 sub task를 재 호출하는 방식). Coroutine도 마찬가지이다. Coroutine이 다시 sub-coroutine을 호출하는 구조가 될 수 있다. 예제로 보면 다음과 같이 될 것이다.

def subcoroutine():
    yield 1
    yield 2
    
def coroutine():
    for v in subcoroutine():
        yield v
        
x = coroutine()
print(next(x))    # 1 출력
print(next(x))    # 2 출력 
next(x)           # StopIteration

따라서 next(x)함수 호출 시 coroutine()함수가 호출이 되고, for문 안에서 iterable한 subcoroutine()함수가 차례대로 실행되서 v에 return 시켜준다. -> 그러면 "yield 1" 리턴 --> 메인에서 next(x) 하면 --> "yield 2" 리턴 해주는 형식

 

하지만 yield base coroutine에서 추가된 send(), throw(), close()를 지원하려면 단순히 이것으로만 되지 않는다. 예를 들어, 간단히 send()만 지원하게 하려면 중간의 coroutine()은 다음과 같이 수정되어야 한다.

def subcoroutine():
    print("Subcoroutine")
    x = yield 1
    print("Recv: " + str(x))
    x = yield 2
    print("Recv: " + str(x))

def coroutine():
    _i = subcoroutine()
    _x = next(_i)
    while True:
        _s = yield _x

        if _s is None:
            _x = next(_i)
        else:
            _x = _i.send(_s)

테스트 :

>>> from test import *
>>> x = coroutine()
>>> next(x)
Subcoroutine
1
>>> x.send(10)
Recv: 10
2
>>> x.send(20)
Recv: 20
StopIteration

중간에 _s의 값을 보고 next() send()를 구분하는데, 위 코드에서는 그냥 _x = _i.send(_s) 를 호출해도 된다. send(None) next()와 동일하다. 하지만 _i가 위처럼 generator가 아니라 iterator라면 반드시 next()를 해주어야 한다(iterator는 __next__()만 구현되고, send()는 없다). 여기에다 throw(), close() 등의 예외 사항을 추가한다면 복잡한 일이다.

위와 같이 coroutine에서 sub-coroutine을 호출하여 결과적으로 caller <=> sub-coroutine 이 데이타를 주고 받게 하려면 위와 같이 복잡하게 구현을 하지 않고 yield from을 사용하면 된다.

위 subcoroutine() 함수(yield로 구현)을 "yield from"으로 구현하면 ex)

def subcoroutine():
    print("Subcoroutine")
    x = yield 1
    print("Recv: " + str(x))
    x = yield 2
    print("Recv: " + str(x))

def coroutine():
    yield from subcoroutine()

 

 

yield vs yield from 코드차이 (둘다 동일함)

ex1)

def number_generator():
    x = [1, 2, 3]
    for i in x:
        yield i
 
for i in number_generator():
    print(i)

==

def number_generator():
    x = [1, 2, 3]
    yield from x    # 리스트에 들어있는 요소를 한 개씩 바깥으로 전달
 
for i in number_generator():
    print(i)

둘다 실행결과 같음

1
2
3

yield from 은 (yield + 반복문)으로 만들지 않고 사용하기 위해 만들어진 것

yield from 반복가능한 객체

yield from 이터레이터

yield from 제네레이터 객체

 

ex2)

def subcoroutine():
    print("Subcoroutine")
    x = yield 1
    print("Recv: " + str(x))
    x = yield 2
    print("Recv: " + str(x))

def coroutine():
    _i = subcoroutine()
    _x = next(_i)
    while True:
        _s = yield _x

        if _s is None:
            _x = next(_i)
        else:
            _x = _i.send(_s)

==

def subcoroutine():
    print("Subcoroutine")
    x = yield 1
    print("Recv: " + str(x))
    x = yield 2
    print("Recv: " + str(x))

def coroutine():
    yield from subcoroutine()

둘다 실행결과 같음

>>> from test import *
>>> x = coroutine()
>>> next(x)
Subcoroutine
1
>>> x.send(10)
Recv: 10
2
>>> x.send(20)
Recv: 20
StopIteration

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'언어 정리 > python_lib,일급함수' 카테고리의 다른 글

4_Asyncio,coroutine  (0) 2022.06.15
5_iter_gen_yield기반coroutine 함수기반정리  (0) 2022.06.15
2_generator  (0) 2022.06.13
1_iterator  (0) 2022.06.12
tts_lib 정리  (0) 2022.05.16

댓글