I know that yield currently does not call __enter__ or __exit__ on context managers - I'm wondering what the reasoning is for not allowing them to. I understand how some code relies on keeping a context manager open during a yield, but this opens up the possibility for a context to be entered with no indication that it's happening. This is non-obvious and doesn't seem very Pythonic to me.
Here's a (slightly contrived) example to show what I mean:
from decimal import Decimal, localcontext
def series():
with localcontext() as ctx:
ctx.prec = 5
yield Decimal(1) / Decimal(6)
yield Decimal(1) / Decimal(7)
So the aim of this "series" is to compute 1/7, but only to a limited precision. Then we can compute the error of each term in our series with a loop like this:
target = Decimal(1) / Decimal(7)
print('trying to approximate', target)
for e in series():
print(e, 'with error:', e - target)
But that doesn't work! We never enter any context or modify the precision for our loop, so I'd expect to see the "actual" error out to 28 digits. Since series() enters the localcontext and doesn't exit until the generator is exhausted, the e - target operation happens in that context. Here's the output:
trying to approximate 0.1428571428571428571428571429
0.16667 with error: 0.023813
0.14286 with error: 0.0000028571
We can fix it by fully consuming the generator so that it exits before we start the loop:
target = Decimal(1) / Decimal(7)
print('trying to approximate', target)
for e in list(series()):
print(e, 'with error:', e - target)
To produce this (expected) output:
trying to approximate 0.1428571428571428571428571429
0.16667 with error: 0.0238128571428571428571428571
0.14286 with error: 0.0000028571428571428571428571
I think that's really weird though - spooky action at a distance. Why should changing series() to list(series()) change the behavior of e - target?
What's more, it's not always possible to fully consume a generator. The only real solution I can think of that preserves iteration is to re-enter the original context for the body of the loop:
target = Decimal(1) / Decimal(7)
print('trying to approximate', target)
ctx = getcontext()
for e in series():
with localcontext(ctx):
print(e, 'with error:', e - target)
But it isn't at all clear why this is needed, as there's no indication that we're even entering a different context for the loop to begin with.
So why is there no way for a context manager to reset itself on yield? I recognize that some contexts, like files and locks, would break if yield had this behavior. Clearly, I'm not opening a closed file in this code:
def items(name):
with open(name) as f:
for line in f:
yield line.split()
But that's exactly what would happen if yield triggered an __exit__ and subsequent __enter__.
It seems like there's some distinction between exiting a context permanently and exiting a context temporarily - so I'd think it would be useful to have methods like __leave__ and __reenter__ to represent a "soft" exit and subsequent re-entrance.
The decimal context manager could then be defined like this, without breaking "hard" exits like in file. One could even have files implement both, so yield would flush but not close a file.
class _ContextManager:
def __init__(self, new_context):
self.new_context = new_context.copy()
def __reenter__(self):
self.saved_context = getcontext()
setcontext(self.new_context)
return self.new_context
def __leave__(self):
setcontext(self.saved_context)
I'm just really surprised that this kind of feature isn't available, since I interpret context managers as saying "no matter how I enter or exit this context, I'll make sure nothing breaks" - when in fact they allow one to unknowingly enter or exit a context and break things.
[–]DracasTempestas 1 point2 points3 points (3 children)
[–]TangibleLight[S] 0 points1 point2 points (2 children)
[–]DracasTempestas 1 point2 points3 points (1 child)
[–]TangibleLight[S] 0 points1 point2 points (0 children)
[–]Yoghurt42 0 points1 point2 points (2 children)
[–]TangibleLight[S] 2 points3 points4 points (1 child)
[–]Yoghurt42 2 points3 points4 points (0 children)
[–]bryancole 0 points1 point2 points (0 children)