yield in try/finally case
Peter Otten
__peter__ at web.de
Thu Mar 3 08:47:37 EST 2016
刘琦帆 wrote:
> 在 2016年3月3日星期四 UTC+8下午8:14:29,Oscar Benjamin写道:
>> On 3 March 2016 at 11:52, 刘琦帆 <lqf.txx at gmail.com> wrote:
>> >
>> > "A yield statement is not allowed in the try clause of a try/finally
>> > construct. The difficulty is that there's no guarantee the generator
>> > will ever be resumed, hence no guarantee that the finally block will
>> > ever get executed; that's too much a violation of finally's purpose to
>> > bear." from https://www.python.org/dev/peps/pep-0255/
>> >
>> > But, meanwhile, the code showed on that page use yield in a try/finally
>> > case. It really puzzles me. Is there anything wrong?
>>
>> I think what it means is that you can put a yield in the finally block
>> but not the try block so:
>>
>> # Not allowed
>> def f():
>> try:
>> yield 1
>> finally:
>> pass
>>
>> # Allowed
>> def f():
>> try:
>> pass
>> finally:
>> yield 1
>>
>> However that information is out of date. The restriction was removed
>> in some later Python version. Actually the construct is quite common
>> when using generator functions to implement context managers:
>>
>> @contextlib.contextmanager
>> def replace_stdin(newstdin):
>> oldstdin = sys.stdin
>> try:
>> sys.stdin = newstdin
>> yield
>> finally:
>> sys.stdin = oldstdin
>>
>> Although the restriction was removed the problem itself still remains.
>> There's no guarantee that a finally block will execute if there is a
>> yield in the try block. The same happens if you use a context manager
>> around a yield statement: the __exit__ method is not guaranteed to be
>> called. One implication of this is that in the following code it is
>> not guaranteed that the file will be closed:
>>
>> def upperfile(filename):
>> with open(filename) as fin:
>> for line in fin:
>> yield line.upper()
>>
>> --
>> Oscar
>
>
> It really nice of you to answer the question. But I am still confused with
> your last example, is there any case that the file with not be closed? I
> just run the code and no exception occur.
It doesn't happen easily, you have to defeat CPython's garbage collection.
Consider the follwing script:
$ cat upper1.py
import sys
_open = open
files = []
def open(*args, **kw):
"""Use a modified open() which keeps track of opened files.
This allows us to check whether the files are properly closed and
also to defeat garbage collection.
"""
f = _open(*args, **kw)
files.append(f)
return f
for filename in sys.argv[1:]:
with open(filename) as f:
for line in f:
print(line.upper(), end="")
break
assert all(f.closed for f in files)
$ echo -e 'foo\nbar\nbaz' > tmp1.txt
$ echo -e 'hams\nspam\njam' > tmp2.txt
$ python3 upper1.py *.txt
FOO
HAMS
As expected it prints the first lines of the files provided as commandline
args, in upper case. Now let's refactor:
$ cat upper2.py
import sys
_open = open
files = []
def open(*args, **kw):
"""Use a modified open() which keeps track of opened files.
This allows us to check whether the files are properly closed (and
also defeats garbage collection).
"""
f = _open(*args, **kw)
files.append(f)
return f
def upperfile(filename):
with open(filename) as f:
for line in f:
yield line.upper()
for uf in map(upperfile, sys.argv[1:]):
for line in uf:
print(line, end="")
break
assert all(f.closed for f in files)
The change looks harmless, we moved the with statement and the conversion
into the generater. But when whe run it:
$ python3 upper2.py *.txt
FOO
HAMS
Traceback (most recent call last):
File "upper2.py", line 26, in <module>
assert all(f.closed for f in files)
AssertionError
This is because the last generator uf = upperfile(...) is not garbage-
collected and wasn't explicitly closed either. If you do care here's one
possible fix:
from contextlib import closing
...
for uf in map(upperfile, sys.argv[1:]):
with closing(uf):
for line in uf:
print(line, end="")
break
More information about the Python-list
mailing list