Skip to content Skip to sidebar Skip to footer

Does __await__ Need To Be A Generator?

I want to implement an awaitable and noticed that __await__ 'needs' to be a generator. From PEP-492: An object with an __await__ method returning an iterator. ... Objects with __a

Solution 1:

In order to work in an await expression, __await__ does not need to be a generator. However, certain operations are only available if the result of __await__ support the generator interface.

Namely, it is not possible to send values or throw exceptions into an iterator-__await__. Only None can be "sent" to an iterator-__await__, as if generator.__next__ were used.


Let's consider a simple Awaitable that returns an iterator from its __await__.

classIter:
    """Basic iterator that yields the same value"""def__next__(self): return1def__iter__(self): return self

classIterAwait:
    """Awaitable that uses an iterator for __await__"""def__await__(self):
        return Iter()

We can check that they implement the desired interfaces:

>>>from collections.abc import Awaitable, Iterator, Generator>>>isinstance(IterAwait(), Awaitable)
True
>>>isinstance(IterAwait().__await__(), Iterator)
True
>>>isinstance(IterAwait().__await__(), Generator)
False

In order to see how this interacts with await, we wrap it in a coroutine:

async def iter_await():
    awaitIterAwait()

Every operation we perform on iter_await with the full coroutine/generator interface is forwarded by await to our iterator-__await__. This allows to study how the iterator-__await__ receives signals:

>>> test_iter = iter_await()
>>> test_iter.send(3)         # 0. does it appear like a coroutine?
TypeError: can`t send non-None value to a just-started coroutine
>>> test_iter.send(None)      # 1. must initialise just-started coroutine1>>> test_iter.send(None)      # 2. repeatedly use the underlying iterator1>>> next(test_iter)           # 3. do we expose the iterator?
TypeError: 'coroutine'objectisnot an iterator
>>> test_iter.send(3)         # 4. can we send non-None values?
AttributeError: 'Iter'object has no attribute 'send'>>> test_iter = iter_await()  # we just broke the coroutine...>>> test_iter.send(None)      # ...need a new one1>>> test_iter.throw(KeyError) # 4. can we throw Exceptions?
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in iter_await
KeyError

As can be seen, await can handle an iterator-__await__, but does not forward all operations. However, some are translated and some are handled early.

  • It is always possible to .send(None), which is translated to a bare __next__(). (1, 2)
  • The coroutine does not magically expose .__next__ (3) and cannot translate .send with a value either (4).
  • It is possible to .throw an exception, but await handles it early in the coroutine.

Note that await uses the throw and send method as available. If the result of __await__ implements send but not throw or vice versa, the functionality present is used. Only __next__ is mandatory.

Solution 2:

So it appears asyncio.sleep doesn't have the __await__ method

True, but it doesn't have to have one to be awaitable. The documentation says that __await__, if present, needs to return an iterator, not that await will only work on objects that define __await__. In fact, it explicitly documents that the argument to await can be one of:

  • A native coroutine object returned from a native coroutine function.

  • A generator-based coroutine object returned from a function decorated with types.coroutine().

  • An object with an __await__ method returning an iterator.

  • An object defined in C providing the Python/C equivalent of the __await__ special method.

So now I'm wondering, does the __await__ method really need to be a generator using the yield from syntax?

If you actually have an __await__ method, it does need to return an iterator.

Post a Comment for "Does __await__ Need To Be A Generator?"