|
| 1 | +.. _guide_iteration: |
| 2 | + |
| 3 | +================ |
| 4 | +Iterator Scoping |
| 5 | +================ |
| 6 | + |
| 7 | +Cleanup of ``async`` resources is special in that it may require an active event loop. |
| 8 | +Since :term:`asynchronous iterators <python:asynchronous iterator>` can hold resources |
| 9 | +indefinitely, they should be cleaned up deterministically whenever possible |
| 10 | +(see `PEP 533`_ for discussion). |
| 11 | +Thus, ``asyncstdlib`` defaults to deterministic cleanup but provides tools to explicitly |
| 12 | +manage the lifetime of iterators. |
| 13 | + |
| 14 | +Cleanup in ``asyncstdlib`` |
| 15 | +========================== |
| 16 | + |
| 17 | +All async iterators of :py:mod:`asyncstdlib` that work on other iterators |
| 18 | +assume sole ownership of the iterators passed to them. |
| 19 | +Passed in async iterators are guaranteed to be :py:meth:`~agen.aclose`\ d |
| 20 | +as soon as the :py:mod:`asyncstdlib` async iterator itself is cleaned up. |
| 21 | +This provides a resource-safe default for the most common operation of |
| 22 | +exhausting iterators. |
| 23 | + |
| 24 | +.. code-block:: python3 |
| 25 | +
|
| 26 | + >>> import asyncio |
| 27 | + >>> import asyncstdlib as a |
| 28 | + >>> |
| 29 | + >>> async def async_squares(i=0): |
| 30 | + ... """Provide an infinite stream of squared numbers""" |
| 31 | + ... while True: |
| 32 | + ... await asyncio.sleep(0.1) |
| 33 | + ... yield i**2 |
| 34 | + ... i += 1 |
| 35 | + ... |
| 36 | + >>> async def main(): |
| 37 | + ... async_iter = async_squares() |
| 38 | + ... # loop until we are done |
| 39 | + ... async for i, s in a.zip(range(5), async_iter): |
| 40 | + ... print(f"{i}: {s}") |
| 41 | + ... assert await a.anext(async_iter, "Closed!") == "Closed!" |
| 42 | + ... |
| 43 | + >>> asyncio.run(main()) |
| 44 | +
|
| 45 | +For consistency, every :py:mod:`asyncstdlib` async iterator performs such cleanup. |
| 46 | +This may be unexpected for async variants of iterator utilities that are usually |
| 47 | +applied multiple times, such as :py:func:`itertools.islice`. |
| 48 | +Thus, to manage the lifetime of async iterators one can explicitly scope them. |
| 49 | + |
| 50 | +Scoping async iterator lifetime |
| 51 | +=============================== |
| 52 | + |
| 53 | +In order to use a single async iterator across several iterations but guarantee cleanup, |
| 54 | +the iterator can be scoped to an ``async with`` block: |
| 55 | +using :py:func:`asyncstdlib.scoped_iter` creates an async iterator that is guaranteed |
| 56 | +to :py:meth:`~agen.aclose` at the end of the block, but cannot be closed before. |
| 57 | + |
| 58 | +.. code-block:: python3 |
| 59 | +
|
| 60 | + >>> import asyncio |
| 61 | + >>> import asyncstdlib as a |
| 62 | + >>> |
| 63 | + >>> async def async_squares(i=0): |
| 64 | + ... """Provide an infinite stream of squared numbers""" |
| 65 | + ... while True: |
| 66 | + ... await asyncio.sleep(0.1) |
| 67 | + ... yield i**2 |
| 68 | + ... i += 1 |
| 69 | + ... |
| 70 | + >>> async def main(): |
| 71 | + ... # iterator can be re-used in the async with block |
| 72 | + ... async with a.scoped_iter(async_squares()) as async_iter: |
| 73 | + ... async for s in a.islice(async_iter, 3): |
| 74 | + ... print(f"1st Batch: {s}") |
| 75 | + ... # async_iter is still open for further iteration |
| 76 | + ... async for s in a.islice(async_iter, 3): |
| 77 | + ... print(f"2nd Batch: {s}") |
| 78 | + ... async for s in a.islice(async_iter, 3): |
| 79 | + ... print(f"3rd Batch: {s}") |
| 80 | + ... # iterator is closed after the async with block |
| 81 | + ... assert await a.anext(async_iter, "Closed!") == "Closed!" |
| 82 | + ... |
| 83 | + >>> asyncio.run(main()) |
| 84 | +
|
| 85 | +Scoped iterators should be the go-to approach for managing iterator lifetimes. |
| 86 | +However, not all lifetimes correspond to well-defined lexical scopes; |
| 87 | +for these cases, one can :term:`borrow <borrowing>` an iterator instead. |
| 88 | + |
| 89 | +.. _PEP 533: https://www.python.org/dev/peps/pep-0533/ |
0 commit comments