Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 342 – Coroutines via Enhanced Generators

Author:
Guido van Rossum, Phillip J. Eby
Status:
Final
Type:
Standards Track
Created:
10-May-2005
Python-Version:
2.5
Post-History:


Table of Contents

Introduction

This PEP proposes some enhancements to the API and syntax of generators, tomake them usable as simple coroutines. It is basically a combination of ideasfrom these two PEPs, which may be considered redundant if this PEP isaccepted:

  • PEP 288, Generators Attributes and Exceptions. The current PEP covers itssecond half, generator exceptions (in fact thethrow() method name wastaken fromPEP 288).PEP 342 replaces generator attributes, however, with aconcept from an earlier revision ofPEP 288, theyield expression.
  • PEP 325, Resource-Release Support for Generators.PEP 342 ties up a fewloose ends in thePEP 325 spec, to make it suitable for actualimplementation.

Motivation

Coroutines are a natural way of expressing many algorithms, such assimulations, games, asynchronous I/O, and other forms of event-drivenprogramming or co-operative multitasking. Python’s generator functions arealmost coroutines – but not quite – in that they allow pausing execution toproduce a value, but do not provide for values or exceptions to be passed inwhen execution resumes. They also do not allow execution to be paused withinthetry portion oftry/finally blocks, and therefore make it difficultfor an aborted coroutine to clean up after itself.

Also, generators cannot yield control while other functions are executing,unless those functions are themselves expressed as generators, and the outergenerator is written to yield in response to values yielded by the innergenerator. This complicates the implementation of even relatively simple usecases like asynchronous communications, because calling any functions eitherrequires the generator toblock (i.e. be unable to yield control), or else alot of boilerplate looping code must be added around every needed functioncall.

However, if it were possible to pass values or exceptionsinto a generator atthe point where it was suspended, a simple co-routine scheduler ortrampolinefunction would let coroutinescall each other without blocking – atremendous boon for asynchronous applications. Such applications could thenwrite co-routines to do non-blocking socket I/O by yielding control to an I/Oscheduler until data has been sent or becomes available. Meanwhile, code thatperforms the I/O would simply do something like this:

data=(yieldnonblocking_read(my_socket,nbytes))

in order to pause execution until thenonblocking_read() coroutine produceda value.

In other words, with a few relatively minor enhancements to the language and tothe implementation of the generator-iterator type, Python will be able tosupport performing asynchronous operations without needing to write the entireapplication as a series of callbacks, and without requiring the use ofresource-intensive threads for programs that need hundreds or even thousands ofco-operatively multitasking pseudothreads. Thus, these enhancements will givestandard Python many of the benefits of the Stackless Python fork, withoutrequiring any significant modification to the CPython core or its APIs. Inaddition, these enhancements should be readily implementable by any Pythonimplementation (such as Jython) that already supports generators.

Specification Summary

By adding a few simple methods to the generator-iterator type, and with twominor syntax adjustments, Python developers will be able to use generatorfunctions to implement co-routines and other forms of co-operativemultitasking. These methods and adjustments are:

  1. Redefineyield to be an expression, rather than a statement. The currentyield statement would become a yield expression whose value is thrown away.A yield expression’s value isNone whenever the generator is resumed bya normalnext() call.
  2. Add a newsend() method for generator-iterators, which resumes thegenerator andsends a value that becomes the result of the currentyield-expression. Thesend() method returns the next value yielded bythe generator, or raisesStopIteration if the generator exits withoutyielding another value.
  3. Add a newthrow() method for generator-iterators, which raises anexception at the point where the generator was paused, and which returns thenext value yielded by the generator, raisingStopIteration if thegenerator exits without yielding another value. (If the generator does notcatch the passed-in exception, or raises a different exception, then thatexception propagates to the caller.)
  4. Add aclose() method for generator-iterators, which raisesGeneratorExit at the point where the generator was paused. If thegenerator then raisesStopIteration (by exiting normally, or due toalready being closed) orGeneratorExit (by not catching the exception),close() returns to its caller. If the generator yields a value, aRuntimeError is raised. If the generator raises any other exception, itis propagated to the caller.close() does nothing if the generator hasalready exited due to an exception or normal exit.
  5. Add support to ensure thatclose() is called when a generator iteratoris garbage-collected.
  6. Allowyield to be used intry/finally blocks, since garbagecollection or an explicitclose() call would now allow thefinallyclause to execute.

A prototype patch implementing all of these changes against the current PythonCVS HEAD is available as SourceForge patch #1223381(https://bugs.python.org/issue1223381).

Specification: Sending Values into Generators

New generator method:send(value)

A new method for generator-iterators is proposed, calledsend(). Ittakes exactly one argument, which is the value that should besent in tothe generator. Callingsend(None) is exactly equivalent to calling agenerator’snext() method. Callingsend() with any other value isthe same, except that the value produced by the generator’s currentyield expression will be different.

Because generator-iterators begin execution at the top of the generator’sfunction body, there is no yield expression to receive a value when thegenerator has just been created. Therefore, callingsend() with anon-None argument is prohibited when the generator iterator has juststarted, and aTypeError is raised if this occurs (presumably due to alogic error of some kind). Thus, before you can communicate with acoroutine you must first callnext() orsend(None) to advance itsexecution to the first yield expression.

As with thenext() method, thesend() method returns the next valueyielded by the generator-iterator, or raisesStopIteration if thegenerator exits normally, or has already exited. If the generator raises anuncaught exception, it is propagated tosend()’s caller.

New syntax: Yield Expressions

The yield-statement will be allowed to be used on the right-hand side of anassignment; in that case it is referred to as yield-expression. The valueof this yield-expression isNone unlesssend() was called with anon-None argument; see below.

A yield-expression must always be parenthesized except when it occurs at thetop-level expression on the right-hand side of an assignment. So

x=yield42x=yieldx=12+(yield42)x=12+(yield)foo(yield42)foo(yield)

are all legal, but

x=12+yield42x=12+yieldfoo(yield42,12)foo(yield,12)

are all illegal. (Some of the edge cases are motivated by the currentlegality ofyield12,42.)

Note that a yield-statement or yield-expression without an expression is nowlegal. This makes sense: when the information flow in thenext() callis reversed, it should be possible to yield without passing an explicitvalue (yield is of course equivalent toyieldNone).

Whensend(value) is called, the yield-expression that it resumes willreturn the passed-in value. Whennext() is called, the resumedyield-expression will returnNone. If the yield-expression is ayield-statement, this returned value is ignored, similar to ignoring thevalue returned by a function call used as a statement.

In effect, a yield-expression is like an inverted function call; theargument to yield is in fact returned (yielded) from the currently executingfunction, and thereturn value of yield is the argument passed in viasend().

Note: the syntactic extensions to yield make its use very similar to that inRuby. This is intentional. Do note that in Python the block passes a valueto the generator usingsend(EXPR) rather thanreturnEXPR, and theunderlying mechanism whereby control is passed between the generator and theblock is completely different. Blocks in Python are not compiled intothunks; rather,yield suspends execution of the generator’s frame. Someedge cases work differently; in Python, you cannot save the block for lateruse, and you cannot test whether there is a block or not. (XXX - this stuffabout blocks seems out of place now, perhaps Guido can edit to clarify.)

Specification: Exceptions and Cleanup

Let a generator object be the iterator produced by calling a generatorfunction. Below,g always refers to a generator object.

New syntax:yield allowed insidetry-finally

The syntax for generator functions is extended to allow a yield-statementinside atry-finally statement.

New generator method:throw(type,value=None,traceback=None)

g.throw(type,value,traceback) causes the specified exception to bethrown at the point where the generatorg is currently suspended (i.e. ata yield-statement, or at the start of its function body ifnext() hasnot been called yet). If the generator catches the exception and yieldsanother value, that is the return value ofg.throw(). If it doesn’tcatch the exception, thethrow() appears to raise the same exceptionpassed it (itfalls through). If the generator raises another exception(this includes theStopIteration produced when it returns) thatexception is raised by thethrow() call. In summary,throw()behaves likenext() orsend(), except it raises an exception at thesuspension point. If the generator is already in the closed state,throw() just raises the exception it was passed without executing any ofthe generator’s code.

The effect of raising the exception is exactly as if the statement:

raisetype,value,traceback

was executed at the suspension point. The type argument must not beNone, and the type and value must be compatible. If the value is not aninstance of the type, a new exception instance is created using the value,following the same rules that theraise statement uses to create anexception instance. The traceback, if supplied, must be a valid Pythontraceback object, or aTypeError occurs.

Note: The name of thethrow() method was selected for several reasons.Raise is a keyword and so cannot be used as a method name. Unlikeraise (which immediately raises an exception from the current executionpoint),throw() first resumes the generator, and only then raises theexception. The wordthrow is suggestive of putting the exception inanother location, and is already associated with exceptions in otherlanguages.

Alternative method names were considered:resolve(),signal(),genraise(),raiseinto(), andflush(). None of these seem to fitas well asthrow().

New standard exception:GeneratorExit

A new standard exception is defined,GeneratorExit, inheriting fromException. A generator should handle this by re-raising it (or just notcatching it) or by raisingStopIteration.

New generator method:close()

g.close() is defined by the following pseudo-code:

defclose(self):try:self.throw(GeneratorExit)except(GeneratorExit,StopIteration):passelse:raiseRuntimeError("generator ignored GeneratorExit")# Other exceptions are not caught

New generator method: __del__()

g.__del__() is a wrapper forg.close(). This will be called whenthe generator object is garbage-collected (in CPython, this is when itsreference count goes to zero). Ifclose() raises an exception, atraceback for the exception is printed tosys.stderr and furtherignored; it is not propagated back to the place that triggered the garbagecollection. This is consistent with the handling of exceptions in__del__() methods on class instances.

If the generator object participates in a cycle,g.__del__() may not becalled. This is the behavior of CPython’s current garbage collector. Thereason for the restriction is that the GC code needs tobreak a cycle atan arbitrary point in order to collect it, and from then on no Python codeshould be allowed to see the objects that formed the cycle, as they may bein an invalid state. Objectshanging off a cycle are not subject to thisrestriction.

Note that it is unlikely to see a generator object participate in a cycle inpractice. However, storing a generator object in a global variable createsa cycle via the generator frame’sf_globals pointer. Another way tocreate a cycle would be to store a reference to the generator object in adata structure that is passed to the generator as an argument (e.g., if anobject has a method that’s a generator, and keeps a reference to a runningiterator created by that method). Neither of these cases are very likelygiven the typical patterns of generator use.

Also, in the CPython implementation of this PEP, the frame object used bythe generator should be released whenever its execution is terminated due toan error or normal exit. This will ensure that generators that cannot beresumed do not remain part of an uncollectable reference cycle. This allowsother code to potentially useclose() in atry/finally orwithblock (perPEP 343) to ensure that a given generator is properly finalized.

Optional Extensions

The Extendedcontinue Statement

An earlier draft of this PEP proposed a newcontinueEXPR syntax for usein for-loops (carried over fromPEP 340), that would pass the value ofEXPR into the iterator being looped over. This feature has been withdrawnfor the time being, because the scope of this PEP has been narrowed to focusonly on passing values into generator-iterators, and not other kinds ofiterators. It was also felt by some on the Python-Dev list that adding newsyntax for this particular feature would be premature at best.

Open Issues

Discussion on python-dev has revealed some open issues. I list them here, withmy preferred resolution and its motivation. The PEP as currently writtenreflects this preferred resolution.

  1. What exception should be raised byclose() when the generator yieldsanother value as a response to theGeneratorExit exception?

    I originally choseTypeError because it represents gross misbehavior ofthe generator function, which should be fixed by changing the code. But thewith_template decorator class inPEP 343 usesRuntimeError forsimilar offenses. Arguably they should all use the same exception. I’drather not introduce a new exception class just for this purpose, since it’snot an exception that I want people to catch: I want it to turn into atraceback which is seen by the programmer who then fixes the code. So now Ibelieve they should both raiseRuntimeError. There are some precedentsfor that: it’s raised by the core Python code in situations where endlessrecursion is detected, and for uninitialized objects (and for a variety ofmiscellaneous conditions).

  2. Oren Tirosh has proposed renaming thesend() method tofeed(), forcompatibility with theconsumer interface (seehttp://effbot.org/zone/consumer.htm for the specification.)

    However, looking more closely at the consumer interface, it seems that thedesired semantics forfeed() are different than forsend(), becausesend() can’t be meaningfully called on a just-started generator. Also,the consumer interface as currently defined doesn’t include handling forStopIteration.

    Therefore, it seems like it would probably be more useful to create a simpledecorator that wraps a generator function to make it conform to the consumerinterface. For example, it couldwarm up the generator with an initialnext() call, trap StopIteration, and perhaps even providereset() byre-invoking the generator function.

Examples

  1. A simpleconsumer decorator that makes a generator function automaticallyadvance to its first yield point when initially called:
    defconsumer(func):defwrapper(*args,**kw):gen=func(*args,**kw)gen.next()returngenwrapper.__name__=func.__name__wrapper.__dict__=func.__dict__wrapper.__doc__=func.__doc__returnwrapper
  2. An example of using theconsumer decorator to create areverse generatorthat receives images and creates thumbnail pages, sending them on to anotherconsumer. Functions like this can be chained together to form efficientprocessing pipelines ofconsumers that each can have complex internalstate:
    @consumerdefthumbnail_pager(pagesize,thumbsize,destination):whileTrue:page=new_image(pagesize)rows,columns=pagesize/thumbsizepending=Falsetry:forrowinxrange(rows):forcolumninxrange(columns):thumb=create_thumbnail((yield),thumbsize)page.write(thumb,col*thumbsize.x,row*thumbsize.y)pending=TrueexceptGeneratorExit:# close() was called, so flush any pending outputifpending:destination.send(page)# then close the downstream consumer, and exitdestination.close()returnelse:# we finished a page full of thumbnails, so send it# downstream and keep on loopingdestination.send(page)@consumerdefjpeg_writer(dirname):fileno=1whileTrue:filename=os.path.join(dirname,"page%04d.jpg"%fileno)write_jpeg((yield),filename)fileno+=1# Put them together to make a function that makes thumbnail# pages from a list of images and other parameters.#defwrite_thumbnails(pagesize,thumbsize,images,output_dir):pipeline=thumbnail_pager(pagesize,thumbsize,jpeg_writer(output_dir))forimageinimages:pipeline.send(image)pipeline.close()
  3. A simple co-routine scheduler ortrampoline that lets coroutinescallother coroutines by yielding the coroutine they wish to invoke. Anynon-generator value yielded by a coroutine is returned to the coroutine thatcalled the one yielding the value. Similarly, if a coroutine raises anexception, the exception is propagated to itscaller. In effect, thisexample emulates simple tasklets as are used in Stackless Python, as long asyou use a yield expression to invoke routines that would otherwiseblock.This is only a very simple example, and far more sophisticated schedulersare possible. (For example, the existing GTasklet framework for Python(http://www.gnome.org/~gjc/gtasklet/gtasklets.html) and the peak.eventsframework (http://peak.telecommunity.com/) already implement similarscheduling capabilities, but must currently use awkward workarounds for theinability to pass values or exceptions into generators.)
    importcollectionsclassTrampoline:"""Manage communications between coroutines"""running=Falsedef__init__(self):self.queue=collections.deque()defadd(self,coroutine):"""Request that a coroutine be executed"""self.schedule(coroutine)defrun(self):result=Noneself.running=Truetry:whileself.runningandself.queue:func=self.queue.popleft()result=func()returnresultfinally:self.running=Falsedefstop(self):self.running=Falsedefschedule(self,coroutine,stack=(),val=None,*exc):defresume():value=valtry:ifexc:value=coroutine.throw(value,*exc)else:value=coroutine.send(value)except:ifstack:# send the error back to the "caller"self.schedule(stack[0],stack[1],*sys.exc_info())else:# Nothing left in this pseudothread to# handle it, let it propagate to the# run loopraiseifisinstance(value,types.GeneratorType):# Yielded to a specific coroutine, push the# current one on the stack, and call the new# one with no argsself.schedule(value,(coroutine,stack))elifstack:# Yielded a result, pop the stack and send the# value to the callerself.schedule(stack[0],stack[1],value)# else: this pseudothread has endedself.queue.append(resume)
  4. A simpleecho server, and code to run it using a trampoline (presumes theexistence ofnonblocking_read,nonblocking_write, and other I/Ocoroutines, that e.g. raiseConnectionLost if the connection isclosed):
    # coroutine function that echos data back on a connected# socket#defecho_handler(sock):whileTrue:try:data=yieldnonblocking_read(sock)yieldnonblocking_write(sock,data)exceptConnectionLost:pass# exit normally if connection lost# coroutine function that listens for connections on a# socket, and then launches a service "handler" coroutine# to service the connection#deflisten_on(trampoline,sock,handler):whileTrue:# get the next incoming connectionconnected_socket=yieldnonblocking_accept(sock)# start another coroutine to handle the connectiontrampoline.add(handler(connected_socket))# Create a scheduler to manage all our coroutinest=Trampoline()# Create a coroutine instance to run the echo_handler on# incoming connections#server=listen_on(t,listening_socket("localhost","echo"),echo_handler)# Add the coroutine to the schedulert.add(server)# loop forever, accepting connections and servicing them# "in parallel"#t.run()

Reference Implementation

A prototype patch implementing all of the features described in this PEP isavailable as SourceForge patch #1223381 (https://bugs.python.org/issue1223381).

This patch was committed to CVS 01-02 August 2005.

Acknowledgements

Raymond Hettinger (PEP 288) and Samuele Pedroni (PEP 325) first formallyproposed the ideas of communicating values or exceptions into generators, andthe ability toclose generators. Timothy Delaney suggested the title of thisPEP, and Steven Bethard helped edit a previous version. See also theAcknowledgements section ofPEP 340.

References

TBD.

Copyright

This document has been placed in the public domain.


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

Last modified:2025-02-01 08:59:27 GMT


[8]ページ先頭

©2009-2025 Movatter.jp