Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 492 – Coroutines with async and await syntax

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

Abstract

The growth of Internet and general connectivity has triggered theproportionate need for responsive and scalable code. This proposalaims to answer that need by making writing explicitly asynchronous,concurrent Python code easier and more Pythonic.

It is proposed to makecoroutines a proper standalone concept inPython, and introduce new supporting syntax. The ultimate goalis to help establish a common, easily approachable, mentalmodel of asynchronous programming in Python and make it as close tosynchronous programming as possible.

This PEP assumes that the asynchronous tasks are scheduled andcoordinated by an Event Loop similar to that of stdlib moduleasyncio.events.AbstractEventLoop. While the PEP is not tied to anyspecific Event Loop implementation, it is relevant only to the kind ofcoroutine that usesyield as a signal to the scheduler, indicatingthat the coroutine will be waiting until an event (such as IO) iscompleted.

We believe that the changes proposed here will help keep Pythonrelevant and competitive in a quickly growing area of asynchronousprogramming, as many other languages have adopted, or are planning toadopt, similar features:[2],[5],[6],[7],[8],[10].

API Design and Implementation Revisions

  1. Feedback on the initial beta release of Python 3.5 resulted in aredesign of the object model supporting this PEP to more clearlyseparate native coroutines from generators - rather than being anew kind of generator, native coroutines are now their owncompletely distinct type (implemented in[17]).

    This change was implemented based primarily due to problemsencountered attempting to integrate support for native coroutinesinto the Tornado web server (reported in[18]).

  2. In CPython 3.5.2, the__aiter__ protocol was updated.

    Before 3.5.2,__aiter__ was expected to return anawaitableresolving to anasynchronous iterator. Starting with 3.5.2,__aiter__ should return asynchronous iterators directly.

    If the old protocol is used in 3.5.2, Python will raise aPendingDeprecationWarning.

    In CPython 3.6, the old__aiter__ protocol will still besupported with aDeprecationWarning being raised.

    In CPython 3.7, the old__aiter__ protocol will no longer besupported: aRuntimeError will be raised if__aiter__returns anything but an asynchronous iterator.

    See[19] and[20] for more details.

Rationale and Goals

Current Python supports implementing coroutines via generators (PEP342), further enhanced by theyieldfrom syntax introduced in PEP380. This approach has a number of shortcomings:

  • It is easy to confuse coroutines with regular generators, since theyshare the same syntax; this is especially true for new developers.
  • Whether or not a function is a coroutine is determined by a presenceofyield oryieldfrom statements in itsbody, which canlead to unobvious errors when such statements appear in or disappearfrom function body during refactoring.
  • Support for asynchronous calls is limited to expressions whereyield is allowed syntactically, limiting the usefulness ofsyntactic features, such aswith andfor statements.

This proposal makes coroutines a native Python language feature, andclearly separates them from generators. This removesgenerator/coroutine ambiguity, and makes it possible to reliably definecoroutines without reliance on a specific library. This also enableslinters and IDEs to improve static code analysis and refactoring.

Native coroutines and the associated new syntax features make itpossible to define context manager and iteration protocols inasynchronous terms. As shown later in this proposal, the newasyncwith statement lets Python programs perform asynchronous calls whenentering and exiting a runtime context, and the newasyncforstatement makes it possible to perform asynchronous calls in iterators.

Specification

This proposal introduces new syntax and semantics to enhance coroutinesupport in Python.

This specification presumes knowledge of the implementation ofcoroutines in Python (PEP 342 andPEP 380). Motivation for the syntaxchanges proposed here comes from the asyncio framework (PEP 3156) andthe “Cofunctions” proposal (PEP 3152, now rejected in favor of thisspecification).

From this point in this document we use the wordnative coroutine torefer to functions declared using the new syntax.generator-basedcoroutine is used where necessary to refer to coroutines that arebased on generator syntax.coroutine is used in contexts where bothdefinitions are applicable.

New Coroutine Declaration Syntax

The following new syntax is used to declare anative coroutine:

asyncdefread_data(db):pass

Key properties ofcoroutines:

  • asyncdef functions are always coroutines, even if they do notcontainawait expressions.
  • It is aSyntaxError to haveyield oryieldfromexpressions in anasync function.
  • Internally, two new code object flags were introduced:
    • CO_COROUTINE is used to marknative coroutines(defined with new syntax).
    • CO_ITERABLE_COROUTINE is used to makegenerator-basedcoroutines compatible withnative coroutines (set bytypes.coroutine() function).
  • Regular generators, when called, return agenerator object;similarly, coroutines return acoroutine object.
  • StopIteration exceptions are not propagated out of coroutines,and are replaced with aRuntimeError. For regular generatorssuch behavior requires a future import (seePEP 479).
  • When anative coroutine is garbage collected, aRuntimeWarningis raised if it was never awaited on (see alsoDebugging Features).
  • See alsoCoroutine objects section.

types.coroutine()

A new functioncoroutine(fn) is added to thetypes module. Itallows interoperability between existinggenerator-based coroutinesin asyncio andnative coroutines introduced by this PEP:

@types.coroutinedefprocess_data(db):data=yield fromread_data(db)...

The function appliesCO_ITERABLE_COROUTINE flag togenerator-function’s code object, making it return acoroutine object.

Iffn is not agenerator function, it is wrapped. If it returnsagenerator, it will be wrapped in anawaitable proxy object(see below the definition of awaitable objects).

Note, that theCO_COROUTINE flag is not applied bytypes.coroutine() to make it possible to separatenativecoroutines defined with new syntax, fromgenerator-based coroutines.

Await Expression

The following newawait expression is used to obtain a result ofcoroutine execution:

asyncdefread_data(db):data=awaitdb.fetch('SELECT ...')...

await, similarly toyieldfrom, suspends execution ofread_data coroutine untildb.fetchawaitable completes andreturns the result data.

It uses theyieldfrom implementation with an extra step ofvalidating its argument.await only accepts anawaitable, whichcan be one of:

  • Anative coroutine object returned from anative coroutinefunction.
  • Agenerator-based coroutine object returned from a functiondecorated withtypes.coroutine().
  • An object with an__await__ method returning an iterator.

    Anyyieldfrom chain of calls ends with ayield. This is afundamental mechanism of howFutures are implemented. Since,internally, coroutines are a special kind of generators, everyawait is suspended by ayield somewhere down the chain ofawait calls (please refer toPEP 3156 for a detailedexplanation).

    To enable this behavior for coroutines, a new magic method called__await__ is added. In asyncio, for instance, to enableFutureobjects inawait statements, the only change is to add__await__=__iter__ line toasyncio.Future class.

    Objects with__await__ method are calledFuture-like objects inthe rest of this PEP.

    It is aTypeError if__await__ returns anything but aniterator.

  • Objects defined with CPython C API with atp_as_async.am_awaitfunction, returning aniterator (similar to__await__ method).

It is aSyntaxError to useawait outside of anasyncdeffunction (like it is aSyntaxError to useyield outside ofdef function).

It is aTypeError to pass anything other than anawaitable objectto anawait expression.

Updated operator precedence table

await keyword is defined as follows:

power::=await["**"u_expr]await::=["await"]primary

where “primary” represents the most tightly bound operations of thelanguage. Its syntax is:

primary::=atom|attributeref|subscription|slicing|call

See Python Documentation[12] andGrammar Updates section of thisproposal for details.

The keyawait difference fromyield andyieldfromoperators is thatawait expressions do not require parentheses aroundthem most of the times.

Also,yieldfrom allows any expression as its argument, includingexpressions likeyieldfroma()+b(), that would be parsed asyieldfrom(a()+b()), which is almost always a bug. In general,the result of any arithmetic operation is not anawaitable object.To avoid this kind of mistakes, it was decided to makeawaitprecedence lower than[],(), and., but higher than**operators.

OperatorDescription
yieldx,yieldfromxYield expression
lambdaLambda expression
ifelseConditional expression
orBoolean OR
andBoolean AND
notxBoolean NOT
in,notin,is,isnot,<,<=,>,>=,!=,==Comparisons, including membershiptests and identity tests
|Bitwise OR
^Bitwise XOR
&Bitwise AND
<<,>>Shifts
+,-Addition and subtraction
*,@,/,//,%Multiplication, matrixmultiplication, division,remainder
+x,-x,~xPositive, negative, bitwise NOT
**Exponentiation
awaitxAwait expression
x[index],x[index:index],x(arguments...),x.attributeSubscription, slicing,call, attribute reference
(expressions...),[expressions...],{key:value...},{expressions...}Binding or tuple display,list display,dictionary display,set display

Examples of “await” expressions

Valid syntax examples:

ExpressionWill be parsed as
ifawaitfut:passif(awaitfut):pass
ifawaitfut+1:passif(awaitfut)+1:pass
pair=awaitfut,'spam'pair=(awaitfut),'spam'
withawaitfut,open():passwith(awaitfut),open():pass
awaitfoo()['spam'].baz()()await(foo()['spam'].baz()())
returnawaitcoro()return(awaitcoro())
res=awaitcoro()**2res=(awaitcoro())**2
func(a1=awaitcoro(),a2=0)func(a1=(awaitcoro()),a2=0)
awaitfoo()+awaitbar()(awaitfoo())+(awaitbar())
-awaitfoo()-(awaitfoo())

Invalid syntax examples:

ExpressionShould be written as
awaitawaitcoro()await(awaitcoro())
await-coro()await(-coro())

Asynchronous Context Managers and “async with”

Anasynchronous context manager is a context manager that is able tosuspend execution in itsenter andexit methods.

To make this possible, a new protocol for asynchronous context managersis proposed. Two new magic methods are added:__aenter__ and__aexit__. Both must return anawaitable.

An example of an asynchronous context manager:

classAsyncContextManager:asyncdef__aenter__(self):awaitlog('entering context')asyncdef__aexit__(self,exc_type,exc,tb):awaitlog('exiting context')

New Syntax

A new statement for asynchronous context managers is proposed:

asyncwithEXPRasVAR:BLOCK

which is semantically equivalent to:

mgr=(EXPR)aexit=type(mgr).__aexit__aenter=type(mgr).__aenter__VAR=awaitaenter(mgr)try:BLOCKexcept:ifnotawaitaexit(mgr,*sys.exc_info()):raiseelse:awaitaexit(mgr,None,None,None)

As with regularwith statements, it is possible to specify multiplecontext managers in a singleasyncwith statement.

It is an error to pass a regular context manager without__aenter__and__aexit__ methods toasyncwith. It is aSyntaxErrorto useasyncwith outside of anasyncdef function.

Example

Withasynchronous context managers it is easy to implement properdatabase transaction managers for coroutines:

asyncdefcommit(session,data):...asyncwithsession.transaction():...awaitsession.update(data)...

Code that needs locking also looks lighter:

asyncwithlock:...

instead of:

with(yield fromlock):...

Asynchronous Iterators and “async for”

Anasynchronous iterable is able to call asynchronous code in itsiter implementation, andasynchronous iterator can callasynchronous code in itsnext method. To support asynchronousiteration:

  1. An object must implement an__aiter__ method (or, if definedwith CPython C API,tp_as_async.am_aiter slot) returning anasynchronous iterator object.
  2. Anasynchronous iterator object must implement an__anext__method (or, if defined with CPython C API,tp_as_async.am_anextslot) returning anawaitable.
  3. To stop iteration__anext__ must raise aStopAsyncIterationexception.

An example of asynchronous iterable:

classAsyncIterable:def__aiter__(self):returnselfasyncdef__anext__(self):data=awaitself.fetch_data()ifdata:returndataelse:raiseStopAsyncIterationasyncdeffetch_data(self):...

New Syntax

A new statement for iterating through asynchronous iterators isproposed:

asyncforTARGETinITER:BLOCKelse:BLOCK2

which is semantically equivalent to:

iter=(ITER)iter=type(iter).__aiter__(iter)running=Truewhilerunning:try:TARGET=awaittype(iter).__anext__(iter)exceptStopAsyncIteration:running=Falseelse:BLOCKelse:BLOCK2

It is aTypeError to pass a regular iterable without__aiter__method toasyncfor. It is aSyntaxError to useasyncforoutside of anasyncdef function.

As for with regularfor statement,asyncfor has an optionalelse clause.

Example 1

With asynchronous iteration protocol it is possible to asynchronouslybuffer data during iteration:

asyncfordataincursor:...

Wherecursor is an asynchronous iterator that prefetchesN rowsof data from a database after everyN iterations.

The following code illustrates new asynchronous iteration protocol:

classCursor:def__init__(self):self.buffer=collections.deque()asyncdef_prefetch(self):...def__aiter__(self):returnselfasyncdef__anext__(self):ifnotself.buffer:self.buffer=awaitself._prefetch()ifnotself.buffer:raiseStopAsyncIterationreturnself.buffer.popleft()

then theCursor class can be used as follows:

asyncforrowinCursor():print(row)

which would be equivalent to the following code:

i=Cursor().__aiter__()whileTrue:try:row=awaiti.__anext__()exceptStopAsyncIteration:breakelse:print(row)

Example 2

The following is a utility class that transforms a regular iterable toan asynchronous one. While this is not a very useful thing to do, thecode illustrates the relationship between regular and asynchronousiterators.

classAsyncIteratorWrapper:def__init__(self,obj):self._it=iter(obj)def__aiter__(self):returnselfasyncdef__anext__(self):try:value=next(self._it)exceptStopIteration:raiseStopAsyncIterationreturnvalueasyncforletterinAsyncIteratorWrapper("abc"):print(letter)

Why StopAsyncIteration?

Coroutines are still based on generators internally. So, before PEP479, there was no fundamental difference between

defg1():yield fromfutreturn'spam'

and

defg2():yield fromfutraiseStopIteration('spam')

And sincePEP 479 is accepted and enabled by default for coroutines,the following example will have itsStopIteration wrapped into aRuntimeError

asyncdefa1():awaitfutraiseStopIteration('spam')

The only way to tell the outside code that the iteration has ended isto raise something other thanStopIteration. Therefore, a newbuilt-in exception classStopAsyncIteration was added.

Moreover, with semantics fromPEP 479, allStopIteration exceptionsraised in coroutines are wrapped inRuntimeError.

Coroutine objects

Differences from generators

This section applies only tonative coroutines withCO_COROUTINEflag, i.e. defined with the newasyncdef syntax.

The behavior of existing *generator-based coroutines* in asyncioremains unchanged.

Great effort has been made to make sure that coroutines andgenerators are treated as distinct concepts:

  1. Native coroutine objects do not implement__iter__ and__next__ methods. Therefore, they cannot be iterated over orpassed toiter(),list(),tuple() and other built-ins.They also cannot be used in afor..in loop.

    An attempt to use__iter__ or__next__ on anativecoroutine object will result in aTypeError.

  2. Plain generators cannotyieldfromnative coroutines:doing so will result in aTypeError.
  3. generator-based coroutines (for asyncio code must be decoratedwith@asyncio.coroutine[1]) canyieldfromnative coroutineobjects.
  4. inspect.isgenerator() andinspect.isgeneratorfunction()returnFalse fornative coroutine objects andnativecoroutine functions.

Coroutine object methods

Coroutines are based on generators internally, thus they share theimplementation. Similarly to generator objects,coroutines havethrow(),send() andclose() methods.StopIteration andGeneratorExit play the same role for coroutines (althoughPEP 479 is enabled by default for coroutines). SeePEP 342,PEP 380,and Python Documentation[11] for details.

throw(),send() methods forcoroutines are used to pushvalues and raise errors intoFuture-like objects.

Debugging Features

A common beginner mistake is forgetting to useyieldfrom oncoroutines:

@asyncio.coroutinedefuseful():asyncio.sleep(1)# this will do nothing without 'yield from'

For debugging this kind of mistakes there is a special debug mode inasyncio, in which@coroutine decorator wraps all functions with aspecial object with a destructor logging a warning. Whenever a wrappedgenerator gets garbage collected, a detailed logging message isgenerated with information about where exactly the decorator functionwas defined, stack trace of where it was collected, etc. Wrapperobject also provides a convenient__repr__ function with detailedinformation about the generator.

The only problem is how to enable these debug capabilities. Sincedebug facilities should be a no-op in production mode,@coroutinedecorator makes the decision of whether to wrap or not to wrap based onan OS environment variablePYTHONASYNCIODEBUG. This way it ispossible to run asyncio programs with asyncio’s own functionsinstrumented.EventLoop.set_debug, a different debug facility, hasno impact on@coroutine decorator’s behavior.

With this proposal, coroutines is a native, distinct from generators,concept.In addition to aRuntimeWarning being raised oncoroutines that were never awaited, it is proposed to add two newfunctions to thesys module:set_coroutine_wrapper andget_coroutine_wrapper. This is to enable advanced debuggingfacilities in asyncio and other frameworks (such as displaying whereexactly coroutine was created, and a more detailed stack trace of whereit was garbage collected).

New Standard Library Functions

  • types.coroutine(gen). Seetypes.coroutine() section fordetails.
  • inspect.iscoroutine(obj) returnsTrue ifobj is anative coroutine object.
  • inspect.iscoroutinefunction(obj) returnsTrue ifobj is anative coroutine function.
  • inspect.isawaitable(obj) returnsTrue ifobj is anawaitable.
  • inspect.getcoroutinestate(coro) returns the current state ofanative coroutine object (mirrorsinspect.getfgeneratorstate(gen)).
  • inspect.getcoroutinelocals(coro) returns the mapping of anative coroutine object’s local variables to their values(mirrorsinspect.getgeneratorlocals(gen)).
  • sys.set_coroutine_wrapper(wrapper) allows to intercept creation ofnative coroutine objects.wrapper must be either a callable thataccepts one argument (acoroutine object), orNone.Noneresets the wrapper. If called twice, the new wrapper replaces theprevious one. The function is thread-specific. SeeDebuggingFeatures for more details.
  • sys.get_coroutine_wrapper() returns the current wrapper object.ReturnsNone if no wrapper was set. The function isthread-specific. SeeDebugging Features for more details.

New Abstract Base Classes

In order to allow better integration with existing frameworks (such asTornado, see[13]) and compilers (such as Cython, see[16]), two newAbstract Base Classes (ABC) are added:

  • collections.abc.Awaitable ABC forFuture-like classes, thatimplement__await__ method.
  • collections.abc.Coroutine ABC forcoroutine objects, thatimplementsend(value),throw(type,exc,tb),close() and__await__() methods.

    Note that generator-based coroutines withCO_ITERABLE_COROUTINEflag do not implement__await__ method, and therefore are notinstances ofcollections.abc.Coroutine andcollections.abc.Awaitable ABCs:

    @types.coroutinedefgencoro():yieldassertnotisinstance(gencoro(),collections.abc.Coroutine)# however:assertinspect.isawaitable(gencoro())

To allow easy testing if objects support asynchronous iteration, twomore ABCs are added:

  • collections.abc.AsyncIterable – tests for__aiter__ method.
  • collections.abc.AsyncIterator – tests for__aiter__ and__anext__ methods.

Glossary

Native coroutine function
A coroutine function is declared withasyncdef. It usesawait andreturnvalue; seeNew Coroutine DeclarationSyntax for details.
Native coroutine
Returned from a native coroutine function. SeeAwait Expressionfor details.
Generator-based coroutine function
Coroutines based on generator syntax. Most common example arefunctions decorated with@asyncio.coroutine.
Generator-based coroutine
Returned from a generator-based coroutine function.
Coroutine
Eithernative coroutine orgenerator-based coroutine.
Coroutine object
Eithernative coroutine object orgenerator-based coroutineobject.
Future-like object
An object with an__await__ method, or a C object withtp_as_async->am_await function, returning aniterator. Can beconsumed by anawait expression in a coroutine. A coroutinewaiting for a Future-like object is suspended until the Future-likeobject’s__await__ completes, and returns the result. SeeAwait Expression for details.
Awaitable
AFuture-like object or acoroutine object. SeeAwaitExpression for details.
Asynchronous context manager
An asynchronous context manager has__aenter__ and__aexit__methods and can be used withasyncwith. SeeAsynchronousContext Managers and “async with” for details.
Asynchronous iterable
An object with an__aiter__ method, which must return anasynchronous iterator object. Can be used withasyncfor.SeeAsynchronous Iterators and “async for” for details.
Asynchronous iterator
An asynchronous iterator has an__anext__ method. SeeAsynchronous Iterators and “async for” for details.

Transition Plan

To avoid backwards compatibility issues withasync andawaitkeywords, it was decided to modifytokenizer.c in such a way, thatit:

  • recognizesasyncdefNAME tokens combination;
  • while tokenizingasyncdef block, it replaces'async'NAME token withASYNC, and'await'NAME token withAWAIT;
  • while tokenizingdef block, it yields'async' and'await'NAME tokens as is.

This approach allows for seamless combination of new syntax features(all of them available only inasync functions) with any existingcode.

An example of having “async def” and “async” attribute in one piece ofcode:

classSpam:async=42asyncdefham():print(getattr(Spam,'async'))# The coroutine can be executed and will print '42'

Backwards Compatibility

This proposal preserves 100% backwards compatibility.

asyncio

asyncio module was adapted and tested to work with coroutines andnew statements. Backwards compatibility is 100% preserved, i.e. allexisting code will work as-is.

The required changes are mainly:

  1. Modify@asyncio.coroutine decorator to use newtypes.coroutine() function.
  2. Add__await__=__iter__ line toasyncio.Future class.
  3. Addensure_future() as an alias forasync() function.Deprecateasync() function.

asyncio migration strategy

Becauseplain generators cannotyieldfromnative coroutineobjects (seeDifferences from generators section for more details),it is advised to make sure that all generator-based coroutines aredecorated with@asyncio.coroutinebefore starting to use the newsyntax.

async/await in CPython code base

There is no use ofawait names in CPython.

async is mostly used by asyncio. We are addressing this byrenamingasync() function toensure_future() (seeasynciosection for details).

Another use ofasync keyword is inLib/xml/dom/xmlbuilder.py,to define anasync=False attribute forDocumentLS class.There is no documentation or tests for it, it is not used anywhere elsein CPython. It is replaced with a getter, that raises aDeprecationWarning, advising to useasync_ attribute instead.‘async’ attribute is not documented and is not used in CPython codebase.

Grammar Updates

Grammar changes are fairly minimal:

decorated:decorators(classdef|funcdef|async_funcdef)async_funcdef:ASYNCfuncdefcompound_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]atomtrailer*

Deprecation Plans

async andawait names will be softly deprecated in CPython 3.5and 3.6. In 3.7 we will transform them to proper keywords. Makingasync andawait proper keywords before 3.7 might make it harderfor people to port their code to Python 3.

Design Considerations

PEP 3152

PEP 3152 by Gregory Ewing proposes a different mechanism for coroutines(called “cofunctions”). Some key points:

  1. A new keywordcodef to declare acofunction.Cofunction isalways a generator, even if there is nococall expressionsinside it. Maps toasyncdef in this proposal.
  2. A new keywordcocall to call acofunction. Can only be usedinside acofunction. Maps toawait in this proposal (withsome differences, see below).
  3. It is not possible to call acofunction without acocallkeyword.
  4. cocall grammatically requires parentheses after it:
    atom:cocall|<existingalternativesforatom>cocall:'cocall'atomcotrailer*'('[arglist]')'cotrailer:'['subscriptlist']'|'.'NAME
  5. cocallf(*args,**kwds) is semantically equivalent toyieldfromf.__cocall__(*args,**kwds).

Differences from this proposal:

  1. There is no equivalent of__cocall__ in this PEP, which iscalled and its result is passed toyieldfrom in thecocallexpression.await keyword expects anawaitable object,validates the type, and executesyieldfrom on it. Although,__await__ method is similar to__cocall__, but is only usedto defineFuture-like objects.
  2. await is defined in almost the same way asyieldfrom in thegrammar (it is later enforced thatawait can only be insideasyncdef). It is possible to simply writeawaitfuture,whereascocall always requires parentheses.
  3. To make asyncio work withPEP 3152 it would be required to modify@asyncio.coroutine decorator to wrap all functions in an objectwith a__cocall__ method, or to implement__cocall__ ongenerators. To callcofunctions from existing generator-basedcoroutines it would be required to usecostart(cofunc,*args,**kwargs) built-in.
  4. Since it is impossible to call acofunction without acocallkeyword, it automatically prevents the common mistake of forgettingto useyieldfrom on generator-based coroutines. This proposaladdresses this problem with a different approach, seeDebuggingFeatures.
  5. A shortcoming of requiring acocall keyword to call a coroutineis that if is decided to implement coroutine-generators –coroutines withyield orasyncyield expressions – wewouldn’t need acocall keyword to call them. So we’ll end uphaving__cocall__ and no__call__ for regular coroutines,and having__call__ and no__cocall__ forcoroutine-generators.
  6. Requiring parentheses grammatically also introduces a whole lotof new problems.

    The following code:

    awaitfutawaitfunction_returning_future()awaitasyncio.gather(coro1(arg1,arg2),coro2(arg1,arg2))

    would look like:

    cocallfut()# or cocall costart(fut)cocall(function_returning_future())()cocallasyncio.gather(costart(coro1,arg1,arg2),costart(coro2,arg1,arg2))
  7. There are no equivalents ofasyncfor andasyncwith in PEP3152.

Coroutine-generators

Withasyncfor keyword it is desirable to have a concept of acoroutine-generator – a coroutine withyield andyieldfromexpressions. To avoid any ambiguity with regular generators, we wouldlikely require to have anasync keyword beforeyield, andasyncyieldfrom would raise aStopAsyncIteration exception.

While it is possible to implement coroutine-generators, we believe thatthey are out of scope of this proposal. It is an advanced concept thatshould be carefully considered and balanced, with a non-trivial changesin the implementation of current generator objects. This is a matterfor a separate PEP.

Why “async” and “await” keywords

async/await is not a new concept in programming languages:

  • C# has it since long time ago[5];
  • proposal to add async/await in ECMAScript 7[2];see also Traceur project[9];
  • Facebook’s Hack/HHVM[6];
  • Google’s Dart language[7];
  • Scala[8];
  • proposal to add async/await to C++[10];
  • and many other less popular languages.

This is a huge benefit, as some users already have experience withasync/await, and because it makes working with many languages in oneproject easier (Python with ECMAScript 7 for instance).

Why “__aiter__” does not return an awaitable

PEP 492 was accepted in CPython 3.5.0 with__aiter__ defined asa method, that was expected to return an awaitable resolving to anasynchronous iterator.

In 3.5.2 (asPEP 492 was accepted on a provisional basis) the__aiter__ protocol was updated to return asynchronous iteratorsdirectly.

The motivation behind this change is to make it possible toimplement asynchronous generators in Python. See[19] and[20] formore details.

Importance of “async” keyword

While it is possible to just implementawait expression and treatall functions with at least oneawait as coroutines, this approachmakes APIs design, code refactoring and its long time support harder.

Let’s pretend that Python only hasawait keyword:

defuseful():...awaitlog(...)...defimportant():awaituseful()

Ifuseful() function is refactored and someone removes allawait expressions from it, it would become a regular pythonfunction, and all code that depends on it, includingimportant()would be broken. To mitigate this issue a decorator similar to@asyncio.coroutine has to be introduced.

Why “async def”

For some people bareasyncname():pass syntax might look moreappealing thanasyncdefname():pass. It is certainly easier totype. But on the other hand, it breaks the symmetry betweenasyncdef,asyncwith andasyncfor, whereasync is a modifier,stating that the statement is asynchronous. It is also more consistentwith the existing grammar.

Why not “await for” and “await with”

async is an adjective, and hence it is a better choice for astatement qualifier keyword.awaitfor/with would imply thatsomething is awaiting for a completion of afor orwithstatement.

Why “async def” and not “def async”

async keyword is astatement qualifier. A good analogy to it are“static”, “public”, “unsafe” keywords from other languages. “asyncfor” is an asynchronous “for” statement, “async with” is anasynchronous “with” statement, “async def” is an asynchronous function.

Having “async” after the main statement keyword might introduce someconfusion, like “for async item in iterator” can be read as “for eachasynchronous item in iterator”.

Havingasync keyword beforedef,with andfor alsomakes the language grammar simpler. And “async def” better separatescoroutines from regular functions visually.

Why not a __future__ import

Transition Plan section explains how tokenizer is modified to treatasync andawait as keywordsonly inasyncdef blocks.Henceasyncdef fills the role that a module level compilerdeclaration likefrom__future__importasync_await would otherwisefill.

Why magic methods start with “a”

New asynchronous magic methods__aiter__,__anext__,__aenter__, and__aexit__ all start with the same prefix “a”.An alternative proposal is to use “async” prefix, so that__anext__becomes__async_next__. However, to align new magic methods withthe existing ones, such as__radd__ and__iadd__ it was decidedto use a shorter version.

Why not reuse existing magic names

An alternative idea about new asynchronous iterators and contextmanagers was to reuse existing magic methods, by adding anasynckeyword to their declarations:

classCM:asyncdef__enter__(self):# instead of __aenter__...

This approach has the following downsides:

  • it would not be possible to create an object that works in bothwith andasyncwith statements;
  • it would break backwards compatibility, as nothing prohibits fromreturning a Future-like objects from__enter__ and/or__exit__ in Python <= 3.4;
  • one of the main points of this proposal is to make native coroutinesas simple and foolproof as possible, hence the clear separation ofthe protocols.

Why not reuse existing “for” and “with” statements

The vision behind existing generator-based coroutines and this proposalis to make it easy for users to see where the code might be suspended.Making existing “for” and “with” statements to recognize asynchronousiterators and context managers will inevitably create implicit suspendpoints, making it harder to reason about the code.

Comprehensions

Syntax for asynchronous comprehensions could be provided, but thisconstruct is outside of the scope of this PEP.

Async lambda functions

Syntax for asynchronous lambda functions could be provided, but thisconstruct is outside of the scope of this PEP.

Performance

Overall Impact

This proposal introduces no observable performance impact. Here is anoutput of python’s official set of benchmarks[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_64x86_64 i386Total CPU cores: 8### etree_iterparse ###Min: 0.365359 -> 0.349168: 1.05x fasterAvg: 0.396924 -> 0.379735: 1.05x fasterSignificant (t=9.71)Stddev: 0.01225 -> 0.01277: 1.0423x largerThe 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.

Tokenizer modifications

There is no observable slowdown of parsing python files with themodified tokenizer: parsing of one 12Mb file(Lib/test/test_binop.py repeated 1000 times) takes the same amountof time.

async/await

The following micro-benchmark was used to determine performancedifference between “async” functions and generators:

importsysimporttimedefbinary(n):ifn<=0:return1l=yield frombinary(n-1)r=yield frombinary(n-1)returnl+1+rasyncdefabinary(n):ifn<=0:return1l=awaitabinary(n-1)r=awaitabinary(n-1)returnl+1+rdeftimeit(func,depth,repeat):t0=time.time()for_inrange(repeat):o=func(depth)try:whileTrue:o.send(None)exceptStopIteration:passt1=time.time()print('{}({}) *{}: total{:.3f}s'.format(func.__name__,depth,repeat,t1-t0))

The result is that there is no observable performance difference:

binary(19)*30:total53.321sabinary(19)*30:total55.073sbinary(19)*30:total53.361sabinary(19)*30:total51.360sbinary(19)*30:total49.438sabinary(19)*30:total51.047s

Note that depth of 19 means 1,048,575 calls.

Reference Implementation

The reference implementation can be found here:[3].

List of high-level changes and new protocols

  1. New syntax for defining coroutines:asyncdef and newawaitkeyword.
  2. New__await__ method for Future-like objects, and newtp_as_async.am_await slot inPyTypeObject.
  3. New syntax for asynchronous context managers:asyncwith. Andassociated protocol with__aenter__ and__aexit__ methods.
  4. New syntax for asynchronous iteration:asyncfor. Andassociated protocol with__aiter__,__aexit__ and new built-inexceptionStopAsyncIteration. Newtp_as_async.am_aiterandtp_as_async.am_anext slots inPyTypeObject.
  5. New AST nodes:AsyncFunctionDef,AsyncFor,AsyncWith,Await.
  6. New functions:sys.set_coroutine_wrapper(callback),sys.get_coroutine_wrapper(),types.coroutine(gen),inspect.iscoroutinefunction(func),inspect.iscoroutine(obj),inspect.isawaitable(obj),inspect.getcoroutinestate(coro),andinspect.getcoroutinelocals(coro).
  7. NewCO_COROUTINE andCO_ITERABLE_COROUTINE bit flags for codeobjects.
  8. New ABCs:collections.abc.Awaitable,collections.abc.Coroutine,collections.abc.AsyncIterable, andcollections.abc.AsyncIterator.
  9. C API changes: newPyCoro_Type (exposed to Python astypes.CoroutineType) andPyCoroObject.PyCoro_CheckExact(*o) to test ifo is anative coroutine.

While the list of changes and new things is not short, it is importantto understand, that most users will not use these features directly.It is intended to be used in frameworks and libraries to provide userswith convenient to use and unambiguous APIs withasyncdef,await,asyncfor andasyncwith syntax.

Working example

All concepts proposed in this PEP are implemented[3] and can betested.

importasyncioasyncdefecho_server():print('Serving on localhost:8000')awaitasyncio.start_server(handle_connection,'localhost',8000)asyncdefhandle_connection(reader,writer):print('New connection...')whileTrue:data=awaitreader.read(8192)ifnotdata:breakprint('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()

Acceptance

PEP 492 was accepted by Guido, Tuesday, May 5, 2015[14].

Implementation

The implementation is tracked in issue 24017[15]. It wascommitted on May 11, 2015.

References

[1]
https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine
[2] (1,2)
http://wiki.ecmascript.org/doku.php?id=strawman:async_functions
[3] (1,2)
https://github.com/1st1/cpython/tree/await
[4]
https://hg.python.org/benchmarks
[5] (1,2)
https://msdn.microsoft.com/en-us/library/hh191443.aspx
[6] (1,2)
http://docs.hhvm.com/manual/en/hack.async.php
[7] (1,2)
https://www.dartlang.org/articles/await-async/
[8] (1,2)
http://docs.scala-lang.org/sips/pending/async.html
[9]
https://github.com/google/traceur-compiler/wiki/LanguageFeatures#async-functions-experimental
[10] (1,2)
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3722.pdf (PDF)
[11]
https://docs.python.org/3/reference/expressions.html#generator-iterator-methods
[12]
https://docs.python.org/3/reference/expressions.html#primaries
[13]
https://mail.python.org/pipermail/python-dev/2015-May/139851.html
[14]
https://mail.python.org/pipermail/python-dev/2015-May/139844.html
[15]
http://bugs.python.org/issue24017
[16]
https://github.com/python/asyncio/issues/233
[17]
https://hg.python.org/cpython/rev/7a0a1a4ac639
[18]
http://bugs.python.org/issue24400
[19] (1,2)
http://bugs.python.org/issue27243
[20] (1,2)
https://docs.python.org/3/reference/datamodel.html#async-iterators

Acknowledgments

I thank Guido van Rossum, Victor Stinner, Elvis Pranskevichus, AndrewSvetlov, Łukasz Langa, Greg Ewing, Stephen J. Turnbull, Jim J. Jewett,Brett Cannon, Alyssa Coghlan, Steven D’Aprano, Paul Moore, NathanielSmith, Ethan Furman, Stefan Behnel, Paul Sokolovsky, Victor Petrovykh,and many others for their feedback, ideas, edits, criticism, codereviews, and discussions around this PEP.

Copyright

This document has been placed in the public domain.


Source:https://github.com/python/peps/blob/main/peps/pep-0492.rst

Last modified:2025-02-01 08:55:40 GMT


[8]ページ先頭

©2009-2025 Movatter.jp