[Python-Dev] Yield-From Implementation Updated for Python 3
P.J. Eby
pje at telecommunity.com
Mon Aug 2 08:11:09 CEST 2010
At 09:24 PM 8/1/2010 -0700, Guido van Rossum wrote:
>I don't understand all the details and corner
>cases (e.g. the concatenation of stacks
It's just to ensure that you never have From's iterating over other
From's, vs. just iterating whatever's at the top of the stack.
>which seems to have to do with the special-casing of From objects in __new__)
It isn't connected, actually except that it's another place where I'm
keeping From's flat, instead of nested. (I hear that flat is better. ;-) )
>I am curious whether, if you need a trampoline for async I/O anyway,
>there would be a swaying argument for integrating this functionality
>into the general trampoline (as in the PEP 342 example),
Originally, that was why I wasn't very enthusiastic about PEP 380; it
didn't seem to me to be adding any new value over what you could do
with existing, widely-used libraries. (Twisted's had its own *and*
multiple third-party From-ish libraries supporting it for many years now.)
After I wrote From(), however (which was originally intended to show
why I thought 380 was unnecessary), I realized that having One
Obvious Way to implement generator-based pseudothreads independent of
an event loop, is actually useful precisely *because* it separates
the pseudothreadedness from what you're using the pseudothreadedness for.
Essentially, the PEP 380-ish bit is the hardest part of writing an
actual pseudothread implementation; connecting that implementation to
an I/O framework is actually the relatively simple part. You just
write code that steps into the generator, and uses the yielded object
to initiate an I/O operation and register a callback. (If you're
using Twisted or something else that has "promise"-like deferred
results, it's *really* easy, because you only have a couple of types
of yielded objects to deal with, and a uniform callback signature.)
Indeed, if you're using an existing async I/O framework, you don't
even really *have* a "trampoline" as such -- you just have a bit of
code that registers callbacks to itself, and the app's main event
loop just calls back to that wrapper when the I/O is done.
In effect, an I/O framework integration would just give you a single
API like, "run(From(geniter))", which then performs one iteration,
and then registers whatever callback it's told to by the yield, and
the callback it registers would actually be a reinvocation of run()
on the same From instance when the I/O is ready, but with a value to
pass back into the send(), or an error to throw(). So, the I/O
framework's event loop is half of the "trampoline", and the wrapper
that sends or throws, then registers an I/O callback, is the other half.
Something like:
def run(coroutine, value=None, exc_info=()):
if exc_info:
action = coroutine.throw(*exc_info)
else:
action = coroutine.send(value)
action.registerCallback(partial(run, coroutine))
Where 'action' is some I/O command object, and registerCallback()
will call its argument back with a value or exc_info, after the I/O is done.
Of course, a real framework integration might actually dispatch on
type here rather than using special command objects like this, and
there might be more glue code to deal with exceptions, but really,
the heart of the thing is just going to look like that. (I just
wrote it that way to show the basic structure.)
Really, it's just a few functions, maybe a utility routine or two,
and maybe a big if-then or dictionary dispatch on types if you just
want to be able to 'yield' existing I/O objects provided by the frameworks.
IOW, it's a *lot* simpler than actually rolling your own I/O or GUI
framework like Twisted or Eventlet or wxPython or tk or some other such thing.
>But it seems a bit of a waste to have two different trampolines,
>especially since the trampoline itself is so hard to understand
>(speaking for myself here :-). ISTM that the single combined
>trampoline is easier to understand than the From class.
Well, the PEP 342 example was made to look simple, because it doesn't
have to actually DO anything (like I/O!) To work for real, it'd need
some pluggability, and some things to help it interoperate with
different GUI and I/O frameworks and event loops. (Using your own
event loop "for real" isn't very useful in a lot of non-trivial applications.)
Heck, after writing From(), it gave me an idea that I could just
write a trampoline that *could* integrate with other event loops,
with an idea to have it be a general-purpose companion to From.
But, after several wasted hours, I realized that yes, it *could* be
written (I still have the draft), but it was mostly just something
that would save a little boilerplate in bolting From()'s onto an
existing async I/O framework, and not really anything to write home about.
So, I guess what I'm saying is, the benefit of separating the
trampoline from control flow, is that people can then use them with
their favorite event loop or framework, instead of the stdlib trying
to compete with the experts on a slower release cycle.
There's additional benefit here in that 1) getting pseudothread
implementations correct can be difficult, but once done, they're
extremely stable, and 2) having a blessed syntax for identifying
pseudothreaded calls and returns is a boon to competition in I/O frameworks.
So, instead of everybody having their own versions of From (and I've
written more than a couple in my time), there's One Obvious Way To Do
It. All that will differ between I/O libraries are the actual API
calls for I/O and scheduling and starting up pseudothreads, so there
won't be as big of a switching barrier between frameworks.
More information about the Python-Dev
mailing list