After some basic research, the answer, at least for CPython (as produced by Anaconda) is as follows, based on the script below with its accompanying output. Basically, when an object is collected when scope is lost, the stack will be pointing to the line that instantiated the frame in which, at return, the object is collected. I.e. if after returning from afunc, an object is collected, the __del__ calling frame will be at the point where afunc had been called.
Though I couldn't reproduce an example, presumably if delayed collection occurs, the calling frame of __del__ will be at the point that the delayed collection occurred.
Example script:
import traceback
class ClassWithDel:
def __init__(self, whoami='Default'):
self._whoami = whoami
def __del__(self):
print(f'{self._whoami}: In __del__')
traceback.print_stack()
def whoami(self):
print(f'I am {self._whoami}')
def afunc():
cwd = ClassWithDel()
cwd.whoami()
def closure():
cwd = ClassWithDel()
def closure_with_cwd():
cwd.whoami()
return closure_with_cwd
def afunc_with_closure():
closure_cwd = closure()
closure_cwd()
def afunc_overwrite():
cwd = ClassWithDel()
cwd.whoami()
cwd = ClassWithDel('NotDefault')
cwd.whoami()
def return_list():
results = [ClassWithDel('l1'), ClassWithDel('l2')]
return results
def afunc_overwrite_list():
l = return_list()
l2 = l[1]
l = l2
if __name__ == '__main__':
print('Starting with afunc...')
afunc()
print('\nStarting with closure...')
afunc_with_closure()
print('\nStarting with overwrite...')
afunc_overwrite()
print('\nStarting list overwrite...')
afunc_overwrite_list()
The results:
$ python -m gc_test
Starting with afunc...
I am Default
Default: In __del__
File ".../lib/python3.8/runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
File ".../lib/python3.8/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "gc_test.py", line 56, in <module>
afunc()
File "gc_test.py", line 11, in __del__
traceback.print_stack()
Starting with closure...
I am Default
Default: In __del__
File ".../lib/python3.8/runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
File ".../lib/python3.8/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "gc_test.py", line 58, in <module>
afunc_with_closure()
File "gc_test.py", line 11, in __del__
traceback.print_stack()
Starting with overwrite...
I am Default
Default: In __del__
File ".../lib/python3.8/runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
File ".../lib/python3.8/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "gc_test.py", line 60, in <module>
afunc_overwrite()
File "gc_test.py", line 39, in afunc_overwrite
cwd = ClassWithDel('NotDefault')
File "gc_test.py", line 11, in __del__
traceback.print_stack()
I am NotDefault
NotDefault: In __del__
File ".../lib/python3.8/runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
File ".../lib/python3.8/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "gc_test.py", line 60, in <module>
afunc_overwrite()
File "gc_test.py", line 11, in __del__
traceback.print_stack()
Starting list overwrite...
l1: In __del__
File ".../lib/python3.8/runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
File ".../lib/python3.8/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "gc_test.py", line 62, in <module>
afunc_overwrite_list()
File "gc_test.py", line 51, in afunc_overwrite_list
l = l2
File "gc_test.py", line 11, in __del__
traceback.print_stack()
l2: In __del__
File ".../lib/python3.8/runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
File ".../lib/python3.8/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "gc_test.py", line 62, in <module>
afunc_overwrite_list()
File "gc_test.py", line 11, in __del__
traceback.print_stack()