[ python ] asyncio (2)
[ async, await ]
python 3.5에서는 coroutine을 명시적으로 지정하는 async와 yield를 대체하는 await keyword가 추가되었습니다.
https://peps.python.org/pep-0492/
이를 기존의 yield를 사용하는 generator based coroutine과 구분하기 위하여 native coroutine이라고 합니다. 기존 generator based coroutine은 함수 내에 yield 유무로 결정되나, native coroutine은 함수 앞에 async키워드를 붙여서 사용합니다. async함수에는 기존 문법인 yield(from)을 사용할 수 없고, await를 사용합니다. (await를 사용하지 않아도 coroutine이 됩니다 / generator based coroutine은 함수 안에 yield포함 여부로 결정)
yield from을 대체하기 위해 await오른쪽에 다음과 같은 사항이 올 수 있습니다.
- native coroutine object
- 기존 generator based coroutine object (정확히는 새로 추가된 @types.coroutine decorator를 붙인 generator)
- __await__ method를 가진 iterator
# 자세히 보면 await는 yield from은 대체할 수 있지만, yield의 모든 기능을 대체할수 없습니다.
generator의 기능은 빼고, asyncio와 같이 비동기 concurrent 프로그래밍을 위한 것이라고 이해하면 좋습니다.
Python 3.5에서 예외 처리도 보완이 되었는데, 중첩된 coroutine에서 StopIteration이 발생 시 어느 것의 exception 인지 처리가 모호해지는 문제를 위하여 coroutine 밖으로 전파될때는 StopIteration이 RuntimeError로 변경 (PEP 479 – Change StopIteration handling inside generators)되었습니다.
[ async internal ]
async와 await를 좀 더 깊게 살펴보겠습니다. async/await가 추가되면서 확장된 data model을 보면 다음과 같습니다.
awaitable object :
- __await__()가 구현된 객체, async def함수를 호출하여 리턴되는 native coroutine이 awaitable객체
- object.__await__(self)에서 iterator가 리턴되어, await에서 사용된다. Future의 경우도 __await__()가 구현되어 await에 사용할 수 있는 것입니다. generator based coroutine과 유사하게 __await__()로 받은 iterator를 send()를 이용하여 반복
coroutine object : awaitable object이고, coroutine.send(), throw(), close()가 구현되어 있습니다.
asynchronous iterators : 기존의 iterator와 비슷하게 __aiter__(), __anext__() method가 구현된 객체, async for에 사용됨
asynchronous context managers :
- 기존 with에서 사용하던 객체와 비슷하게 __aenter__(), __aexit__() method가 구현된 객체, async with에 사용됨
[ async with, async for ]
async with은 with다음에 클래스 인스턴스를 지정하고 as 뒤에 결과를 저장할 변수를 지정합니다.
async with으로 동작하는 클래스를 만들려면 __aenter__()과 __aexit__() method를 구현해야 합니다. 메서드를 구현할 때는 반드시 async def를 사용하여 작성합니다.
import asyncio
class AsyncAdd:
def __init__(self, a, b):
self.a = a
self.b = b
async def __aenter__(self):
await asyncio.sleep(1.0)
return self.a + self.b # __aenter__에서 값을 반환하면 as에 지정한 변수에 들어감
async def __aexit__(self, exc_type, exc_value, traceback):
pass
async def main():
async with AsyncAdd(1, 2) as result: # async with에 클래스의 인스턴스 지정
print(result) # 3
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
__aexit__() method는 async with as를 완전히 벗어나면 호출됩니다. (여기서는 특별히 만들 부분이 없으므로 pass)
async for로 동작하는 클래스를 만들려면 __atier__(), __anext__() method를 구현해야 합니다. 마찬가지로 method를 작성할 때는 async def를 사용합니다. method 앞에 a가 붙은 것을 제외하면 iterator만드는 법과 동일합니다. 반복을 끝낼 때는 StopAsyncIteration 예외를 발생시킵니다. native coroutine을 사용할 때는 앞에 await를 사용합니다.
# 위에서는 async함수에서는 yield를 사용할 수 없다고 했는데, python 3.6부터 PEP 525에서 asynchronous generator를 지원하여 yield 10과 같이 데이터를 생성하는 로직을 await에서 구현할 수 없으므로 async함수에서 yield사용을 허용
async for에서 해당 기능을 사용하여 asynchronous iterator를 사용합니다. (data producer역할을 할 수 있는 generator의 기능을 추가)
import asyncio
async def async_counter(stop): # 제너레이터 방식으로 만들기
n = 0
while n < stop:
yield n
n += 1
await asyncio.sleep(1.0)
async def main():
async for i in async_counter(3): # for 앞에 async를 붙임
print(i, end=' ')
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
[ 참고하면 좋을 링크 ]
https://peps.python.org/pep-3156/
https://it-eldorado.tistory.com/159
https://peps.python.org/pep-0492/