[Tutor] Yielding from a with block
Peter Otten
__peter__ at web.de
Thu May 28 10:16:00 CEST 2015
Oscar Benjamin wrote:
> On 23 May 2015 at 10:52, Peter Otten <__peter__ at web.de> wrote:
> I'm just wondering what other people think about this. Should code
> like make_lines below be discouraged?
>> def make_lines():
>> with open('co2-sample.csv') as co2:
>> yield htbegin
>> for linelist in csv.reader(co2):
>> yield from fprint(linelist)
>> yield htend
In my opinion, yes.
As I've used the pattern since Python 2.5 when try...finally in generators
became legal I will need some time to unlearn.
> There's a fundamental contradiction between the with and yield
> statements. With means "don't exit this block without running my
> finalisation routine". Yield means "immediately exit this block
> without doing anything (including any finalisation routines) and then
> allow anything else to occur".
>
> Using yield inside a with statement like this renders the with
> statement pointless: either way we're really depending on __del__ to
> execute the finalisation code. So it with or without the with
> statement it will work fine on CPython. On other implementations it
> may or may not close the file with or without the with statement. IOW
> the with statement is not able to provide the guarantees normally
> expected of it when used this way.
Even if you limit yourself to CPython there is another effect: the order of
execution may not meet one's expectations/requirements:
$ cat with_in_generator.py
import contextlib
@contextlib.contextmanager
def demo():
print("before")
try:
yield
finally:
print("after")
def gen(items="abc"):
with demo():
yield from items
if __name__ == "__main__":
g = gen()
for item in g:
print(item)
if item == "b":
break
print("bye")
$ python3 with_in_generator.py
before
a
b
bye
after
$
(in case you don't spot it: "after" should be printed before "bye")
To get a well-defined order of execution you can close the generator
explicitly
$ cat with_in_generator2.py
[...]
if __name__ == "__main__":
g = gen()
with contextlib.closing(g) as h:
for item in h:
print(item)
if item == "b":
break
print("bye")
$ python3 with_in_generator2.py
before
a
b
after
bye
...but the obvious route is of course
> It's usually fairly trivial to rearrange things so that this doesn't
> happen:
>
> def wrap_header_footer(fin):
> yield htbegin
> for linelist in csv.reader(fin):
> yield from fprint(linelist)
> yield htend
which in this case also has the advantage of better separation of concerns
(I'd probably move the csv.reader() out of the generator, too).
PS: I'm still looking for a fairly elegant rewrite of the problematic
def lines(files):
for file in files:
with open(files) as f:
yield from f
(see Oscar's comment in
<https://mail.python.org/pipermail/tutor/2015-May/105448.html>)
More information about the Tutor
mailing list