In the following minimal example, bar
continues to run after the scope of the with
statement:
import threading
import time
class Foo:
def __init__(self, delay):
self._delay = delay
pass
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
pass
def bar(self):
time.sleep(self._delay)
print("done")
with Foo(2) as my_foo:
thread = threading.Thread(target=my_foo.bar)
thread.start()
time.sleep(3)
It seems to work, but is it guaranteed to be safe to do so, or can it lead to undefined behavior or errors (something with dangling references or similar)?
CodePudding user response:
The context manager has nothing with object creation/deletion, it just guarantee us __exit__
will be called.
In such cases it can be helpfull to see the bytecode:
import dis
class Foo:
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
pass
def f():
with Foo():
pass
dis.dis(f)
13 0 LOAD_GLOBAL 0 (Foo)
2 CALL_FUNCTION 0
4 SETUP_WITH 16 (to 22)
6 POP_TOP
14 8 POP_BLOCK
10 LOAD_CONST 0 (None)
12 DUP_TOP
14 DUP_TOP
16 CALL_FUNCTION 3 # calling __exit__
18 POP_TOP
20 JUMP_FORWARD 16 (to 38) # if there is no exception just go to the end
>> 22 WITH_EXCEPT_START
24 POP_JUMP_IF_TRUE 28
26 RERAISE
>> 28 POP_TOP
30 POP_TOP
32 POP_TOP
34 POP_EXCEPT
36 POP_TOP
>> 38 LOAD_CONST 0 (None)
40 RETURN_VALUE
There is SETUP_WITH which generally push __exit__
on the stack and do some preparation in case of exception. 16 calls the __exit__
.
To sum up, you are right, until thread is running reference counter for Foo won't be zeroed, that's why python won't collect it.