PEP 492 – async 및 await 구문
- Author:
- Yury Selivanov <yury at edgedb.com>
- Discussions-To:
- Python-Dev list
- Status:
- Final
- Type:
- Standards Track
- Created:
- 09-Apr-2015
- Python-Version:
- 3.5
- Post-History:
- 17-Apr-2015, 21-Apr-2015, 27-Apr-2015, 29-Apr-2015, 05-May-2015
Table of Contents
- 초록
- API 설계 및 구현 개정
- 근거 및 목표
- 사양
- 용어집
- 전환 계획
- 설계 고려 사항
- PEP 3152
- 코루틴 제너레이터
- “async” 및 “await” 키워드인 이유
- “__aiter__”가 awaitable을 반환하지 않는 이유
- “async” 키워드의 중요성
- 왜 “async def”인가
- 왜 “await for”와 “await with”가 아닌가
- 왜 “async def”이고 “def async”가 아닌가
- 왜 __future__ 임포트가 아닌가
- 왜 매직 메서드는 “a”로 시작하는가
- 왜 기존 매직 이름을 재사용하지 않는가
- 기존 “for” 및 “with” 문을 재사용하지 않는 이유는 무엇입니까?
- 컴프리헨션
- 비동기 람다 함수
- 성능
- 참조 구현
- 승인
- 구현
- 참고 자료
- 감사의 말
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
인터넷과 전반적인 연결성의 확산으로 인해 이에 비례하여 응답성과 확장성을 갖춘 코드에 대한 필요성이 커졌습니다. 이 제안은 명시적으로 비동기적이고 동시적인 Python 코드를 더 쉽게, 더욱 Python답게 작성할 수 있도록 하여 이러한 필요성에 대응하고자 합니다.
Python에서 코루틴을 적절한 독립 개념으로 만들고 이를 지원하는 새로운 구문을 도입할 것을 제안합니다. 궁극적인 목표는 Python의 비동기 프로그래밍에 대한 공통적이고 쉽게 접근할 수 있는 멘털 모델을 확립하고, 이를 동기 프로그래밍에 최대한 가깝게 만드는 데 있습니다.
이 PEP는 비동기 작업이 asyncio.events.AbstractEventLoop 표준 라이브러리 모듈의 것과 유사한 이벤트 루프에 의해 예약되고 조정된다고 가정합니다. 이 PEP는 특정 이벤트 루프 구현에 종속되지는 않지만, 이벤트(예: IO)가 완료될 때까지 코루틴이 대기하게 됨을 스케줄러에 알리는 신호로 yield를 사용하는 종류의 코루틴에만 관련됩니다.
많은 다른 언어가 유사한 기능을 채택했거나 채택할 계획인 만큼, 여기에서 제안하는 변경 사항이 빠르게 성장하는 비동기 프로그래밍 분야에서 Python의 관련성과 경쟁력을 유지하는 데 도움이 될 것이라고 믿습니다: [2], [5], [6], [7], [8], [10].
API 설계 및 구현 개정
- Python 3.5의 초기 베타 릴리스에 대한 피드백으로 인해 이 PEP를 지원하는 객체 모델을 네이티브 코루틴과 제너레이터에서 더 명확하게 분리하도록 재설계하게 되었습니다. 네이티브 코루틴은 새로운 종류의 제너레이터가 아니라 이제 완전히 별개의 고유한 타입입니다([17]에 구현됨).
이 변경은 네이티브 코루틴 지원을 Tornado 웹 서버에 통합하려고 시도하면서 발생한 문제를 주된 원인으로 구현되었습니다([18]에 보고됨).
- CPython 3.5.2에서
__aiter__프로토콜이 업데이트되었습니다.3.5.2 이전에는
__aiter__가 awaitable을 반환하고, 이 awaitable이 비동기 이터레이터로 해석될 것으로 예상했습니다. 3.5.2부터__aiter__는 비동기 이터레이터를 직접 반환해야 합니다.3.5.2에서 이전 프로토콜을 사용하면 Python은
PendingDeprecationWarning을 발생시킵니다.CPython 3.6에서는 이전
__aiter__프로토콜이 계속 지원되지만DeprecationWarning이 발생합니다.CPython 3.7에서는 이전
__aiter__프로토콜이 더 이상 지원되지 않습니다.__aiter__가 비동기 이터레이터가 아닌 것을 반환하면RuntimeError가 발생합니다.
근거 및 목표
현재 Python은 제너레이터를 통한 코루틴 구현을 지원하며(PEP 342), PEP에서 도입된 yield from 구문으로 더욱 확장되었습니다.
380. 이 접근 방식에는 다음과 같은 여러 단점이 있습니다.
- 코루틴은 일반 제너레이터와 동일한 구문을 공유하므로 서로 혼동하기 쉽습니다. 이는 특히 새로운 개발자에게 해당됩니다.
- 함수가 코루틴인지 여부는 해당 본문에
yield또는yield from문이 존재하는지에 따라 결정되므로, 리팩터링 과정에서 이러한 문이 함수 본문에 나타나거나 사라질 때 명확하지 않은 오류가 발생할 수 있습니다. - 비동기 호출에 대한 지원은 구문상
yield를 허용하는 표현식으로 제한되므로,with및for문과 같은 구문 기능의 유용성이 제한됩니다.
이 제안은 코루틴을 Python의 네이티브 언어 기능으로 만들고 제너레이터와 명확하게 분리합니다. 이를 통해 제너레이터와 코루틴의 모호성이 제거되고, 특정 라이브러리에 의존하지 않고도 코루틴을 안정적으로 정의할 수 있습니다. 또한 린터와 IDE가 정적 코드 분석 및 리팩터링을 개선할 수 있습니다.
네이티브 코루틴과 이에 수반되는 새로운 구문 기능을 사용하면 컨텍스트 관리자 및 이터레이션 프로토콜을 비동기 방식으로 정의할 수 있습니다. 이 제안의 뒷부분에서 설명하듯이, 새로운 async with 문을 사용하면 Python 프로그램이 런타임 컨텍스트에 진입하고 종료할 때 비동기 호출을 수행할 수 있으며, 새로운 async for 문을 사용하면 이터레이터에서 비동기 호출을 수행할 수 있습니다.
사양
이 제안은 Python의 코루틴 지원을 강화하기 위한 새로운 구문과 의미론을 도입합니다.
이 명세는 Python에서 코루틴을 구현하는 방법에 대한 지식을 전제로 합니다 (PEP 342 및 PEP 380). 여기서 제안하는 구문 변경의 동기는 asyncio 프레임워크 (PEP 3156)와 “Cofunctions” 제안 (PEP 3152, 현재 이 명세를 채택하는 대신 거부됨)에서 비롯됩니다.
이 문서의 이 시점부터 새로운 구문을 사용하여 선언된 함수를 네이티브 코루틴이라고 합니다. 제너레이터 구문을 기반으로 하는 코루틴을 가리킬 때는 필요한 경우 제너레이터 기반 코루틴이라고 합니다. 두 정의가 모두 적용되는 문맥에서는 코루틴이라고 합니다.
새로운 코루틴 선언 구문
다음과 같은 새로운 구문을 사용하여 네이티브 코루틴을 선언합니다.:
async def read_data(db):
pass
코루틴의 주요 속성:
async def함수는await표현식을 포함하지 않더라도 항상 코루틴입니다.async함수에yield또는yield from표현식이 있으면SyntaxError가 발생합니다.- 내부적으로 두 개의 새로운 코드 객체 플래그가 도입되었습니다.
CO_COROUTINE은 새로운 구문으로 정의된 네이티브 코루틴을 표시하는 데 사용됩니다.CO_ITERABLE_COROUTINE은 제너레이터 기반 코루틴을 네이티브 코루틴과 호환되도록 만드는 데 사용됩니다(types.coroutine() 함수로 설정됩니다).
- 일반 제너레이터는 호출되면 제너레이터 객체를 반환하며, 코루틴도 마찬가지로 코루틴 객체를 반환합니다.
StopIteration예외는 코루틴 밖으로 전파되지 않고RuntimeError로 대체됩니다. 일반 제너레이터에서 이러한 동작을 사용하려면 future import가 필요합니다(PEP 479 참조).- 네이티브 코루틴이 가비지 컬렉션될 때 한 번도 await되지 않았다면
RuntimeWarning이 발생합니다(Debugging Features도 참조하십시오). - Coroutine objects 섹션도 참조하십시오.
코루틴용 types.coroutine() 함수
coroutine(fn)이라는 새로운 함수가 types 모듈에 추가됩니다. 이 함수는 asyncio의 기존 제너레이터 기반 코루틴과 이 PEP에서 도입된 네이티브 코루틴간의 상호 운용을 가능하게 합니다.:
@types.coroutine
def process_data(db):
data = yield from read_data(db)
...
이 함수는 제너레이터 함수의 코드 객체에 CO_ITERABLE_COROUTINE 플래그를 적용하여 코루틴 객체를 반환하도록 합니다.
fn이 제너레이터 함수가 아니면 이를 래핑합니다. 반환하면 제너레이터로 래핑되고, awaitable 프록시 객체로 래핑됩니다( awaitable 객체의 정의는 아래를 참조하십시오).
새로운 구문으로 정의된 네이티브 코루틴과 제너레이터 기반 코루틴을 구분할 수 있도록 types.coroutine()은 CO_COROUTINE 플래그를 적용하지 않습니다.
Await 표현식
다음과 같은 새로운 await 표현식은 코루틴 실행 결과를 얻는 데 사용됩니다.:
async def read_data(db):
data = await db.fetch('SELECT ...')
...
await는 yield from과 마찬가지로 read_data 코루틴의 실행을 일시 중단하고, db.fetch의 awaitable이 완료되어 결과 데이터를 반환할 때까지 기다립니다.
이는 인자를 검증하는 추가 단계를 포함한 yield from 구현을 사용합니다. await는 다음 중 하나일 수 있는 어웨이터블만 허용합니다.
- 네이티브 코루틴함수가 반환하는 네이티브 코루틴 객체
types.coroutine()으로 데코레이트된 함수가 반환하는 제너레이터 기반 코루틴 객체- 이터레이터를 반환하는
__await__메서드를 가진 객체모든
yield from호출 체인은yield로 끝납니다. 이는 Futures가 구현되는 방식의 기본 메커니즘입니다. 코루틴은 내부적으로 제너레이터의 특수한 종류이므로, 모든await는await호출 체인의 어딘가에서yield에 의해 일시 중단됩니다(자세한 설명은 PEP 3156을 참조하십시오).코루틴에서 이러한 동작을 활성화하기 위해
__await__라는 새로운 매직 메서드가 추가됩니다. 예를 들어 asyncio에서await문에서 Future객체를 활성화하는 데 필요한 유일한 변경 사항은asyncio.Future클래스에__await__ = __iter__줄을 추가하는 것입니다.__await__메서드를 사용하는 객체를 이 PEP의 나머지 부분에서는 Future-like 객체라고 부릅니다.__await__가 이터레이터 이외의 것을 반환하면TypeError입니다. tp_as_async.am_await함수를 사용하여 CPython C API로 정의된 객체로,__await__메서드와 유사하게 이터레이터를 반환합니다.
async def함수 바깥에서 await를 사용하는 것은 SyntaxError입니다(def 함수 바깥에서 yield를 사용하는 것이 SyntaxError인 것과 같습니다).
await 표현식에 awaitable 객체 이외의 것을 전달하면 TypeError입니다.
업데이트된 연산자 우선순위 표
await 키워드는 다음과 같이 정의됩니다.:
power ::= await ["**" u_expr]
await ::= ["await"] primary
여기서 “primary”는 언어에서 가장 강하게 결합되는 연산을 나타냅니다. 구문은 다음과 같습니다.:
primary ::= atom | attributeref | subscription | slicing | call
자세한 내용은 Python Documentation [12] 및 Grammar Updates 절을 참조하십시오.
yield 및 yield from 연산자와 await의 핵심적인 차이점은 await expressions는 대부분의 경우 주위에 괄호가 필요하지 않다는 점입니다.
또한 yield from은 인자로 어떤 표현식이든 허용하며, yield from a() + b()와 같은 표현식도 포함됩니다. 이 표현식은 yield from (a() + b())로 해석되며, 이는 거의 항상 버그입니다. 일반적으로 모든 산술 연산의 결과는 awaitable 객체가 아닙니다. 이러한 종류의 실수를 피하기 위해 await의 우선순위는 [], () 및 .보다 낮고 ** 연산자보다 높게 정하기로 했습니다.
| 연산자 | 설명 |
|---|---|
yield x,
yield from x |
Yield 표현식 |
lambda |
람다 표현식 |
if – else |
조건 표현식 |
or |
불리언 OR |
and |
불리언 AND |
not x |
불리언 NOT |
in, not in,
is, is not, <,
<=, >, >=,
!=, == |
멤버십 테스트와 동일성 테스트를 포함한 비교 |
| |
비트 단위 OR |
^ |
비트 단위 XOR |
& |
비트 단위 AND |
<<, >> |
시프트 |
+, - |
덧셈 및 뺄셈 |
*, @, /, //,
% |
곱셈, 행렬 곱셈, 나눗셈, 나머지 |
+x, -x, ~x |
양의 부호, 음의 부호, 비트 단위 NOT |
** |
거듭제곱 |
await x |
Await 표현식 |
x[index],
x[index:index],
x(arguments...),
x.attribute |
첨자 참조, 슬라이싱, 호출, 속성 참조 |
(expressions...),
[expressions...],
{key: value...},
{expressions...} |
바인딩 또는 튜플 표시, 리스트 표시, 딕셔너리 표시, 집합 표시 |
“await” 표현식의 예
유효한 구문 예:
| 표현식 | 다음과 같이 구문 분석됩니다 |
|---|---|
if await fut: pass |
if (await fut): pass |
if await fut + 1: pass |
if (await fut) + 1: pass |
pair = await fut, 'spam' |
pair = (await fut), 'spam' |
with await fut, open(): pass |
with (await fut), open(): pass |
await foo()['spam'].baz()() |
await ( foo()['spam'].baz()() ) |
return await coro() |
return ( await coro() ) |
res = await coro() ** 2 |
res = (await coro()) ** 2 |
func(a1=await coro(), a2=0) |
func(a1=(await coro()), a2=0) |
await foo() + await bar() |
(await foo()) + (await bar()) |
-await foo() |
-(await foo()) |
잘못된 구문 예시는 다음과 같습니다.
| 표현식 | 다음과 같이 작성해야 합니다. |
|---|---|
await await coro() |
await (await coro()) |
await -coro() |
await (-coro()) |
비동기 컨텍스트 관리자와 “async with”
비동기 컨텍스트 관리자는 enter 및 exit 메서드에서 실행을 일시 중단할 수 있는 컨텍스트 관리자입니다.
이를 가능하게 하기 위해 비동기 컨텍스트 관리자를 위한 새로운 프로토콜이 제안되었습니다. 두 개의 새로운 매직 메서드인 __aenter__와 __aexit__가 추가됩니다. 두 메서드 모두 어웨이터블을 반환해야 합니다.
비동기 컨텍스트 관리자의 예:
class AsyncContextManager:
async def __aenter__(self):
await log('entering context')
async def __aexit__(self, exc_type, exc, tb):
await log('exiting context')
새로운 구문
비동기 컨텍스트 관리자를 위한 새로운 문이 제안되었습니다.:
async with EXPR as VAR:
BLOCK
이는 다음과 의미적으로 동등합니다.:
mgr = (EXPR)
aexit = type(mgr).__aexit__
aenter = type(mgr).__aenter__
VAR = await aenter(mgr)
try:
BLOCK
except:
if not await aexit(mgr, *sys.exc_info()):
raise
else:
await aexit(mgr, None, None, None)
일반 with 문과 마찬가지로, 하나의 async with 문에서 여러 컨텍스트 관리자를 지정할 수 있습니다.
__aenter__ 및 __aexit__ 메서드가 없는 일반 컨텍스트 관리자를 async with에 전달하면 오류가 발생합니다. async def 함수 바깥에서 async with를 사용하면 SyntaxError가 발생합니다.
예
비동기 컨텍스트 관리자를 사용하면 코루틴을 위한 적절한 데이터베이스 트랜잭션 관리자를 쉽게 구현할 수 있습니다.:
async def commit(session, data):
...
async with session.transaction():
...
await session.update(data)
...
잠금이 필요한 코드도 더 간결해 보입니다.:
async with lock:
...
대신에:
with (yield from lock):
...
비동기 이터레이터와 “async for”
비동기 반복 가능 객체는 iter 구현에서 비동기 코드를 호출할 수 있고, 비동기 이터레이터는 next 메서드에서 비동기 코드를 호출할 수 있습니다. 비동기 반복을 지원하려면:
- 객체는 비동기 이터레이터 객체를 반환하는
__aiter__메서드(또는 CPython C API로 정의된 경우tp_as_async.am_aiter슬롯)를 구현해야 합니다. - 비동기 이터레이터 객체는 어웨이터블을 반환하는
__anext__메서드(또는 CPython C API로 정의된 경우tp_as_async.am_anext슬롯)를 구현해야 합니다. - 반복을 중지하려면
__anext__는StopAsyncIteration을 발생시켜야 합니다.
비동기 반복 가능 객체의 예:
class AsyncIterable:
def __aiter__(self):
return self
async def __anext__(self):
data = await self.fetch_data()
if data:
return data
else:
raise StopAsyncIteration
async def fetch_data(self):
...
새로운 구문
비동기 이터레이터를 순회하기 위한 새로운 문이 제안됩니다.:
async for TARGET in ITER:
BLOCK
else:
BLOCK2
이는 의미상 다음과 동등합니다.:
iter = (ITER)
iter = type(iter).__aiter__(iter)
running = True
while running:
try:
TARGET = await type(iter).__anext__(iter)
except StopAsyncIteration:
running = False
else:
BLOCK
else:
BLOCK2
__aiter__ 메서드가 없는 일반 반복 가능 객체를 async for에 전달하면 TypeError가 발생합니다. async def 함수 밖에서 async for를 사용하면 SyntaxError입니다.
일반 for 문과 마찬가지로 async for에는 선택적인 else 절이 있습니다.
예제 1
비동기 반복 프로토콜을 사용하면 반복 중에 데이터를 비동기 방식으로 버퍼링할 수 있습니다.:
async for data in cursor:
...
여기서 cursor는 N번 반복할 때마다 데이터베이스에서 데이터 N개 행을 미리 가져오는 비동기 이터레이터입니다.
다음 코드는 새로운 비동기 반복 프로토콜을 보여 줍니다.:
class Cursor:
def __init__(self):
self.buffer = collections.deque()
async def _prefetch(self):
...
def __aiter__(self):
return self
async def __anext__(self):
if not self.buffer:
self.buffer = await self._prefetch()
if not self.buffer:
raise StopAsyncIteration
return self.buffer.popleft()
그러면 Cursor 클래스는 다음과 같이 사용할 수 있습니다.:
async for row in Cursor():
print(row)
이는 다음 코드와 동등합니다.:
i = Cursor().__aiter__()
while True:
try:
row = await i.__anext__()
except StopAsyncIteration:
break
else:
print(row)
예제 2
다음은 일반 반복 가능 객체를 비동기 반복 가능 객체로 변환하는 유틸리티 클래스입니다. 이 작업은 그다지 유용하지 않지만, 이 코드는 일반 이터레이터와 비동기 이터레이터의 관계를 보여 줍니다.
class AsyncIteratorWrapper:
def __init__(self, obj):
self._it = iter(obj)
def __aiter__(self):
return self
async def __anext__(self):
try:
value = next(self._it)
except StopIteration:
raise StopAsyncIteration
return value
async for letter in AsyncIteratorWrapper("abc"):
print(letter)
StopAsyncIteration인 이유
코루틴은 내부적으로 여전히 제너레이터를 기반으로 합니다. 따라서 PEP 479 이전에는 다음 두 가지 사이에 근본적인 차이가 없었습니다.
def g1():
yield from fut
return 'spam'
그리고
def g2():
yield from fut
raise StopIteration('spam')
또한 PEP 479가 코루틴에 적용되고 기본적으로 활성화되었으므로, 다음 예제에서는 StopIteration이 RuntimeError로 래핑됩니다.
async def a1():
await fut
raise StopIteration('spam')
반복이 종료되었음을 외부 코드에 알리는 유일한 방법은 StopIteration이 아닌 무언가를 발생시키는 것입니다. 따라서 새로운 내장 예외 클래스 StopAsyncIteration이 추가되었습니다.
또한 PEP 479의 의미론에 따라 코루틴에서 발생한 모든 StopIteration 예외는 RuntimeError로 래핑됩니다.
코루틴 객체
생성기와의 차이
이 절은 CO_COROUTINE 플래그가 있는 네이티브 코루틴에만 적용되며, 새로운 async def 구문으로 정의된 경우입니다.
asyncio에서 기존 *제너레이터 기반 코루틴*의 동작은 변경되지 않습니다.
코루틴과 제너레이터가 서로 구별되는 개념으로 취급되도록 많은 노력을 기울였습니다.
- 네이티브 코루틴 객체는
__iter__및__next__메서드를 구현하지 않습니다. 따라서 이러한 객체는 반복할 수 없으며,iter(),list(),tuple()및 기타 내장 함수에 전달할 수도 없습니다. 또한for..in루프에서 사용할 수도 없습니다.네이티브 코루틴 객체에서
__iter__또는__next__를 사용하려고 하면TypeError가 발생합니다. - 일반 제너레이터는 네이티브 코루틴에
yield from을 사용할 수 없으며, 그렇게 하면TypeError가 발생합니다. - 제너레이터 기반 코루틴은 asyncio 코드에서
@asyncio.coroutine[1]로 데코레이터가 적용되어야 하며, 네이티브 코루틴 객체에yield from을 사용할 수 있습니다. inspect.isgenerator()및inspect.isgeneratorfunction()은 네이티브 코루틴 객체와 네이티브 코루틴 함수에 대해False를 반환합니다.
코루틴 객체 메서드
코루틴은 내부적으로 제너레이터를 기반으로 하므로 구현을 공유합니다. 제너레이터 객체와 마찬가지로 코루틴은 throw(), send() 및 close() 메서드를 가집니다. StopIteration 및 GeneratorExit은 코루틴에서 동일한 역할을 합니다(단, 코루틴에서는 PEP 479가 기본적으로 활성화됩니다). 자세한 내용은 PEP 342, PEP 380 및 Python Documentation [11]을 참조하십시오.
코루틴의 throw() 및 send() 메서드는 Future와 유사한 객체에 값을 전달하고 오류를 발생시키는 데 사용됩니다.
디버깅 기능
초보자가 흔히 하는 실수는 코루틴에 yield from을 사용하는 것을 잊는 것입니다.:
@asyncio.coroutine
def useful():
asyncio.sleep(1) # this will do nothing without 'yield from'
이러한 실수를 디버깅하기 위해 asyncio에는 특수 디버그 모드가 있으며, 이 모드에서 @coroutine 데코레이터는 모든 함수를 소멸자가 경고를 기록하는 특수 객체로 감쌉니다. 감싸진 제너레이터가 가비지 컬렉션될 때마다 데코레이터 함수가 정확히 어디에서 정의되었는지, 해당 객체가 수집된 위치의 스택 추적 등의 정보를 포함한 자세한 로깅 메시지가 생성됩니다. 래퍼 객체는 제너레이터에 대한 자세한 정보가 포함된 편리한 __repr__ 함수도 제공합니다.
유일한 문제는 이러한 디버깅 기능을 활성화하는 방법입니다. 디버깅 기능은 프로덕션 모드에서 아무 작업도 하지 않아야 하므로, @coroutine 데코레이터는 OS 환경 변수 PYTHONASYNCIODEBUG를 기반으로 감쌀지 여부를 결정합니다. 이를 통해 asyncio 자체의 함수가 계측된 상태로 asyncio 프로그램을 실행할 수 있습니다. 다른 디버깅 기능인 EventLoop.set_debug는 @coroutine 데코레이터의 동작에 영향을 주지 않습니다.
이 제안에 따르면 코루틴은 제너레이터와 구별되는 네이티브 개념입니다. 한 번도 await되지 않은 코루틴에서 RuntimeWarning이 발생하는 것에 추가로, sys 모듈에 set_coroutine_wrapper와 get_coroutine_wrapper라는 두 함수를 추가할 것을 제안합니다. 이는 asyncio 및 기타 프레임워크에서 고급 디버깅 기능을 사용할 수 있도록 하기 위한 것입니다(예: 코루틴이 정확히 어디에서 생성되었는지 표시하고, 해당 코루틴이 가비지 컬렉션된 위치의 더 자세한 스택 추적을 표시하는 기능).
새로운 표준 라이브러리 함수
types.coroutine(gen)입니다. 자세한 내용은 types.coroutine() 섹션을 참조하십시오.inspect.iscoroutine(obj)은obj가 네이티브 코루틴 객체인 경우True를 반환합니다.inspect.iscoroutinefunction(obj)은obj가 네이티브 코루틴 함수인 경우True를 반환합니다.inspect.isawaitable(obj)은obj가 대기 가능 객체인 경우True를 반환합니다.inspect.getcoroutinestate(coro)는 네이티브 코루틴 객체의 현재 상태를 반환합니다(inspect.getfgeneratorstate(gen)을 미러링합니다).inspect.getcoroutinelocals(coro)는 네이티브 코루틴 객체의 지역 변수와 해당 값의 매핑을 반환합니다(inspect.getgeneratorlocals(gen)을 미러링합니다).sys.set_coroutine_wrapper(wrapper)를 사용하면 네이티브 코루틴 객체의 생성을 가로챌 수 있습니다.wrapper는 인수 하나( 코루틴 객체)를 받는 호출 가능 객체이거나None이어야 합니다.None은 래퍼를 재설정합니다. 두 번 호출하면 새 래퍼가 이전 래퍼를 대체합니다. 이 함수는 스레드별로 적용됩니다. 자세한 내용은 Debugging Features를 참조하십시오.sys.get_coroutine_wrapper()는 현재 래퍼 객체를 반환합니다. 래퍼가 설정되지 않은 경우None을 반환합니다. 이 함수는 스레드별로 적용됩니다. 자세한 내용은 Debugging Features를 참조하십시오.
새로운 추상 베이스 클래스
기존 프레임워크(예: Tornado, [13] 참조) 및 컴파일러(예: Cython, [16] 참조)와 더 원활하게 통합할 수 있도록 두 개의 새로운 추상 베이스 클래스(ABC)가 추가됩니다.
__await__메서드를 구현하는 Future와 유사한 클래스에 대한collections.abc.AwaitableABC입니다.send(value),throw(type, exc, tb),close()및__await__()메서드를 구현하는 코루틴 객체에 대한collections.abc.CoroutineABC입니다.CO_ITERABLE_COROUTINE플래그가 있는 제너레이터 기반 코루틴은__await__메서드를 구현하지 않으므로collections.abc.Coroutine및collections.abc.AwaitableABC의 인스턴스가 아니라는 점에 유의하십시오.:@types.coroutine def gencoro(): yield assert not isinstance(gencoro(), collections.abc.Coroutine) # however: assert inspect.isawaitable(gencoro())
객체가 비동기 이터레이션을 지원하는지 쉽게 테스트할 수 있도록 두 개의 ABC가 더 추가됩니다.
collections.abc.AsyncIterable–__aiter__메서드를 테스트합니다.collections.abc.AsyncIterator–__aiter__및__anext__메서드를 테스트합니다.
용어집
- 네이티브 코루틴 함수
- 코루틴 함수는
async def로 선언합니다.await와return value를 사용합니다. 자세한 내용은 New Coroutine Declaration Syntax을 참조하십시오. - 네이티브 코루틴
- 네이티브 코루틴 함수에서 반환됩니다. 자세한 내용은 Await Expression을 참조하십시오.
- 제너레이터 기반 코루틴 함수
- 제너레이터 구문을 기반으로 하는 코루틴입니다. 가장 일반적인 예는
@asyncio.coroutine으로 데코레이트된 함수입니다. - 제너레이터 기반 코루틴
- 제너레이터 기반 코루틴 함수에서 반환됩니다.
- 코루틴
- 네이티브 코루틴 또는 제너레이터 기반 코루틴 중 하나입니다.
- 코루틴 객체
- 네이티브 코루틴 객체 또는 제너레이터 기반 코루틴 객체입니다.
- Future와 유사한 객체입니다.
__await__메서드를 가진 객체 또는tp_as_async->am_await함수를 가진 C 객체로, 이터레이터를 반환합니다. 코루틴에서await표현식으로 소비할 수 있습니다. Future와 유사한 객체를 기다리는 코루틴은 해당 객체의__await__가 완료될 때까지 일시 중단되며, 결과를 반환합니다. 자세한 내용은 Await Expression을 참조하십시오.- 어웨이터블
- Future와 유사한 객체 또는 코루틴 객체입니다. 자세한 내용은 Await Expression을 참조하십시오.
- 비동기 컨텍스트 관리자
- 비동기 컨텍스트 관리자에는
__aenter__및__aexit__메서드가 있으며async with와 함께 사용할 수 있습니다. 자세한 내용은 Asynchronous Context Managers and “async with”를 참조하십시오. - 비동기 이터러블
__aiter__메서드를 가진 객체로, 비동기 이터레이터 객체를 반환해야 합니다.async for와 함께 사용할 수 있습니다. 자세한 내용은 Asynchronous Iterators and “async for”를 참조하십시오.- 비동기 이터레이터
- 비동기 이터레이터에는
__anext__메서드가 있습니다. 자세한 내용은 Asynchronous Iterators and “async for”를 참조하십시오.
전환 계획
async 및 await 키워드와 관련된 하위 호환성 문제를 피하기 위해 tokenizer.c를 다음과 같은 방식으로 수정하기로 결정되었습니다:
async defNAME토큰 조합을 인식합니다;async def블록을 토큰화하는 동안'async'NAME토큰을ASYNC로,'await'NAME토큰을AWAIT로 대체합니다;def블록을 토큰화하는 동안'async'및'await'NAME토큰을 있는 그대로 생성합니다.
이 접근 방식을 사용하면 새 구문 기능(모두 async 함수에서만 사용할 수 있음)을 기존 코드와 원활하게 결합할 수 있습니다.
하나의 코드 조각에 “async def”와 “async” 속성이 함께 있는 예:
class Spam:
async = 42
async def ham():
print(getattr(Spam, 'async'))
# The coroutine can be executed and will print '42'
하위 호환성
이 제안은 하위 호환성을 100% 유지합니다.
asyncio
asyncio모듈은 코루틴 및 새 문과 함께 작동하도록 조정되고 테스트되었습니다. 하위 호환성은 100% 유지됩니다. 즉, 기존 코드는 모두 있는 그대로 작동합니다.
필요한 변경 사항은 주로 다음과 같습니다:
@asyncio.coroutine데코레이터를 새로운types.coroutine()함수를 사용하도록 수정합니다.__await__ = __iter__줄을asyncio.Future클래스에 추가합니다.ensure_future()를async()함수의 별칭으로 추가합니다.async()함수를 사용 중단합니다.
asyncio 마이그레이션 전략
일반 제너레이터는 네이티브 코루틴 객체에 대해 yield from을 사용할 수 없으므로(자세한 내용은 Differences from generators 절을 참조하십시오), 새 구문을 사용하기 전에 모든 제너레이터 기반 코루틴에 @asyncio.coroutine 데코레이터를 적용하는 것이 좋습니다.
CPython 코드 베이스에서의 async/await
CPython에서는 await를 이름으로 사용하지 않습니다.
async는 주로 asyncio에서 사용됩니다. asyncio 섹션에서 자세히 설명하듯이, async()함수를 ensure_future()로 이름을 변경하여 이 문제를 해결하고 있습니다.
async키워드의 또 다른 용도는 Lib/xml/dom/xmlbuilder.py에서 DocumentLS 클래스에 async = False특성을 정의하는 것입니다. 이에 대한 문서나 테스트가 없으며, CPython의 다른 어느 곳에서도 사용되지 않습니다. 이를 getter로 대체하여 DeprecationWarning을 발생시키고 대신 async_특성을 사용하도록 권고합니다. ‘async’ 특성은 문서화되어 있지 않으며 CPython 코드 베이스에서 사용되지 않습니다.
문법 업데이트
문법 변경은 매우 제한적입니다.:
decorated: decorators (classdef | funcdef | async_funcdef)
async_funcdef: ASYNC funcdef
compound_stmt: (if_stmt | while_stmt | for_stmt | try_stmt | with_stmt
| funcdef | classdef | decorated | async_stmt)
async_stmt: ASYNC (funcdef | with_stmt | for_stmt)
power: atom_expr ['**' factor]
atom_expr: [AWAIT] atom trailer*
사용 중단 계획
async와 await이름은 CPython 3.5와 3.6에서 점진적으로 사용 중단됩니다. 3.7에서는 이를 정식 키워드로 변환합니다. 3.7 이전에 async와 await를 정식 키워드로 만들면 사람들이 코드를 Python 3으로 포팅하기가 더 어려워질 수 있습니다.
설계 고려 사항
PEP 3152
PEP 3152는 Gregory Ewing이 제안한 것으로, 코루틴을 위한 다른 메커니즘(“cofunctions”라고 부름)을 제안합니다. 몇 가지 핵심 사항은 다음과 같습니다.
- Cofunction을 선언하기 위한 새로운 키워드
codef를 도입합니다. Cofunction은 내부에cocall표현식이 없더라도 항상 제너레이터입니다. 이 제안에서는async def에 대응합니다. - Cofunction을 호출하기 위한 새로운 키워드
cocall을 도입합니다. Cofunction 내부에서만 사용할 수 있습니다. 이 제안에서는await에 대응합니다(아래 설명하는 몇 가지 차이점이 있습니다). cocall키워드 없이는 cofunction을 호출할 수 없습니다.cocall뒤에는 문법상 괄호가 와야 합니다.:atom: cocall | <existing alternatives for atom> cocall: 'cocall' atom cotrailer* '(' [arglist] ')' cotrailer: '[' subscriptlist ']' | '.' NAME
cocall f(*args, **kwds)는 의미상yield from f.__cocall__(*args, **kwds)와 동등합니다.
이 제안과의 차이점:
- 이 PEP에는
__cocall__에 해당하는 것이 없으며,cocall표현식에서 호출되고 그 결과가yield from에 전달됩니다.await키워드는 awaitable 객체를 기대하고, 타입을 검증한 다음 해당 객체에yield from을 실행합니다.__await__메서드는__cocall__과 유사하지만, Future-like 객체를 정의하는 데만 사용됩니다. await는 문법에서yield from과 거의 같은 방식으로 정의됩니다(await는async def내부에서만 사용할 수 있도록 나중에 강제됩니다).await future라고 간단히 작성할 수 있지만,cocall은 항상 괄호가 필요합니다.- asyncio가 PEP 3152와 함께 작동하도록 하려면
@asyncio.coroutine데코레이터를 수정하여 모든 함수를__cocall__메서드가 있는 객체로 감싸거나, 제너레이터에__cocall__을 구현해야 합니다. 기존 제너레이터 기반 코루틴에서 cofunctions 를 호출하려면costart(cofunc, *args, **kwargs)내장 함수를 사용해야 합니다. cocall키워드 없이 cofunction 을 호출할 수 없으므로, 제너레이터 기반 코루틴에서yield from을 사용하는 것을 잊는 흔한 실수를 자동으로 방지합니다. 이 제안은 다른 접근 방식으로 이 문제를 해결합니다. Debugging Features를 참조하십시오.- 코루틴을 호출할 때
cocall키워드를 요구하는 방식의 단점은,yield또는async yield표현식을 사용하는 코루틴인 코루틴 제너레이터를 구현하기로 결정한다면 이를 호출하는 데cocall키워드가 필요하지 않다는 점입니다. 따라서 일반 코루틴에는__cocall__이 있고__call__은 없게 되며, 코루틴 제너레이터에는__call__이 있고__cocall__은 없게 됩니다. - 문법적으로 괄호를 요구하는 것은 새로운 문제를 매우 많이 일으키기도 합니다.
다음 코드는:
await fut await function_returning_future() await asyncio.gather(coro1(arg1, arg2), coro2(arg1, arg2))
다음과 같이 보입니다.:
cocall fut() # or cocall costart(fut) cocall (function_returning_future())() cocall asyncio.gather(costart(coro1, arg1, arg2), costart(coro2, arg1, arg2))
- PEP 3152에는
async for와async with에 해당하는 것이 없습니다.
코루틴 제너레이터
async for 키워드를 사용하면 yield 및 yield from 표현식을 사용하는 코루틴-제너레이터라는 개념이 바람직합니다. 일반 제너레이터와의 모호성을 피하려면 yield앞에 async 키워드를 두도록 요구할 가능성이 높으며, async yield from은 StopAsyncIteration 예외를 발생시킬 것입니다.
코루틴 제너레이터를 구현하는 것은 가능하지만, 이것은 이 제안의 범위를 벗어난다고 생각합니다. 이는 신중하게 고려하고 균형을 맞춰야 하는 고급 개념이며, 현재 제너레이터 객체의 구현을 상당히 복잡하게 변경해야 합니다. 이는 별도의 PEP에서 다룰 사안입니다.
“async” 및 “await” 키워드인 이유
async/await는 프로그래밍 언어에서 새로운 개념이 아닙니다.
- C#에는 오래전부터 이 기능이 있었습니다 [5].
- ECMAScript 7에 async/await를 추가하자는 제안이 있었으며 [2]; Traceur 프로젝트 [9] 도 참조하십시오.
- Facebook의 Hack/HHVM [6].
- Google의 Dart 언어 [7].
- Scala [8].
- C++에 async/await를 추가하자는 제안 [10].
- 그리고 그 밖의 덜 널리 사용되는 많은 언어가 있습니다.
일부 사용자는 이미 async/await를 사용해 본 경험이 있고, 하나의 프로젝트에서 여러 언어로 작업하기도 더 쉬워지므로(예를 들어 ECMAScript 7과 함께 사용하는 Python), 이는 큰 이점입니다.
“__aiter__”가 awaitable을 반환하지 않는 이유
PEP 492은 CPython 3.5.0에서 __aiter__를 비동기 이터레이터로 해석되는 awaitable을 반환할 것으로 예상되는 메서드로 정의한 상태로 승인되었습니다.
3.5.2에서 (PEP 492가 잠정적으로 승인되었을 때) __aiter__ 프로토콜은 비동기 이터레이터를 직접 반환하도록 업데이트되었습니다.
이 변경의 동기는 Python에서 비동기 제너레이터를 구현할 수 있게 하는 것입니다. 자세한 내용은 [19] 및 [20]을 참조하십시오.
“async” 키워드의 중요성
await 표현식만 구현하고 await를 하나 이상 포함하는 모든 함수를 코루틴으로 취급할 수도 있지만, 이 방식은 API 설계, 코드 리팩터링 및 장기적인 지원을 더 어렵게 만듭니다.
Python에 await 키워드만 있다고 가정해 봅시다.:
def useful():
...
await log(...)
...
def important():
await useful()
useful() 함수가 리팩터링되어 누군가 함수에서 모든 await 표현식을 제거하면, 이 함수는 일반 Python 함수가 되고 important()를 비롯해 이 함수에 의존하는 모든 코드가 망가집니다. 이 문제를 완화하려면 @asyncio.coroutine과 유사한 데코레이터를 도입해야 합니다.
왜 “async def”인가
어떤 사람들에게는 async name(): pass와 같은 단순한 구문이 async def name(): pass보다 더 매력적으로 보일 수 있습니다. 확실히 입력하기는 더 쉽습니다. 그러나 반면에 async가 해당 문이 비동기임을 나타내는 수정자인 async def, async with 및 async for 사이의 대칭성이 깨집니다. 또한 기존 문법과도 더 일관됩니다.
왜 “await for”와 “await with”가 아닌가
async는 형용사이므로 문 한정자 키워드로 더 적합합니다. await for/with는 무언가가 for 또는 with 문의 완료를 기다린다는 의미가 됩니다.
왜 “async def”이고 “def async”가 아닌가
async 키워드는 문 한정자입니다. 이에 대한 좋은 비유는 다른 언어의 “static”, “public”, “unsafe” 키워드입니다. “async for”는 비동기 “for” 문이고, “async with”는 비동기 “with” 문이며, “async def”는 비동기 함수입니다.
주 문 키워드 뒤에 “async”를 두면 “for async item in iterator”가 “이터레이터의 각 비동기 항목에 대해”로 읽힐 수 있는 것과 같은 혼동이 생길 수 있습니다.
또한 def, with 및 for 앞에 async 키워드를 두면 언어 문법이 더 단순해집니다. 그리고 “async def”는 코루틴과 일반 함수를 시각적으로 더 잘 구분합니다.
왜 __future__ 임포트가 아닌가
Transition Plan 절에서는 토크나이저가 async와 await를 키워드로 처리하도록 수정되어 오직 async def 블록에서만 인식하는 방식을 설명합니다. 따라서 async def는 from __future__ import async_await와 같은 모듈 수준 컴파일러 선언이 수행했을 역할을 대신합니다.
왜 매직 메서드는 “a”로 시작하는가
새로운 비동기 매직 메서드인 __aiter__, __anext__, __aenter__ 및 __aexit__는 모두 동일한 접두사 “a”로 시작합니다. 대안으로 “async” 접두사를 사용하여 __anext__를 __async_next__로 만들자는 제안도 있습니다. 그러나 __radd__ 및 __iadd__와 같은 기존 매직 메서드와 새로운 매직 메서드를 일치시키기 위해 더 짧은 형태를 사용하기로 결정했습니다.
왜 기존 매직 이름을 재사용하지 않는가
새로운 비동기 이터레이터와 컨텍스트 관리자에 대한 대안은 선언에 async 키워드를 추가하여 기존 매직 메서드를 재사용하는 것이었습니다:
class CM:
async def __enter__(self): # instead of __aenter__
...
이 방식에는 다음과 같은 단점이 있습니다:
with및async with문 모두에서 작동하는 객체를 만들 수 없습니다;- 하위 호환성을 깨뜨릴 수 있습니다. Python <= 3.4에서는
__enter__및/또는__exit__에서 Future와 유사한 객체를 반환하는 것을 금지하는 것이 없기 때문입니다; - 이 제안의 주요 목적 중 하나는 네이티브 코루틴을 가능한 한 단순하고 실수하기 어렵게 만드는 것이며, 따라서 프로토콜을 명확히 분리합니다.
기존 “for” 및 “with” 문을 재사용하지 않는 이유는 무엇입니까?
기존 제너레이터 기반 코루틴과 이 제안의 취지는 사용자가 코드가 일시 중단될 수 있는 지점을 쉽게 파악할 수 있도록 하는 것입니다. 기존 “for” 및 “with” 문이 비동기 이터레이터와 컨텍스트 관리자를 인식하도록 만들면 암묵적인 일시 중단 지점이 필연적으로 생성되어 코드의 동작을 추론하기 어려워집니다.
컴프리헨션
비동기 컴프리헨션을 위한 구문을 제공할 수 있지만, 이 구성은 이 PEP의 범위 밖입니다.
비동기 람다 함수
비동기 람다 함수를 위한 구문을 제공할 수 있지만, 이 구성은 이 PEP의 범위 밖입니다.
성능
전반적인 영향
이 제안은 관찰 가능한 성능 영향을 초래하지 않습니다. 다음은 Python의 공식 벤치마크 집합의 출력입니다 [4]:
python perf.py -r -b default ../cpython/python.exe ../cpython-aw/python.exe
[skipped]
Report on Darwin ysmac 14.3.0 Darwin Kernel Version 14.3.0:
Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64
x86_64 i386
Total CPU cores: 8
### etree_iterparse ###
Min: 0.365359 -> 0.349168: 1.05x faster
Avg: 0.396924 -> 0.379735: 1.05x faster
Significant (t=9.71)
Stddev: 0.01225 -> 0.01277: 1.0423x larger
The following not significant results are hidden, use -v to show them:
django_v2, 2to3, etree_generate, etree_parse, etree_process, fastpickle,
fastunpickle, json_dump_v2, json_load, nbody, regex_v8, tornado_http.
토크나이저 수정
수정된 토크나이저로 Python 파일을 구문 분석할 때 관찰 가능한 속도 저하는 없습니다. 하나의 12Mb 파일(Lib/test/test_binop.py를 1000번 반복)의 구문 분석에는 동일한 시간이 걸립니다.
async/await
“async” 함수와 제너레이터 간의 성능 차이를 확인하기 위해 다음 마이크로 벤치마크를 사용했습니다.:
import sys
import time
def binary(n):
if n <= 0:
return 1
l = yield from binary(n - 1)
r = yield from binary(n - 1)
return l + 1 + r
async def abinary(n):
if n <= 0:
return 1
l = await abinary(n - 1)
r = await abinary(n - 1)
return l + 1 + r
def timeit(func, depth, repeat):
t0 = time.time()
for _ in range(repeat):
o = func(depth)
try:
while True:
o.send(None)
except StopIteration:
pass
t1 = time.time()
print('{}({}) * {}: total {:.3f}s'.format(
func.__name__, depth, repeat, t1-t0))
결과적으로 관찰 가능한 성능 차이는 없습니다.:
binary(19) * 30: total 53.321s
abinary(19) * 30: total 55.073s
binary(19) * 30: total 53.361s
abinary(19) * 30: total 51.360s
binary(19) * 30: total 49.438s
abinary(19) * 30: total 51.047s
깊이 19는 1,048,575회의 호출을 의미한다는 점에 유의하십시오.
참조 구현
참조 구현은 여기에서 찾을 수 있습니다: [3].
상위 수준 변경 사항 및 새로운 프로토콜 목록
- 코루틴을 정의하기 위한 새로운 구문:
async def및 새로운await키워드입니다. - Future와 유사한 객체를 위한 새로운
__await__메서드와PyTypeObject의 새로운tp_as_async.am_await슬롯입니다. - 비동기 컨텍스트 관리자를 위한 새로운 구문:
async with구문입니다. 그리고__aenter__및__aexit__메서드와 관련된 프로토콜입니다. - 비동기 반복을 위한 새로운 구문:
async for구문입니다. 그리고__aiter__,__aexit__및 새로운 내장 예외StopAsyncIteration를 포함하는 관련 프로토콜입니다.PyTypeObject의 새로운tp_as_async.am_aiter및tp_as_async.am_anext슬롯입니다. - 새로운 AST 노드:
AsyncFunctionDef,AsyncFor,AsyncWith,Await입니다. - 새로운 함수:
sys.set_coroutine_wrapper(callback),sys.get_coroutine_wrapper(),types.coroutine(gen),inspect.iscoroutinefunction(func),inspect.iscoroutine(obj),inspect.isawaitable(obj),inspect.getcoroutinestate(coro),inspect.getcoroutinelocals(coro)가 있습니다. - 코드 객체를 위한 새로운
CO_COROUTINE및CO_ITERABLE_COROUTINE비트 플래그입니다. - 새로운 ABC:
collections.abc.Awaitable,collections.abc.Coroutine,collections.abc.AsyncIterable및collections.abc.AsyncIterator. - C API 변경 사항: 새로운
PyCoro_Type(Python에서는types.CoroutineType으로 노출됨) 및PyCoroObject입니다.PyCoro_CheckExact(*o)는o가 네이티브 코루틴인지 확인하는 데 사용합니다.
변경 사항과 새 기능의 목록이 짧지는 않지만, 대부분의 사용자는 이러한 기능을 직접 사용하지 않는다는 점을 이해하는 것이 중요합니다. 이 기능은 프레임워크와 라이브러리에서 async def, await, async for 및 async with 구문을 사용하는 편리하고 모호하지 않은 API를 사용자에게 제공하는 데 사용하도록 설계되었습니다.
작동 예제
이 PEP에서 제안된 모든 개념은 구현되었으며 [3] 테스트할 수 있습니다.
import asyncio
async def echo_server():
print('Serving on localhost:8000')
await asyncio.start_server(handle_connection,
'localhost', 8000)
async def handle_connection(reader, writer):
print('New connection...')
while True:
data = await reader.read(8192)
if not data:
break
print('Sending {:.10}... back'.format(repr(data)))
writer.write(data)
loop = asyncio.get_event_loop()
loop.run_until_complete(echo_server())
try:
loop.run_forever()
finally:
loop.close()
승인
구현
구현은 이슈 24017 [15]에서 추적됩니다. 2015년 5월 11일에 커밋되었습니다.
참고 자료
감사의 말
Guido van Rossum, Victor Stinner, Elvis Pranskevichus, Andrew Svetlov, Łukasz Langa, Greg Ewing, Stephen J. Turnbull, Jim J. Jewett, Brett Cannon, Alyssa Coghlan, Steven D’Aprano, Paul Moore, Nathaniel Smith, Ethan Furman, Stefan Behnel, Paul Sokolovsky, Victor Petrovykh를 비롯한 많은 분들께 이 PEP에 관한 피드백, 아이디어, 수정, 비판, 코드 리뷰, 그리고 토론에 감사드립니다.
Copyright
This document has been placed in the public domain.