[Python-ideas] Generator-based context managers can't skip __exit__

Steven D'Aprano steve at pearwood.info
Sun Nov 6 03:07:12 EST 2016


On Sun, Nov 06, 2016 at 06:46:40AM +0200, Ram Rachum wrote:
> Hi everyone,
> 
> Here is a simplification of a problem that's been happening in my code:
> 
> import contextlib
> 
> @contextlib.contextmanager
> def f():
>     print('1')
>     try:
>         yield
>     finally:
>         print('2')
> 
> 
> g = f()
> g.__enter__()
> 
> 
> This code prints 1 and then 2, not just 1 like you might expect.

I expect it to print 2. After all, its in a finally clause.

And why are you calling g.__enter__() directly? Calling dunder methods 
by hand is nearly always the wrong thing to do.

> This is
> because when the generator is garbage-collected, it gets `GeneratorExit`
> sent to it.

Right. That's what they're designed to do.

Later, in another thread you say:

"Well, you think it's weird that I want a `finally` clause to not be 
called in some circumstances. Do you think it's equally weird to want an
`__exit__` method that is not called in some circumstances?"

Yes to both. It is seriously weird.

You might as well be complaining that Python calls your __iter__ 
method when you call iter(my_instance). That's the whole point of 
__iter__, and the whole point of finally clauses and the __exit__ method 
is that they are unconditionally called, always, when you leave the 
with block.

But judging from your code above, it looks like you're not even using a 
with block. In that case, instead of abusing the __enter__ and __exit__ 
methods, why not just create a class with non-dunder enter() and exit() 
methods and call them by hand?

g = f()  # implementation of f is left as an exercise
g.enter()
if condition:
    g.exit()


I'm having a lot of difficulty in understanding your use-case here, and 
so maybe I've completely misunderstood something.


> This has been a problem in my code since in some instances, I tell a
> context manager not to do its `__exit__` function. (I do this by using
> `ExitStack.pop_all()`. However the `__exit__` is still called here.

Have you considered something like:

def f():
    print('1')
    try:
        yield
    finally:
        if f.closing:
            print('2')


You can then write a decorator to set f.closing to True or False as 
needed. But again, I don't understand why you would want this feature, 
or how you are using it, so I might have this completely wrong.



-- 
Steve


More information about the Python-ideas mailing list