<div dir="ltr">This is a very interesting proposal.  I just wanted to share something I found in my quick search:<div><br></div><div>http://stackoverflow.com/questions/14797930/python-custom-iterator-close-a-file-on-stopiteration</div><div><br></div><div>Could you explain why the accepted answer there doesn't address this issue?</div><div><br></div><div><pre class="lang-py prettyprint prettyprinted" style="padding: 5px; width: auto; max-height: 600px; overflow: auto; font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, sans-serif; background-color: rgb(239, 240, 241); color: rgb(57, 51, 24); word-wrap: normal;"><code style="font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, sans-serif; white-space: inherit;"><span class="kwd" style="color: rgb(16, 16, 148);">class</span><span class="pln" style="color: rgb(48, 51, 54);"> </span><span class="typ" style="color: rgb(43, 145, 175);">Parse</span><span class="pun" style="color: rgb(48, 51, 54);">(</span><span class="pln" style="color: rgb(48, 51, 54);">object</span><span class="pun" style="color: rgb(48, 51, 54);">):</span><span class="pln" style="color: rgb(48, 51, 54);">
    </span><span class="str" style="color: rgb(125, 39, 39);">"""A generator that iterates through a file"""</span><span class="pln" style="color: rgb(48, 51, 54);">
    </span><span class="kwd" style="color: rgb(16, 16, 148);">def</span><span class="pln" style="color: rgb(48, 51, 54);"> __init__</span><span class="pun" style="color: rgb(48, 51, 54);">(</span><span class="pln" style="color: rgb(48, 51, 54);">self</span><span class="pun" style="color: rgb(48, 51, 54);">,</span><span class="pln" style="color: rgb(48, 51, 54);"> path</span><span class="pun" style="color: rgb(48, 51, 54);">):</span><span class="pln" style="color: rgb(48, 51, 54);">
        self</span><span class="pun" style="color: rgb(48, 51, 54);">.</span><span class="pln" style="color: rgb(48, 51, 54);">path </span><span class="pun" style="color: rgb(48, 51, 54);">=</span><span class="pln" style="color: rgb(48, 51, 54);"> path</span></code></pre></div><div><pre class="lang-py prettyprint prettyprinted" style="padding: 5px; width: auto; max-height: 600px; overflow: auto; font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, sans-serif; background-color: rgb(239, 240, 241); word-wrap: normal;"><code style="font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, sans-serif; white-space: inherit;"><span class="pln" style="color: rgb(48, 51, 54);">  </span><span class="kwd" style="color: rgb(16, 16, 148);">def</span><span class="pln" style="color: rgb(48, 51, 54);"> __iter__</span><span class="pun" style="color: rgb(48, 51, 54);">(</span><span class="pln" style="color: rgb(48, 51, 54);">self</span><span class="pun" style="color: rgb(48, 51, 54);">):</span><span class="pln" style="color: rgb(48, 51, 54);">
        </span><span class="kwd" style="color: rgb(16, 16, 148);">with</span><span class="pln" style="color: rgb(48, 51, 54);"> open</span><span class="pun" style="color: rgb(48, 51, 54);">(</span><span class="pln" style="color: rgb(48, 51, 54);">self</span><span class="pun" style="color: rgb(48, 51, 54);">.</span><span class="pln" style="color: rgb(48, 51, 54);">path</span><span class="pun" style="color: rgb(48, 51, 54);">)</span><span class="pln" style="color: rgb(48, 51, 54);"> </span><span class="kwd" style="color: rgb(16, 16, 148);">as</span><span class="pln" style="color: rgb(48, 51, 54);"> f</span><span class="pun" style="color: rgb(48, 51, 54);">:</span><span class="pln" style="color: rgb(48, 51, 54);">
            </span><span class="pln"><font color="#101094">yield from f</font></span></code></pre></div><div><br></div><div>Best,</div><div><br></div><div>Neil<br><br>On Wednesday, October 19, 2016 at 12:39:34 AM UTC-4, Nathaniel Smith wrote:<blockquote class="gmail_quote" style="margin: 0;margin-left: 0.8ex;border-left: 1px #ccc solid;padding-left: 1ex;">Hi all,
<br>
<br>I'd like to propose that Python's iterator protocol be enhanced to add
<br>a first-class notion of completion / cleanup.
<br>
<br>This is mostly motivated by thinking about the issues around async
<br>generators and cleanup. Unfortunately even though PEP 525 was accepted
<br>I found myself unable to stop pondering this, and the more I've
<br>pondered the more convinced I've become that the GC hooks added in PEP
<br>525 are really not enough, and that we'll regret it if we stick with
<br>them, or at least with them alone :-/. The strategy here is pretty
<br>different -- it's an attempt to dig down and make a fundamental
<br>improvement to the language that fixes a number of long-standing rough
<br>spots, including async generators.
<br>
<br>The basic concept is relatively simple: just adding a '__iterclose__'
<br>method that 'for' loops call upon completion, even if that's via break
<br>or exception. But, the overall issue is fairly complicated + iterators
<br>have a large surface area across the language, so the text below is
<br>pretty long. Mostly I wrote it all out to convince myself that there
<br>wasn't some weird showstopper lurking somewhere :-). For a first pass
<br>discussion, it probably makes sense to mainly focus on whether the
<br>basic concept makes sense? The main rationale is at the top, but the
<br>details are there too for those who want them.
<br>
<br>Also, for *right* now I'm hoping -- probably unreasonably -- to try to
<br>get the async iterator parts of the proposal in ASAP, ideally for
<br>3.6.0 or 3.6.1. (I know this is about the worst timing for a proposal
<br>like this, which I apologize for -- though async generators are
<br>provisional in 3.6, so at least in theory changing them is not out of
<br>the question.) So again, it might make sense to focus especially on
<br>the async parts, which are a pretty small and self-contained part, and
<br>treat the rest of the proposal as a longer-term plan provided for
<br>context. The comparison to PEP 525 GC hooks comes right after the
<br>initial rationale.
<br>
<br>Anyway, I'll be interested to hear what you think!
<br>
<br>-n
<br>
<br>------------------
<br>
<br>Abstract
<br>========
<br>
<br>We propose to extend the iterator protocol with a new
<br>``__(a)iterclose__`` slot, which is called automatically on exit from
<br>``(async) for`` loops, regardless of how they exit. This allows for
<br>convenient, deterministic cleanup of resources held by iterators
<br>without reliance on the garbage collector. This is especially valuable
<br>for asynchronous generators.
<br>
<br>
<br>Note on timing
<br>==============
<br>
<br>In practical terms, the proposal here is divided into two separate
<br>parts: the handling of async iterators, which should ideally be
<br>implemented ASAP, and the handling of regular iterators, which is a
<br>larger but more relaxed project that can't start until 3.7 at the
<br>earliest. But since the changes are closely related, and we probably
<br>don't want to end up with async iterators and regular iterators
<br>diverging in the long run, it seems useful to look at them together.
<br>
<br>
<br>Background and motivation
<br>=========================
<br>
<br>Python iterables often hold resources which require cleanup. For
<br>example: ``file`` objects need to be closed; the `WSGI spec
<br><<a href="https://www.python.org/dev/peps/pep-0333/" target="_blank" rel="nofollow" onmousedown="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0333%2F\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNGGiCWfEybdg1k2SG7_DjCKAQlZ9g';return true;" onclick="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0333%2F\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNGGiCWfEybdg1k2SG7_DjCKAQlZ9g';return true;">https://www.python.org/dev/<wbr>peps/pep-0333/</a>>`_ adds a ``close`` method
<br>on top of the regular iterator protocol and demands that consumers
<br>call it at the appropriate time (though forgetting to do so is a
<br>`frequent source of bugs
<br><<a href="http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html" target="_blank" rel="nofollow" onmousedown="this.href='http://www.google.com/url?q\x3dhttp%3A%2F%2Fblog.dscpl.com.au%2F2012%2F10%2Fobligations-for-calling-close-on.html\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNEaCZ8kUvs4smzKqeHTtBQ-86SMxg';return true;" onclick="this.href='http://www.google.com/url?q\x3dhttp%3A%2F%2Fblog.dscpl.com.au%2F2012%2F10%2Fobligations-for-calling-close-on.html\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNEaCZ8kUvs4smzKqeHTtBQ-86SMxg';return true;">http://blog.dscpl.com.au/<wbr>2012/10/obligations-for-<wbr>calling-close-on.html</a>>`_);
<br>and PEP 342 (based on PEP 325) extended generator objects to add a
<br>``close`` method to allow generators to clean up after themselves.
<br>
<br>Generally, objects that need to clean up after themselves also define
<br>a ``__del__`` method to ensure that this cleanup will happen
<br>eventually, when the object is garbage collected. However, relying on
<br>the garbage collector for cleanup like this causes serious problems in
<br>at least two cases:
<br>
<br>- In Python implementations that do not use reference counting (e.g.
<br>PyPy, Jython), calls to ``__del__`` may be arbitrarily delayed -- yet
<br>many situations require *prompt* cleanup of resources. Delayed cleanup
<br>produces problems like crashes due to file descriptor exhaustion, or
<br>WSGI timing middleware that collects bogus times.
<br>
<br>- Async generators (PEP 525) can only perform cleanup under the
<br>supervision of the appropriate coroutine runner. ``__del__`` doesn't
<br>have access to the coroutine runner; indeed, the coroutine runner
<br>might be garbage collected before the generator object. So relying on
<br>the garbage collector is effectively impossible without some kind of
<br>language extension. (PEP 525 does provide such an extension, but it
<br>has a number of limitations that this proposal fixes; see the
<br>"alternatives" section below for discussion.)
<br>
<br>Fortunately, Python provides a standard tool for doing resource
<br>cleanup in a more structured way: ``with`` blocks. For example, this
<br>code opens a file but relies on the garbage collector to close it::
<br>
<br>  def read_newline_separated_json(<wbr>path):
<br>      for line in open(path):
<br>          yield json.loads(line)
<br>
<br>  for document in read_newline_separated_json(<wbr>path):
<br>      ...
<br>
<br>and recent versions of CPython will point this out by issuing a
<br>``ResourceWarning``, nudging us to fix it by adding a ``with`` block::
<br>
<br>  def read_newline_separated_json(<wbr>path):
<br>      with open(path) as file_handle:      # <-- with block
<br>          for line in file_handle:
<br>              yield json.loads(line)
<br>
<br>  for document in read_newline_separated_json(<wbr>path):  # <-- outer for loop
<br>      ...
<br>
<br>But there's a subtlety here, caused by the interaction of ``with``
<br>blocks and generators. ``with`` blocks are Python's main tool for
<br>managing cleanup, and they're a powerful one, because they pin the
<br>lifetime of a resource to the lifetime of a stack frame. But this
<br>assumes that someone will take care of cleaning up the stack frame...
<br>and for generators, this requires that someone ``close`` them.
<br>
<br>In this case, adding the ``with`` block *is* enough to shut up the
<br>``ResourceWarning``, but this is misleading -- the file object cleanup
<br>here is still dependent on the garbage collector. The ``with`` block
<br>will only be unwound when the ``read_newline_separated_json`<wbr>`
<br>generator is closed. If the outer ``for`` loop runs to completion then
<br>the cleanup will happen immediately; but if this loop is terminated
<br>early by a ``break`` or an exception, then the ``with`` block won't
<br>fire until the generator object is garbage collected.
<br>
<br>The correct solution requires that all *users* of this API wrap every
<br>``for`` loop in its own ``with`` block::
<br>
<br>  with closing(read_newline_<wbr>separated_json(path)) as genobj:
<br>      for document in genobj:
<br>          ...
<br>
<br>This gets even worse if we consider the idiom of decomposing a complex
<br>pipeline into multiple nested generators::
<br>
<br>  def read_users(path):
<br>      with closing(read_newline_<wbr>separated_json(path)) as gen:
<br>          for document in gen:
<br>              yield User.from_json(document)
<br>
<br>  def users_in_group(path, group):
<br>      with closing(read_users(path)) as gen:
<br>          for user in gen:
<br>              if user.group == group:
<br>                  yield user
<br>
<br>In general if you have N nested generators then you need N+1 ``with``
<br>blocks to clean up 1 file. And good defensive programming would
<br>suggest that any time we use a generator, we should assume the
<br>possibility that there could be at least one ``with`` block somewhere
<br>in its (potentially transitive) call stack, either now or in the
<br>future, and thus always wrap it in a ``with``. But in practice,
<br>basically nobody does this, because programmers would rather write
<br>buggy code than tiresome repetitive code. In simple cases like this
<br>there are some workarounds that good Python developers know (e.g. in
<br>this simple case it would be idiomatic to pass in a file handle
<br>instead of a path and move the resource management to the top level),
<br>but in general we cannot avoid the use of ``with``/``finally`` inside
<br>of generators, and thus dealing with this problem one way or another.
<br>When beauty and correctness fight then beauty tends to win, so it's
<br>important to make correct code beautiful.
<br>
<br>Still, is this worth fixing? Until async generators came along I would
<br>have argued yes, but that it was a low priority, since everyone seems
<br>to be muddling along okay -- but async generators make it much more
<br>urgent. Async generators cannot do cleanup *at all* without some
<br>mechanism for deterministic cleanup that people will actually use, and
<br>async generators are particularly likely to hold resources like file
<br>descriptors. (After all, if they weren't doing I/O, they'd be
<br>generators, not async generators.) So we have to do something, and it
<br>might as well be a comprehensive fix to the underlying problem. And
<br>it's much easier to fix this now when async generators are first
<br>rolling out, then it will be to fix it later.
<br>
<br>The proposal itself is simple in concept: add a ``__(a)iterclose__``
<br>method to the iterator protocol, and have (async) ``for`` loops call
<br>it when the loop is exited, even if this occurs via ``break`` or
<br>exception unwinding. Effectively, we're taking the current cumbersome
<br>idiom (``with`` block + ``for`` loop) and merging them together into a
<br>fancier ``for``. This may seem non-orthogonal, but makes sense when
<br>you consider that the existence of generators means that ``with``
<br>blocks actually depend on iterator cleanup to work reliably, plus
<br>experience showing that iterator cleanup is often a desireable feature
<br>in its own right.
<br>
<br>
<br>Alternatives
<br>============
<br>
<br>PEP 525 asyncgen hooks
<br>----------------------
<br>
<br>PEP 525 proposes a `set of global thread-local hooks managed by new
<br>``sys.{get/set}_asyncgen_<wbr>hooks()`` functions
<br><<a href="https://www.python.org/dev/peps/pep-0525/#finalization" target="_blank" rel="nofollow" onmousedown="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0525%2F%23finalization\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNEeIw2DswUFXTMA6rdQMvqvDXzq3Q';return true;" onclick="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fwww.python.org%2Fdev%2Fpeps%2Fpep-0525%2F%23finalization\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNEeIw2DswUFXTMA6rdQMvqvDXzq3Q';return true;">https://www.python.org/dev/<wbr>peps/pep-0525/#finalization</a>>`_<wbr>, which
<br>allow event loops to integrate with the garbage collector to run
<br>cleanup for async generators. In principle, this proposal and PEP 525
<br>are complementary, in the same way that ``with`` blocks and
<br>``__del__`` are complementary: this proposal takes care of ensuring
<br>deterministic cleanup in most cases, while PEP 525's GC hooks clean up
<br>anything that gets missed. But ``__aiterclose__`` provides a number of
<br>advantages over GC hooks alone:
<br>
<br>- The GC hook semantics aren't part of the abstract async iterator
<br>protocol, but are instead restricted `specifically to the async
<br>generator concrete type <XX find and link Yury's email saying this>`_.
<br>If you have an async iterator implemented using a class, like::
<br>
<br>    class MyAsyncIterator:
<br>        async def __anext__():
<br>            ...
<br>
<br>  then you can't refactor this into an async generator without
<br>changing its semantics, and vice-versa. This seems very unpythonic.
<br>(It also leaves open the question of what exactly class-based async
<br>iterators are supposed to do, given that they face exactly the same
<br>cleanup problems as async generators.) ``__aiterclose__``, on the
<br>other hand, is defined at the protocol level, so it's duck-type
<br>friendly and works for all iterators, not just generators.
<br>
<br>- Code that wants to work on non-CPython implementations like PyPy
<br>cannot in general rely on GC for cleanup. Without ``__aiterclose__``,
<br>it's more or less guaranteed that developers who develop and test on
<br>CPython will produce libraries that leak resources when used on PyPy.
<br>Developers who do want to target alternative implementations will
<br>either have to take the defensive approach of wrapping every ``for``
<br>loop in a ``with`` block, or else carefully audit their code to figure
<br>out which generators might possibly contain cleanup code and add
<br>``with`` blocks around those only. With ``__aiterclose__``, writing
<br>portable code becomes easy and natural.
<br>
<br>- An important part of building robust software is making sure that
<br>exceptions always propagate correctly without being lost. One of the
<br>most exciting things about async/await compared to traditional
<br>callback-based systems is that instead of requiring manual chaining,
<br>the runtime can now do the heavy lifting of propagating errors, making
<br>it *much* easier to write robust code. But, this beautiful new picture
<br>has one major gap: if we rely on the GC for generator cleanup, then
<br>exceptions raised during cleanup are lost. So, again, with
<br>``__aiterclose__``, developers who care about this kind of robustness
<br>will either have to take the defensive approach of wrapping every
<br>``for`` loop in a ``with`` block, or else carefully audit their code
<br>to figure out which generators might possibly contain cleanup code.
<br>``__aiterclose__`` plugs this hole by performing cleanup in the
<br>caller's context, so writing more robust code becomes the path of
<br>least resistance.
<br>
<br>- The WSGI experience suggests that there exist important
<br>iterator-based APIs that need prompt cleanup and cannot rely on the
<br>GC, even in CPython. For example, consider a hypothetical WSGI-like
<br>API based around async/await and async iterators, where a response
<br>handler is an async generator that takes request headers + an async
<br>iterator over the request body, and yields response headers + the
<br>response body. (This is actually the use case that got me interested
<br>in async generators in the first place, i.e. this isn't hypothetical.)
<br>If we follow WSGI in requiring that child iterators must be closed
<br>properly, then without ``__aiterclose__`` the absolute most
<br>minimalistic middleware in our system looks something like::
<br>
<br>    async def noop_middleware(handler, request_header, request_body):
<br>        async with aclosing(handler(request_body, request_body)) as aiter:
<br>            async for response_item in aiter:
<br>                yield response_item
<br>
<br>  Arguably in regular code one can get away with skipping the ``with``
<br>block around ``for`` loops, depending on how confident one is that one
<br>understands the internal implementation of the generator. But here we
<br>have to cope with arbitrary response handlers, so without
<br>``__aiterclose__``, this ``with`` construction is a mandatory part of
<br>every middleware.
<br>
<br>  ``__aiterclose__`` allows us to eliminate the mandatory boilerplate
<br>and an extra level of indentation from every middleware::
<br>
<br>    async def noop_middleware(handler, request_header, request_body):
<br>        async for response_item in handler(request_header, request_body):
<br>            yield response_item
<br>
<br>So the ``__aiterclose__`` approach provides substantial advantages
<br>over GC hooks.
<br>
<br>This leaves open the question of whether we want a combination of GC
<br>hooks + ``__aiterclose__``, or just ``__aiterclose__`` alone. Since
<br>the vast majority of generators are iterated over using a ``for`` loop
<br>or equivalent, ``__aiterclose__`` handles most situations before the
<br>GC has a chance to get involved. The case where GC hooks provide
<br>additional value is in code that does manual iteration, e.g.::
<br>
<br>    agen = fetch_newline_separated_json_<wbr>from_url(...)
<br>    while True:
<br>        document = await type(agen).__anext__(agen)
<br>        if document["id"] == needle:
<br>            break
<br>    # doesn't do 'await agen.aclose()'
<br>
<br>If we go with the GC-hooks + ``__aiterclose__`` approach, this
<br>generator will eventually be cleaned up by GC calling the generator
<br>``__del__`` method, which then will use the hooks to call back into
<br>the event loop to run the cleanup code.
<br>
<br>If we go with the no-GC-hooks approach, this generator will eventually
<br>be garbage collected, with the following effects:
<br>
<br>- its ``__del__`` method will issue a warning that the generator was
<br>not closed (similar to the existing "coroutine never awaited"
<br>warning).
<br>
<br>- The underlying resources involved will still be cleaned up, because
<br>the generator frame will still be garbage collected, causing it to
<br>drop references to any file handles or sockets it holds, and then
<br>those objects's ``__del__`` methods will release the actual operating
<br>system resources.
<br>
<br>- But, any cleanup code inside the generator itself (e.g. logging,
<br>buffer flushing) will not get a chance to run.
<br>
<br>The solution here -- as the warning would indicate -- is to fix the
<br>code so that it calls ``__aiterclose__``, e.g. by using a ``with``
<br>block::
<br>
<br>    async with aclosing(fetch_newline_<wbr>separated_json_from_url(...)) as agen:
<br>        while True:
<br>            document = await type(agen).__anext__(agen)
<br>            if document["id"] == needle:
<br>                break
<br>
<br>Basically in this approach, the rule would be that if you want to
<br>manually implement the iterator protocol, then it's your
<br>responsibility to implement all of it, and that now includes
<br>``__(a)iterclose__``.
<br>
<br>GC hooks add non-trivial complexity in the form of (a) new global
<br>interpreter state, (b) a somewhat complicated control flow (e.g.,
<br>async generator GC always involves resurrection, so the details of PEP
<br>442 are important), and (c) a new public API in asyncio (``await
<br>loop.shutdown_asyncgens()``) that users have to remember to call at
<br>the appropriate time. (This last point in particular somewhat
<br>undermines the argument that GC hooks provide a safe backup to
<br>guarantee cleanup, since if ``shutdown_asyncgens()`` isn't called
<br>correctly then I *think* it's possible for generators to be silently
<br>discarded without their cleanup code being called; compare this to the
<br>``__aiterclose__``-only approach where in the worst case we still at
<br>least get a warning printed. This might be fixable.) All this
<br>considered, GC hooks arguably aren't worth it, given that the only
<br>people they help are those who want to manually call ``__anext__`` yet
<br>don't want to manually call ``__aiterclose__``. But Yury disagrees
<br>with me on this :-). And both options are viable.
<br>
<br>
<br>Always inject resources, and do all cleanup at the top level
<br>------------------------------<wbr>------------------------------
<br>
<br>It was suggested on python-dev (XX find link) that a pattern to avoid
<br>these problems is to always pass resources in from above, e.g.
<br>``read_newline_separated_json`<wbr>` should take a file object rather than
<br>a path, with cleanup handled at the top level::
<br>
<br>  def read_newline_separated_json(<wbr>file_handle):
<br>      for line in file_handle:
<br>          yield json.loads(line)
<br>
<br>  def read_users(file_handle):
<br>      for document in read_newline_separated_json(<wbr>file_handle):
<br>          yield User.from_json(document)
<br>
<br>  with open(path) as file_handle:
<br>      for user in read_users(file_handle):
<br>          ...
<br>
<br>This works well in simple cases; here it lets us avoid the "N+1
<br>``with`` blocks problem". But unfortunately, it breaks down quickly
<br>when things get more complex. Consider if instead of reading from a
<br>file, our generator was reading from a streaming HTTP GET request --
<br>while handling redirects and authentication via OAUTH. Then we'd
<br>really want the sockets to be managed down inside our HTTP client
<br>library, not at the top level. Plus there are other cases where
<br>``finally`` blocks embedded inside generators are important in their
<br>own right: db transaction management, emitting logging information
<br>during cleanup (one of the major motivating use cases for WSGI
<br>``close``), and so forth. So this is really a workaround for simple
<br>cases, not a general solution.
<br>
<br>
<br>More complex variants of __(a)iterclose__
<br>------------------------------<wbr>-----------
<br>
<br>The semantics of ``__(a)iterclose__`` are somewhat inspired by
<br>``with`` blocks, but context managers are more powerful:
<br>``__(a)exit__`` can distinguish between a normal exit versus exception
<br>unwinding, and in the case of an exception it can examine the
<br>exception details and optionally suppress propagation.
<br>``__(a)iterclose__`` as proposed here does not have these powers, but
<br>one can imagine an alternative design where it did.
<br>
<br>However, this seems like unwarranted complexity: experience suggests
<br>that it's common for iterables to have ``close`` methods, and even to
<br>have ``__exit__`` methods that call ``self.close()``, but I'm not
<br>aware of any common cases that make use of ``__exit__``'s full power.
<br>I also can't think of any examples where this would be useful. And it
<br>seems unnecessarily confusing to allow iterators to affect flow
<br>control by swallowing exceptions -- if you're in a situation where you
<br>really want that, then you should probably use a real ``with`` block
<br>anyway.
<br>
<br>
<br>Specification
<br>=============
<br>
<br>This section describes where we want to eventually end up, though
<br>there are some backwards compatibility issues that mean we can't jump
<br>directly here. A later section describes the transition plan.
<br>
<br>
<br>Guiding principles
<br>------------------
<br>
<br>Generally, ``__(a)iterclose__`` implementations should:
<br>
<br>- be idempotent,
<br>- perform any cleanup that is appropriate on the assumption that the
<br>iterator will not be used again after ``__(a)iterclose__`` is called.
<br>In particular, once ``__(a)iterclose__`` has been called then calling
<br>``__(a)next__`` produces undefined behavior.
<br>
<br>And generally, any code which starts iterating through an iterable
<br>with the intention of exhausting it, should arrange to make sure that
<br>``__(a)iterclose__`` is eventually called, whether or not the iterator
<br>is actually exhausted.
<br>
<br>
<br>Changes to iteration
<br>--------------------
<br>
<br>The core proposal is the change in behavior of ``for`` loops. Given
<br>this Python code::
<br>
<br>  for VAR in ITERABLE:
<br>      LOOP-BODY
<br>  else:
<br>      ELSE-BODY
<br>
<br>we desugar to the equivalent of::
<br>
<br>  _iter = iter(ITERABLE)
<br>  _iterclose = getattr(type(_iter), "__iterclose__", lambda: None)
<br>  try:
<br>      traditional-for VAR in _iter:
<br>          LOOP-BODY
<br>      else:
<br>          ELSE-BODY
<br>  finally:
<br>      _iterclose(_iter)
<br>
<br>where the "traditional-for statement" here is meant as a shorthand for
<br>the classic 3.5-and-earlier ``for`` loop semantics.
<br>
<br>Besides the top-level ``for`` statement, Python also contains several
<br>other places where iterators are consumed. For consistency, these
<br>should call ``__iterclose__`` as well using semantics equivalent to
<br>the above. This includes:
<br>
<br>- ``for`` loops inside comprehensions
<br>- ``*`` unpacking
<br>- functions which accept and fully consume iterables, like
<br>``list(it)``, ``tuple(it)``, ``itertools.product(it1, it2, ...)``, and
<br>others.
<br>
<br>
<br>Changes to async iteration
<br>--------------------------
<br>
<br>We also make the analogous changes to async iteration constructs,
<br>except that the new slot is called ``__aiterclose__``, and it's an
<br>async method that gets ``await``\ed.
<br>
<br>
<br>Modifications to basic iterator types
<br>------------------------------<wbr>-------
<br>
<br>Generator objects (including those created by generator comprehensions):
<br>- ``__iterclose__`` calls ``self.close()``
<br>- ``__del__`` calls ``self.close()`` (same as now), and additionally
<br>issues a ``ResourceWarning`` if the generator wasn't exhausted. This
<br>warning is hidden by default, but can be enabled for those who want to
<br>make sure they aren't inadverdantly relying on CPython-specific GC
<br>semantics.
<br>
<br>Async generator objects (including those created by async generator
<br>comprehensions):
<br>- ``__aiterclose__`` calls ``self.aclose()``
<br>- ``__del__`` issues a ``RuntimeWarning`` if ``aclose`` has not been
<br>called, since this probably indicates a latent bug, similar to the
<br>"coroutine never awaited" warning.
<br>
<br>QUESTION: should file objects implement ``__iterclose__`` to close the
<br>file? On the one hand this would make this change more disruptive; on
<br>the other hand people really like writing ``for line in open(...):
<br>...``, and if we get used to iterators taking care of their own
<br>cleanup then it might become very weird if files don't.
<br>
<br>
<br>New convenience functions
<br>-------------------------
<br>
<br>The ``itertools`` module gains a new iterator wrapper that can be used
<br>to selectively disable the new ``__iterclose__`` behavior::
<br>
<br>  # QUESTION: I feel like there might be a better name for this one?
<br>  class preserve(iterable):
<br>      def __init__(self, iterable):
<br>          self._it = iter(iterable)
<br>
<br>      def __iter__(self):
<br>          return self
<br>
<br>      def __next__(self):
<br>          return next(self._it)
<br>
<br>      def __iterclose__(self):
<br>          # Swallow __iterclose__ without passing it on
<br>          pass
<br>
<br>Example usage (assuming that file objects implements ``__iterclose__``)::
<br>
<br>  with open(...) as handle:
<br>      # Iterate through the same file twice:
<br>      for line in itertools.preserve(handle):
<br>          ...
<br>      handle.seek(0)
<br>      for line in itertools.preserve(handle):
<br>          ...
<br>
<br>The ``operator`` module gains two new functions, with semantics
<br>equivalent to the following::
<br>
<br>  def iterclose(it):
<br>      if hasattr(type(it), "__iterclose__"):
<br>          type(it).__iterclose__(it)
<br>
<br>  async def aiterclose(ait):
<br>      if hasattr(type(ait), "__aiterclose__"):
<br>          await type(ait).__aiterclose__(ait)
<br>
<br>These are particularly useful when implementing the changes in the next section:
<br>
<br>
<br>__iterclose__ implementations for iterator wrappers
<br>------------------------------<wbr>---------------------
<br>
<br>Python ships a number of iterator types that act as wrappers around
<br>other iterators: ``map``, ``zip``, ``itertools.accumulate``,
<br>``csv.reader``, and others. These iterators should define a
<br>``__iterclose__`` method which calls ``__iterclose__`` in turn on
<br>their underlying iterators. For example, ``map`` could be implemented
<br>as::
<br>
<br>  class map:
<br>      def __init__(self, fn, *iterables):
<br>          self._fn = fn
<br>          self._iters = [iter(iterable) for iterable in iterables]
<br>
<br>      def __iter__(self):
<br>          return self
<br>
<br>      def __next__(self):
<br>          return self._fn(*[next(it) for it in self._iters])
<br>
<br>      def __iterclose__(self):
<br>          for it in self._iters:
<br>              operator.iterclose(it)
<br>
<br>In some cases this requires some subtlety; for example,
<br>```itertools.tee``
<br><<a href="https://docs.python.org/3/library/itertools.html#itertools.tee" target="_blank" rel="nofollow" onmousedown="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fitertools.html%23itertools.tee\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNFZPPAKgYZ3Rsff_wCho1uYoPmtQQ';return true;" onclick="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fitertools.html%23itertools.tee\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNFZPPAKgYZ3Rsff_wCho1uYoPmtQQ';return true;">https://docs.python.org/3/<wbr>library/itertools.html#<wbr>itertools.tee</a>>`_
<br>should not call ``__iterclose__`` on the underlying iterator until it
<br>has been called on *all* of the clone iterators.
<br>
<br>
<br>Example / Rationale
<br>-------------------
<br>
<br>The payoff for all this is that we can now write straightforward code like::
<br>
<br>  def read_newline_separated_json(<wbr>path):
<br>      for line in open(path):
<br>          yield json.loads(line)
<br>
<br>and be confident that the file will receive deterministic cleanup
<br>*without the end-user having to take any special effort*, even in
<br>complex cases. For example, consider this silly pipeline::
<br>
<br>  list(map(lambda key: key.upper(),
<br>           doc["key"] for doc in read_newline_separated_json(<wbr>path)))
<br>
<br>If our file contains a document where ``doc["key"]`` turns out to be
<br>an integer, then the following sequence of events will happen:
<br>
<br>1. ``key.upper()`` raises an ``AttributeError``, which propagates out
<br>of the ``map`` and triggers the implicit ``finally`` block inside
<br>``list``.
<br>2. The ``finally`` block in ``list`` calls ``__iterclose__()`` on the
<br>map object.
<br>3. ``map.__iterclose__()`` calls ``__iterclose__()`` on the generator
<br>comprehension object.
<br>4. This injects a ``GeneratorExit`` exception into the generator
<br>comprehension body, which is currently suspended inside the
<br>comprehension's ``for`` loop body.
<br>5. The exception propagates out of the ``for`` loop, triggering the
<br>``for`` loop's implicit ``finally`` block, which calls
<br>``__iterclose__`` on the generator object representing the call to
<br>``read_newline_separated_json`<wbr>`.
<br>6. This injects an inner ``GeneratorExit`` exception into the body of
<br>``read_newline_separated_json`<wbr>`, currently suspended at the ``yield``.
<br>7. The inner ``GeneratorExit`` propagates out of the ``for`` loop,
<br>triggering the ``for`` loop's implicit ``finally`` block, which calls
<br>``__iterclose__()`` on the file object.
<br>8. The file object is closed.
<br>9. The inner ``GeneratorExit`` resumes propagating, hits the boundary
<br>of the generator function, and causes
<br>``read_newline_separated_json`<wbr>`'s ``__iterclose__()`` method to return
<br>successfully.
<br>10. Control returns to the generator comprehension body, and the outer
<br>``GeneratorExit`` continues propagating, allowing the comprehension's
<br>``__iterclose__()`` to return successfully.
<br>11. The rest of the ``__iterclose__()`` calls unwind without incident,
<br>back into the body of ``list``.
<br>12. The original ``AttributeError`` resumes propagating.
<br>
<br>(The details above assume that we implement ``file.__iterclose__``; if
<br>not then add a ``with`` block to ``read_newline_separated_json`<wbr>` and
<br>essentially the same logic goes through.)
<br>
<br>Of course, from the user's point of view, this can be simplified down to just:
<br>
<br>1. ``int.upper()`` raises an ``AttributeError``
<br>1. The file object is closed.
<br>2. The ``AttributeError`` propagates out of ``list``
<br>
<br>So we've accomplished our goal of making this "just work" without the
<br>user having to think about it.
<br>
<br>
<br>Transition plan
<br>===============
<br>
<br>While the majority of existing ``for`` loops will continue to produce
<br>identical results, the proposed changes will produce
<br>backwards-incompatible behavior in some cases. Example::
<br>
<br>  def read_csv_with_header(lines_<wbr>iterable):
<br>      lines_iterator = iter(lines_iterable)
<br>      for line in lines_iterator:
<br>          column_names = line.strip().split("\t")
<br>          break
<br>      for line in lines_iterator:
<br>          values = line.strip().split("\t")
<br>          record = dict(zip(column_names, values))
<br>          yield record
<br>
<br>This code used to be correct, but after this proposal is implemented
<br>will require an ``itertools.preserve`` call added to the first ``for``
<br>loop.
<br>
<br>[QUESTION: currently, if you close a generator and then try to iterate
<br>over it then it just raises ``Stop(Async)Iteration``, so code the
<br>passes the same generator object to multiple ``for`` loops but forgets
<br>to use ``itertools.preserve`` won't see an obvious error -- the second
<br>``for`` loop will just exit immediately. Perhaps it would be better if
<br>iterating a closed generator raised a ``RuntimeError``? Note that
<br>files don't have this problem -- attempting to iterate a closed file
<br>object already raises ``ValueError``.]
<br>
<br>Specifically, the incompatibility happens when all of these factors
<br>come together:
<br>
<br>- The automatic calling of ``__(a)iterclose__`` is enabled
<br>- The iterable did not previously define ``__(a)iterclose__``
<br>- The iterable does now define ``__(a)iterclose__``
<br>- The iterable is re-used after the ``for`` loop exits
<br>
<br>So the problem is how to manage this transition, and those are the
<br>levers we have to work with.
<br>
<br>First, observe that the only async iterables where we propose to add
<br>``__aiterclose__`` are async generators, and there is currently no
<br>existing code using async generators (though this will start changing
<br>very soon), so the async changes do not produce any backwards
<br>incompatibilities. (There is existing code using async iterators, but
<br>using the new async for loop on an old async iterator is harmless,
<br>because old async iterators don't have ``__aiterclose__``.) In
<br>addition, PEP 525 was accepted on a provisional basis, and async
<br>generators are by far the biggest beneficiary of this PEP's proposed
<br>changes. Therefore, I think we should strongly consider enabling
<br>``__aiterclose__`` for ``async for`` loops and async generators ASAP,
<br>ideally for 3.6.0 or 3.6.1.
<br>
<br>For the non-async world, things are harder, but here's a potential
<br>transition path:
<br>
<br>In 3.7:
<br>
<br>Our goal is that existing unsafe code will start emitting warnings,
<br>while those who want to opt-in to the future can do that immediately:
<br>
<br>- We immediately add all the ``__iterclose__`` methods described above.
<br>- If ``from __future__ import iterclose`` is in effect, then ``for``
<br>loops and ``*`` unpacking call ``__iterclose__`` as specified above.
<br>- If the future is *not* enabled, then ``for`` loops and ``*``
<br>unpacking do *not* call ``__iterclose__``. But they do call some other
<br>method instead, e.g. ``__iterclose_warning__``.
<br>- Similarly, functions like ``list`` use stack introspection (!!) to
<br>check whether their direct caller has ``__future__.iterclose``
<br>enabled, and use this to decide whether to call ``__iterclose__`` or
<br>``__iterclose_warning__``.
<br>- For all the wrapper iterators, we also add ``__iterclose_warning__``
<br>methods that forward to the ``__iterclose_warning__`` method of the
<br>underlying iterator or iterators.
<br>- For generators (and files, if we decide to do that),
<br>``__iterclose_warning__`` is defined to set an internal flag, and
<br>other methods on the object are modified to check for this flag. If
<br>they find the flag set, they issue a ``PendingDeprecationWarning`` to
<br>inform the user that in the future this sequence would have led to a
<br>use-after-close situation and the user should use ``preserve()``.
<br>
<br>In 3.8:
<br>
<br>- Switch from ``PendingDeprecationWarning`` to ``DeprecationWarning``
<br>
<br>In 3.9:
<br>
<br>- Enable the ``__future__`` unconditionally and remove all the
<br>``__iterclose_warning__`` stuff.
<br>
<br>I believe that this satisfies the normal requirements for this kind of
<br>transition -- opt-in initially, with warnings targeted precisely to
<br>the cases that will be effected, and a long deprecation cycle.
<br>
<br>Probably the most controversial / risky part of this is the use of
<br>stack introspection to make the iterable-consuming functions sensitive
<br>to a ``__future__`` setting, though I haven't thought of any situation
<br>where it would actually go wrong yet...
<br>
<br>
<br>Acknowledgements
<br>================
<br>
<br>Thanks to Yury Selivanov, Armin Rigo, and Carl Friedrich Bolz for
<br>helpful discussion on earlier versions of this idea.
<br>
<br>-- 
<br>Nathaniel J. Smith -- <a href="https://vorpus.org" target="_blank" rel="nofollow" onmousedown="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fvorpus.org\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNGu9BH-CNkjUnoU8qy5kxnhJP302A';return true;" onclick="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fvorpus.org\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNGu9BH-CNkjUnoU8qy5kxnhJP302A';return true;">https://vorpus.org</a>
<br>______________________________<wbr>_________________
<br>Python-ideas mailing list
<br><a href="javascript:" target="_blank" gdf-obfuscated-mailto="rRM1MbYUBAAJ" rel="nofollow" onmousedown="this.href='javascript:';return true;" onclick="this.href='javascript:';return true;">Python...@python.org</a>
<br><a href="https://mail.python.org/mailman/listinfo/python-ideas" target="_blank" rel="nofollow" onmousedown="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fmail.python.org%2Fmailman%2Flistinfo%2Fpython-ideas\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNFj1EaNHnVmh20FnFPoUi4J-MpfQw';return true;" onclick="this.href='https://www.google.com/url?q\x3dhttps%3A%2F%2Fmail.python.org%2Fmailman%2Flistinfo%2Fpython-ideas\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNFj1EaNHnVmh20FnFPoUi4J-MpfQw';return true;">https://mail.python.org/<wbr>mailman/listinfo/python-ideas</a>
<br>Code of Conduct: <a href="http://python.org/psf/codeofconduct/" target="_blank" rel="nofollow" onmousedown="this.href='http://www.google.com/url?q\x3dhttp%3A%2F%2Fpython.org%2Fpsf%2Fcodeofconduct%2F\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNHJOrArSUDKkjrnthO6_CznMzkPsA';return true;" onclick="this.href='http://www.google.com/url?q\x3dhttp%3A%2F%2Fpython.org%2Fpsf%2Fcodeofconduct%2F\x26sa\x3dD\x26sntz\x3d1\x26usg\x3dAFQjCNHJOrArSUDKkjrnthO6_CznMzkPsA';return true;">http://python.org/psf/<wbr>codeofconduct/</a>
<br></blockquote></div></div>