[Python-Dev] Re: anonymous blocks
Phillip J. Eby
pje at telecommunity.com
Thu Apr 28 00:38:53 CEST 2005
At 02:50 PM 4/27/05 -0700, Guido van Rossum wrote:
> > >I'm not sure what the relevance of including a stack trace would be,
> > >and why that feature would be necessary to call them coroutines.
> > Well, you need that feature in order to retain traceback information when
> > you're simulating threads with a stack of generators. Although you can't
> > return from a generator inside a nested generator, you can simulate this by
> > keeping a stack of generators and having a wrapper that passes control
> > between generators, such that:
> > def somegen():
> > result = yield othergen()
> > causes the wrapper to push othergen() on the generator stack and execute
> > it. If othergen() raises an error, the wrapper resumes somegen() and
> > passes in the error. If you can only specify the value but not the
> > traceback, you lose the information about where the error occurred in
> > othergen().
> > So, the feature is necessary for anything other than "simple" (i.e.
> > single-frame) coroutines, at least if you want to retain any possibility of
> > debugging. :)
>OK. I think you must be describing continuations there, because my
>brain just exploded. :-)
Probably my attempt at a *brief* explanation backfired. No, they're not
continuations or anything nearly that complicated. I'm "just" simulating
threads using generators that yield a nested generator when they need to do
something that might block waiting for I/O. The pseudothread object pushes
the yielded generator-iterator and resumes it. If that generator-iterator
raises an error, the pseudothread catches it, pops the previous
generator-iterator, and passes the error into it, traceback and all.
The net result is that as long as you use a "yield expression" for any
function/method call that might do blocking I/O, and those functions or
methods are written as generators, you get the benefits of Twisted (async
I/O without threading headaches) without having to "twist" your code into
the callback-registration patterns of Twisted. And, by passing in errors
with tracebacks, the normal process of exception call-stack unwinding
combined with pseudothread stack popping results in a traceback that looks
just as if you had called the functions or methods normally, rather than
via the pseudothreading mechanism. Without that, you would only get the
error context of 'async_readline()', because the traceback wouldn't be able
to show who *called* async_readline.
>In Python 3000 I want to make the traceback a standard attribute of
>Exception instances; would that suffice?
If you're planning to make 'raise' reraise it, such that 'raise exc' is
equivalent to 'raise type(exc), exc, exc.traceback'. Is that what you
mean? (i.e., just making it easier to pass the darn things around)
If so, then I could probably do what I need as long as there exist no error
types whose instances disallow setting a 'traceback' attribute on them
after the fact. Of course, if Exception provides a slot (or dictionary)
for this, then it shouldn't be a problem.
Of course, it seems to me that you also have the problem of adding to the
traceback when the same error is reraised...
All in all it seems more complex than just allowing an exception and a
traceback to be passed.
>I really don't want to pass
>the whole (type, value, traceback) triple that currently represents an
>exception through __next__().
The point of passing it in is so that the traceback can be preserved
without special action in the body of generators the exception is passing
I could be wrong, but it seems to me you need this even for PEP 340, if
you're going to support error management templates, and want tracebacks to
include the line in the block where the error originated. Just reraising
the error inside the generator doesn't seem like it would be enough.
> > >An alternative that solves this would be to give __next__() a second
> > >argument, which is a bool that should be true when the first argument
> > >is an exception that should be raised. What do people think?
> > I think it'd be simpler just to have two methods, conceptually
> > "resume(value=None)" and "error(value,tb=None)", whatever the actual method
> > names are.
>Part of me likes this suggestion, but part of me worries that it
>complicates the iterator API too much.
I was thinking that maybe these would be a "coroutine API" or "generator
API" instead. That is, something not usable except with
generator-iterators and with *new* objects written to conform to it. I
don't really see a lot of value in making template blocks work with
existing iterators. For that matter, I don't see a lot of value in
hand-writing new objects with resume/error, instead of just using a generator.
So, I guess I'm thinking you'd have something like tp_block_resume and
tp_block_error type slots, and generators' tp_iter_next would just be the
same as tp_block_resume(None).
But maybe this is the part you're thinking is complicated. :)
More information about the Python-Dev