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”를 쓰지 않는 이유
- “def async”가 아닌 “async def”를 쓰는 이유
- __future__ 임포트를 쓰지 않는 이유
- 매직 메서드가 “a”로 시작하는 이유
- 기존 매직 이름을 재사용하지 않는 이유
- 기존 “for” 및 “with” 문을 재사용하지 않는 이유
- 컴프리헨션
- 비동기 람다 함수
- 성능
- 참조 구현
- 수락
- 구현
- 참고 문헌
- 감사의 글
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
인터넷과 전반적인 연결성이 성장하면서, 그에 비례하여 응답성이 좋고 확장 가능한 코드에 대한 필요성이 생겨났습니다. 이 제안은 명시적으로 비동기적이고 동시적인 Python 코드를 더 쉽고 Python다운 방식으로 작성할 수 있게 함으로써 그러한 요구에 부응하는 것을 목표로 합니다.
코루틴을 Python의 독립된 고유 개념으로 만들고, 이를 지원하는 새로운 문법을 도입할 것을 제안합니다. 궁극적인 목표는 Python에서 비동기 프로그래밍에 대한 공통적이고 쉽게 접근할 수 있는 멘탈 모델을 확립하는 데 도움을 주고, 이를 동기 프로그래밍에 최대한 가깝게 만드는 것입니다.
이 PEP는 비동기 태스크가 표준 라이브러리 모듈 asyncio.events.AbstractEventLoop의 것과 유사한 이벤트 루프에 의해 스케줄링되고 조정된다고 가정합니다. 이 PEP는 특정 이벤트 루프 구현에 묶여 있지 않지만, 코루틴이 (IO와 같은) 이벤트가 완료될 때까지 대기할 것임을 나타내기 위해 yield를 스케줄러에 대한 신호로 사용하는 종류의 코루틴에만 관련이 있습니다.
다른 많은 언어가 이미 유사한 기능을 도입했거나 도입할 계획이므로([2], [5], [6], [7], [8], [10]), 여기서 제안하는 변경 사항은 빠르게 성장하는 비동기 프로그래밍 분야에서 Python이 시대에 뒤처지지 않고 경쟁력을 유지하는 데 도움이 될 것이라고 믿습니다.
API 설계 및 구현 개정 사항
- Python 3.5의 첫 번째 베타 릴리스에 대한 피드백에 따라, 네이티브 코루틴을 제너레이터와 더 명확하게 구분하기 위해 이 PEP를 지원하는 객체 모델이 재설계되었습니다. 이제 네이티브 코루틴은 새로운 종류의 제너레이터가 아니라 완전히 별개의 고유한 타입입니다([17]에서 구현됨).
이 변경은 주로 Tornado 웹 서버에 네이티브 코루틴 지원을 통합하려고 시도하는 과정에서 발생한 문제들을 근거로 구현되었습니다([18]에서 보고됨).
- CPython 3.5.2에서
__aiter__프로토콜이 갱신되었습니다.3.5.2 이전에는
__aiter__가 비동기 이터레이터로 귀결되는 어웨이터블을 반환해야 했습니다. 3.5.2부터는__aiter__가 비동기 이터레이터를 직접 반환해야 합니다.3.5.2에서 이전 프로토콜을 사용하면 Python은
PendingDeprecationWarning을 발생시킵니다.CPython 3.6에서는 이전
__aiter__프로토콜이 여전히 지원되지만DeprecationWarning이 발생합니다.CPython 3.7에서는 이전
__aiter__프로토콜이 더 이상 지원되지 않습니다.__aiter__가 비동기 이터레이터가 아닌 다른 것을 반환하면RuntimeError가 발생합니다.
근거와 목표
현재 Python은 제너레이터를 통한 코루틴 구현을 지원하며(PEP 342), 이는 PEP 380에서 도입된 yield from 문법으로 더욱 강화되었습니다. 이 접근 방식에는 여러 가지 단점이 있습니다:
- 코루틴은 일반 제너레이터와 같은 문법을 공유하기 때문에 둘을 혼동하기 쉬우며, 특히 새로운 개발자에게 더욱 그렇습니다.
- 함수가 코루틴인지 여부는 함수 본문에
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 임포트가 필요합니다(PEP 479 참조).- 네이티브 코루틴이 한 번도 await되지 않은 채 가비지 컬렉션되면
RuntimeWarning이 발생합니다(Debugging Features도 참조하십시오). - Coroutine objects 절도 참조하십시오.
types.coroutine()
types 모듈에 새 함수 coroutine(fn)이 추가됩니다. 이 함수는 asyncio의 기존 제너레이터 기반 코루틴과 이 PEP에서 도입하는 네이티브 코루틴 사이의 상호 운용을 가능하게 합니다:
@types.coroutine
def process_data(db):
data = yield from read_data(db)
...
이 함수는 제너레이터 함수의 코드 객체에 CO_ITERABLE_COROUTINE 플래그를 적용하여, 해당 함수가 코루틴 객체를 반환하도록 만듭니다.
fn이 제너레이터 함수가 아니면 래핑됩니다. 그 함수가 제너레이터를 반환하면, 반환값은 어웨이터블 프록시 객체로 래핑됩니다(어웨이터블 객체의 정의는 아래를 참조하십시오).
새 문법으로 정의된 네이티브 코루틴을 제너레이터 기반 코루틴과 구별할 수 있도록 types.coroutine()은 CO_COROUTINE 플래그를 적용하지 않는다는 점에 유의하십시오.
Await 표현식
코루틴 실행 결과를 얻을 때는 다음의 새로운 await 표현식을 사용합니다:
async def read_data(db):
data = await db.fetch('SELECT ...')
...
await는 yield from과 마찬가지로 db.fetch 어웨이터블이 완료되어 결과 데이터를 반환할 때까지 read_data 코루틴의 실행을 일시 중단합니다.
이 표현식은 yield from 구현을 사용하되, 인자를 검증하는 단계가 하나 더 있습니다. await는 어웨이터블만 받으며, 어웨이터블은 다음 중 하나일 수 있습니다:
- 네이티브 코루틴 함수가 반환한 네이티브 코루틴 객체.
types.coroutine()으로 데코레이트된 함수가 반환한 제너레이터 기반 코루틴 객체.- 이터레이터를 반환하는
__await__메서드를 가진 객체.모든
yield from호출 체인은yield로 끝납니다. 이는 Futures가 구현되는 방식의 근본적인 메커니즘입니다. 내부적으로 코루틴은 특수한 종류의 제너레이터이므로, 모든await는await호출 체인을 따라 내려간 어딘가에 있는yield에 의해 일시 중단됩니다(자세한 설명은 PEP 3156을 참조하십시오).코루틴에서 이 동작을 가능하게 하기 위해
__await__라는 새로운 매직 메서드가 추가됩니다. 예를 들어 asyncio에서 Future 객체를await문에 사용할 수 있게 하려면,asyncio.Future클래스에__await__ = __iter__줄을 추가하기만 하면 됩니다.이 PEP의 나머지 부분에서는
__await__메서드를 가진 객체를 Future-like 객체라고 부릅니다.__await__가 이터레이터가 아닌 다른 것을 반환하면TypeError입니다. - 이터레이터를 반환하는
tp_as_async.am_await함수를 가진, CPython C API로 정의된 객체(__await__메서드와 유사합니다).
async def 함수 밖에서 await를 사용하는 것은 SyntaxError입니다(def 함수 밖에서 yield를 사용하는 것이 SyntaxError인 것과 같습니다).
await 표현식에 어웨이터블 객체 이외의 것을 전달하면 TypeError입니다.
갱신된 연산자 우선순위 표
await 키워드는 다음과 같이 정의됩니다:
power ::= await ["**" u_expr]
await ::= ["await"] primary
여기서 “primary”는 언어에서 가장 강하게 결합하는 연산을 나타냅니다. 그 문법은 다음과 같습니다:
primary ::= atom | attributeref | subscription | slicing | call
자세한 내용은 Python 문서 [12] 및 이 제안의 Grammar Updates 절을 참조하십시오.
await가 yield 및 yield from 연산자와 다른 핵심적인 점은 대부분의 경우 await 표현식을 괄호로 감쌀 필요가 없다는 것입니다.
또한 yield from은 어떤 표현식이든 인자로 허용하는데, 여기에는 yield from a() + b() 같은 표현식도 포함됩니다. 이 표현식은 yield from (a() + b())로 파싱되며, 이는 거의 항상 버그입니다. 일반적으로 산술 연산의 결과는 어웨이터블 객체가 아닙니다. 이런 종류의 실수를 방지하기 위해 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 문서 [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)가 추가됩니다:
collections.abc.Awaitable은__await__메서드를 구현하는 Future와 유사한 클래스를 위한 ABC입니다.collections.abc.Coroutine은send(value),throw(type, exc, tb),close(),__await__()메서드를 구현하는 코루틴 객체를 위한 ABC입니다.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()함수를 사용하도록 수정합니다.asyncio.Future클래스에__await__ = __iter__줄을 추가합니다.async()함수의 별칭으로ensure_future()를 추가합니다.async()함수를 폐지 예정(deprecated)으로 지정합니다.
asyncio 이전 전략
일반 제너레이터는 네이티브 코루틴 객체를 yield from 할 수 없으므로(자세한 내용은 Differences from generators 절을 참조하십시오), 새 문법을 사용하기 전에 모든 제너레이터 기반 코루틴이 @asyncio.coroutine으로 데코레이트되어 있는지 확인할 것을 권장합니다.
CPython 코드 베이스의 async/await
CPython에는 await라는 이름을 사용하는 곳이 없습니다.
async는 주로 asyncio에서 사용됩니다. 이 문제는 async() 함수의 이름을 ensure_future()로 바꾸는 방식으로 해결합니다(자세한 내용은 asyncio 절을 참조하십시오).
async 키워드가 사용되는 또 다른 곳은 Lib/xml/dom/xmlbuilder.py로, DocumentLS 클래스의 async = False 속성을 정의하는 데 사용됩니다. 이 속성에 대한 문서나 테스트는 없으며, CPython의 다른 어디에서도 사용되지 않습니다. 이 속성은 DeprecationWarning을 발생시키며 대신 async_ 속성을 사용하도록 안내하는 getter로 대체됩니다. ‘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에서 소프트하게 폐지 예정(deprecated)으로 지정됩니다. 3.7에서는 이들을 정식 키워드로 전환합니다. 3.7 이전에 async와 await를 정식 키워드로 만들면 사람들이 코드를 Python 3으로 이식하기가 더 어려워질 수 있습니다.
설계 시 고려 사항
PEP 3152
Gregory Ewing이 작성한 PEP 3152는 코루틴을 위한 다른 메커니즘(“cofunction”이라고 부릅니다)을 제안합니다. 몇 가지 핵심 사항은 다음과 같습니다:
- 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표현식에서 호출되고 그 결과가yield from에 전달되는__cocall__에 해당하는 것이 없습니다.await키워드는 awaitable 객체를 기대하며, 타입을 검증한 후 그 객체에yield from을 실행합니다. 다만__await__메서드는__cocall__과 비슷하지만, Future-like 객체를 정의하는 데에만 사용됩니다. await는 문법에서yield from과 거의 같은 방식으로 정의됩니다(await가async def안에서만 사용될 수 있다는 제약은 이후 단계에서 적용됩니다). 간단히await future라고 쓸 수 있는 반면,cocall은 항상 괄호가 필요합니다.- asyncio가 PEP 3152와 함께 동작하게 하려면
@asyncio.coroutine데코레이터를 수정하여 모든 함수를__cocall__메서드를 가진 객체로 감싸거나, 제너레이터에__cocall__을 구현해야 합니다. 기존 제너레이터 기반 코루틴에서 cofunction을 호출하려면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를 사용해 본 경험이 있고, 한 프로젝트에서 여러 언어(예를 들어 Python과 ECMAScript 7)를 함께 다루기가 더 쉬워지므로, 이는 큰 이점입니다.
“__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 def, async with, async for 사이의 대칭성을 깨뜨립니다. 이들에서 async는 해당 문이 비동기임을 나타내는 수식어입니다. 또한 기존 문법과도 더 일관됩니다.
“await for”와 “await with”를 쓰지 않는 이유
async는 형용사이므로 문 한정자(statement qualifier) 키워드로 더 나은 선택입니다. await for/with는 무언가가 for 또는 with 문의 완료를 기다리고 있다는 의미로 읽힐 것입니다.
“def async”가 아닌 “async def”를 쓰는 이유
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 def 블록 안에서만 async와 await를 키워드로 취급하도록 어떻게 수정되는지 설명합니다. 따라서 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가 추가되었습니다.o가 네이티브 코루틴인지 검사하는PyCoro_CheckExact(*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일에 커밋되었습니다.
참고 문헌
감사의 글
이 PEP에 대해 피드백, 아이디어, 편집, 비판, 코드 리뷰, 토론을 제공해 주신 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 및 그 밖의 많은 분께 감사드립니다.
Copyright
This document has been placed in the public domain.