Under python pdb when a breakpoint is reached in a __del__ method, what would the stack look like if gc is the cause?

Viewed 88

Situation: I have placed a breakpoint in a __del__ method. When the object is explicitly deleted using del, the stack is clear that the del is the reason that __del__ is called.

However, what would the stack look like if __del__ has been called due to out-of-scope/garbage collection? Would it be clear that gc is on the stack, or will it look like whatever point the code was at when gc executed?

Or is this non-sensical in some other way?

1 Answers

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()
Related