Take a look at the following example:
from contextlib import AbstractContextManager, contextmanager
class MyClass(AbstractContextManager):
_values = {}
@contextmanager
def using(self, name, value):
print(f'Allocating {name} = {value}')
self._values[name] = value
try:
yield
finally:
print(f'Releasing {name}')
del self._values[name]
def __enter__(self):
return self.using('FOO', 42).__enter__()
def __exit__(self, exc_type, exc_val, exc_tb):
pass
with MyClass():
print('Doing work...')
I would expect the above code to print the following:
Allocating FOO = 42
Doing work...
Releasing FOO
Instead, this is what is being printed:
Allocating FOO = 42
Releasing FOO
Doing work...
Why is FOO
getting released eagerly?
CodePudding user response:
You're creating two context managers here. Only one of those context managers is actually implemented correctly.
Your using
context manager is fine, but you've also implemented the context manager protocol on MyClass
itself, and the implementation on MyClass
is broken. MyClass.__enter__
creates a using
context manager, enters it, returns what that context manager's __enter__
returns, and then throws the using
context manager away.
You don't exit the using
context manager when MyClass()
is exited. You never exit it at all! You throw the using
context manager away. It gets reclaimed, and when it does, the generator gets close
called automatically, as part of normal generator cleanup. That throws a GeneratorExit
exception into the generator, triggering the finally
block.
Python doesn't promise when this cleanup will happen (or indeed, if it will happen at all), but in practice, CPython's reference counting mechanism triggers the cleanup as soon as the using
context manager is no longer reachable.
Aside from that, if _values
is supposed to be an instance variable, it should be set as self._values = {}
inside an __init__
method. Right now, it's a class variable.