On 8 August 2018 at 08:48, Nathaniel Smith <n...@pobox.com> wrote: > On Tue, Aug 7, 2018 at 11:14 PM, Ken Hilton <kenlhil...@gmail.com> wrote: >> >> Now, let's take a look at the following scenario: >> >> def read_multiple(*filenames): >> for filename in filenames: >> with open(filename) as f: >> yield f.read() >> >> Can you spot the problem? The "with open(filename)" statement is supposed to >> ensure that the file object is disposed of properly. However, the "yield >> f.read()" statement suspends execution within the with block, so if this >> happened: >> >> for contents in read_multiple('chunk1', 'chunk2', 'chunk3'): >> if contents == 'hello': >> break >> >> and the contents of "chunk2" were "hello" then the loop would exit, and >> "chunk2" would never be closed! Yielding inside a with block, therefore, >> doesn't make sense and can only lead to obscure bugs. > > This is a real problem. (Well, technically the 'with' block's __exit__ > function *will* eventually close the file, when the generator is > garbage-collected – see PEP 342 for details – but this is not exactly > satisfying, because the whole purpose of the 'with' block is to close > the file *without* relying on the garbage collector.)
PEP 342 guarantees that *if* a generator is garbage collected its .close() method will be called which would in turn trigger __exit__() for any active context managers in the generator. However PEP 342 does not guarantee that the generator would be garbage collected. As you noted in PEP 533: In Python implementations that do not use reference counting (e.g. PyPy, Jython), calls to __del__ may be arbitrarily delayed... However this statement does not go far enough. It is not guaranteed that __del__ will be called *at all* under other implementations. An example to test this is: $ cat gencm.py class CM: def __enter__(self): print("Entering") return self def __exit__(self, *args): print("Exiting") def generator(): with CM(): yield 1 yield 2 yield 3 g = generator() def f(): for x in g: break # Or return f() print("End of program") $ python3 gencm.py Entering End of program Exiting $ pypy --version Python 2.7.2 (1.8+dfsg-2, Feb 19 2012, 19:18:08) [PyPy 1.8.0 with GCC 4.6.2] $ pypy gencm.py Entering End of program (I don't actually have pypy to hand right now so I'm copying this from here: https://www.mail-archive.com/tutor@python.org/msg70961.html) What the above shows is that for this example: 1) Under CPython __exit__ is called by __del__ at process exit after every line of Python code has finished. 2) Under PyPy __exit__ was not called at any point That's not a bug in PyPy: Python the language makes very few guarantees about garbage collection. > Unfortunately, your proposal for solving it is a non-starter -- there > are lots of cases where 'yield' inside a 'with' block is not only > used, but is clearly the right thing to do. A notable one is when > defining a next contextmanager in terms of a pre-existing > contextmanager: > > @contextmanager > def tempfile(): > # This is an insecure way of making a temp file but good enough > for an example > tempname = pick_random_filename() > with open(tempname, "w") as f: > yield f This is the only example I can think of where yielding from a with statement is the right thing to do. Really this comes from the way that the contextmanager is abusing syntax though. It takes a bunch of strange machinery to make this work as it does: https://github.com/python/cpython/blob/3.7/Lib/contextlib.py#L116 > One small step that might be doable would be to start issuing > ResourceWarning whenever a generator that was suspended inside a > 'with' or 'try' block is GC'ed. This seems like a great idea to me. -- Oscar _______________________________________________ Python-ideas mailing list Python-ideas@python.org https://mail.python.org/mailman/listinfo/python-ideas Code of Conduct: http://python.org/psf/codeofconduct/