What is needed upon __exit__ in a Python context manager?

105 Views Asked by At

I am trying to write a context manager function. It "works", but I have noticed that the __repr__ method from the context continues to fire even after the "with" has exited.

Here is the TimedContext class:

import time

class TimedContext:
    def __init__(self):
        self.start = self.now = self.duration = 0

    def __enter__(self):
        self.start = time.time()
        return self

    def __format__(self, spec):
        self._update_duration()
        return format(self.duration, spec)

    def __exit__(self, type_, value, traceback):
        self._update_duration()
        return traceback is None

    def _update_duration(self):
        self.now = time.time()
        self.duration = self.now - self.start

Here is me using it:

from TimedContext import TimedContext
import time

with TimedContext() as t:
    time.sleep(1)
    print(f"Time0: {t}")

print(f"Time1: {t}")
time.sleep(1)
print(f"Time2: {t}")

Here is my output running the script:

Time0: 1.001316785812378
Time1: 1.0014879703521729
Time2: 2.0025713443756104

I had expected Time2 to be the same as Time1 since I expected the end of the with completed the context so that t would now just be a variable containing the final return from the context. (do I have my wording right here?)

The reason I am updating the TimedContext within the __format__ function is that I want to be able to reference t from both inside and outside the "with". If I did not update here, t would be correct after the __exit__ was run, but inside the with, it would just be the __init__ value.

I can fix this "issue" by creating a boolean exit_called, setting it to False on __init__ and True on __exit__ and then only update self.duration if exit_called is False. While this "works", I was expecting the context to be gone since the with completed and __exit__ has run to completion. However, it appears the context sticks around. Is it deterministic how long? When does the context get cleaned up? I had assumed the closing of the with did that. Is there something I need to do to allow the context to be cleaned up, or do I need to explicitly do it?

I'd like to understand what should be going on here. I wasn't expecting the context to stick around once __exit__ completes. Maybe I should be addressing my problem a different way?

1

There are 1 best solutions below

2
Barmar On

From Unraveling the with statement:

with a as b:
    body...

is equivalent to

_enter = type(a).__enter__
_exit = type(a).__exit__
b = _enter(a)

try:
   body...
except:
   if not _exit(a, *sys.exc_info()):
       raise
else:
   _exit(a, None, None, None)

with doesn't do any cleanup of its own, it calls the __exit__() method to do that. It also doesn't delete the b variable -- it still contains the value that was assigned by as b.

For example, when you use

with open(filename) as f:
    code...

print(f)

f will still contain the file object, but it will be closed and unusable because its __exit__() method closes the file and reclaims any resources.

This is no different from other constructs that assign variables for use in a block. For instance, the variable in a for statement can be used after the loop is done -- it will contain the value from the last iteration.

The context manager object itself will eventually be cleaned up by the garbage collector, when the variable goes out of scope.