[Tutor] Yielding from a with block

Steven D'Aprano steve at pearwood.info
Fri May 29 14:38:30 CEST 2015


On Thu, May 28, 2015 at 10:46:26AM +0100, Oscar Benjamin wrote:

> I'm sure you understand the fundamental difference between calling a
> function and yielding inside a with statement: when calling a function
> the new frame is *appended* to the call stack keeping the current
> frame and all of its exception traps and context managers in place
> ready for unwinding. When yielding the current frame is *removed* from
> the call stack (along with its exception traps and context managers).

Implementation details.


> When a with block is used without a yield it is not possible (barring
> unrecoverable failure of the runtime itself) to leave the block
> without calling its finaliser. 

Yes.


> It doesn't matter if we call a function
> that in turn calls a generator. 

This sentence is ambiguous. Do you mean "calls a generator function", or 
do you mean "call [next() on] a generator object"?

I don't suppose it really matters though.


> As long as there is no yield inside
> the with block in the current frame then the finalisation guarantee is
> maintained. When using yield we don't have this guarantee and in fact
> there are common non-exceptional cases (break/return) where the
> undesirable occurs.

Er, no. You still have the finalisation guarantee. You just have to 
understand what the guarantee actually is. It does *not* refer to 
pausing the generator to pass control back to the caller. That would 
make yield inside a with block absolutely useless.

The guarantee is that when you *exit* (not pause) the with block, then 
and only then will the context manager's __exit__ method run. That's no 
different from the non-generator use of a CM.

If you think that guarantee is not made, you have to either demonstrate 
a counter-example where exiting the with block fails to run the __exit__ 
method, or point us to official documentation supporting your position.

Otherwise I stand by my earlier position that you are misinterpreting 
what it means to exit a with block. Pausing it to yield is not an exit.

I did an experiment, where I tried to break the finalisation 
guarantee using break, return and raise:

class CM:
    def __enter__(self):
        return self
    def __exit__(self, *args):
        print("exiting")

def test(n):
    for i in range(1):
        with CM():
            if n == "break": break
            if n == "return": return
            if n == "raise": raise RuntimeError
            yield 1



Falling out the bottom of the generator finalises correctly. So do 
break, return and raise.

it = test("")
x = next(it)
next(it, None)  # prints "exiting"

it = test("break")
next(it, None)  # prints "exiting"

it = test("return")
next(it, None)  # prints "exiting"

it = test("raise")
try: next(it)
except: pass  # prints "exiting"



Under what circumstances can execution leave the with block without the 
finalisation method __exit__ running?



-- 
Steve


More information about the Tutor mailing list