python-trio / async_generator

Making it easy to write async iterators in Python 3.5
Other
95 stars 23 forks source link

Questions #1

Closed bj0 closed 8 years ago

bj0 commented 8 years ago

I saw your pycon lightning talk on youtube and thought it was pretty neat. await yield for async for has been something I've wanted since I first read about async for. I have made decorators that try to take care of all the __aiter__/__anext__ boilerplate code in the past, but I have never seen one that used a yield_ function like yours (I have always used queues to pass values). It's a pretty clever idea.

As I was reading through your code and testing differences between your implementation and mine, I had a few questions:

It looks like you are using it as a sentinal value to determine when you've reached the end of the coroutine chain, but why not just yield the value directly and then in send() you can use inspect.isawaitable(result) to determine if you're at the end. I don't know if you would need inspect.isgenerator or inspect.iscoroutine, but for all the tests I've done, inspect.isawaitable(result) has been sufficient.

This would have the added benefit of allowing the decorator to turn any generator into an async iterable (not just ones using yield_).

It seems like unnecessary overhead to create a new instance for each __anext__ call, why not just create an instance in __aiter__ (which is called once per async for) and give it a __anext__ that returns self? This also separates them more into an "async iterable" and "async iterator".

I read your README, and I agree that an asend or athrow seem pretty weird, but I don't understand the problem with yield_from_. The most obvious and useful case I can think of is generator delegation, and as far as I can tell your (commented out) version does that just fine.

For reference, here's the version I was using (updated to adopt your yield_ ideas and be more flexible): https://gist.github.com/bj0/0313ee67766de6a04881

njsmith commented 8 years ago

Hi @bj0! Thanks for checking out my little library :-)

Why YieldWrapper?

(1) It's totally legal for an async for to return awaitables. (2) While it happens to be true that asyncio always uses awaitable objects to communicate between the coroutine runner and the leaf coroutines, this isn't mandatory -- other coroutine runners (tornado, curio, ...) can and do use non-awaitable objects for communication.

Defining my own sentinel value neatly avoids all these issues.

Why a new ANextIter for every __anext__ call?

You're right, that probably would be a little more efficient :-).

Why comment out yield_from_?

You're right, the commented out code does work fine. I commented it out at the request of @1st1 (= Yury Selivanov = the author of the async/await PEP), because we're talking about how to add a real async yield feature to Python 3.6, and while it's easy to implement async yield from in this library, it's much more complicated to implement inside the compiler/bytecode interpreter. Also, if we have to pick between asend/athrow and async yield from, then it's not 100% clear which alternative is better (personally I actually have more of an immediate use case for athrow than I do for async yield from...). And it's easier to add features than to remove them...

bj0 commented 8 years ago

Ahh OK I get it. I have no experience with the other coroutine runners, I was just trying to get it to also wrap normal generators, which isn't a big deal. I guess you could just wrap that generator in your own generator that wraps the result in your sentinel value.

njsmith commented 8 years ago

Removed the unnecessary ANextIter instantiations in 0e492ddfe544fc8fd512c458527cfde2efaac3a1, so closing this.

And yeah, in general you have to match async for <-> async generator, for <-> regular generator, just like you have to match await foo() <-> async function, foo() <-> regular function. But if you want to adapt a synchronous thing to an asynchronous thing then it's pretty trivial in both cases:

async def async_foo(*args, **kwargs):
    return foo(*args, **kwargs)

@async_generator
async def async_my_gen(*args, **kwargs):
    for obj in my_gen(*args, **kwargs):
        await yield_(obj)

Thanks again for checking it out!