0

Below is module which executes as I would expect.

class Z():
    def Y(self):
        return
    def __del__(self):
        print('Z deleted.')
def W(v):
    class Form:
        def X(self):
            #v.Y()
            return  
    return
def U():
    t = Z()
    W(t)
U()

Running the above module produces the following output

Z deleted.

When I remove the comment as shown below, no output is produced.

class Z():
    def Y(self):
        return
    def __del__(self):
        print('Z deleted.')
def W(v):
    class Form:
        def X(self):
            v.Y()
            return  
    return
def U():
    t = Z()
    W(t)
U()

Why is not the destructor called?

I am running this module in the following utility. The operating system is Windows 10 Pro, Version 1803, OS build 17134.165

capture

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
David Anderson
  • 1,108
  • 9
  • 17
  • I tried both of your codes in CPython 3.6.5 and they both produce the same output of `Z deleted.`. – blhsing Sep 04 '18 at 16:37
  • CPython doesn't guarantee that destructors will be called, *period*. Documentation for this is quoted in the closely related (if not duplicative) question [Why aren't destructors guaranteed to be called on interpreter exit?](https://stackoverflow.com/questions/14628486/why-arent-destructors-guaranteed-to-be-called-on-interpreter-exit) – Charles Duffy Sep 05 '18 at 02:18

1 Answers1

1

The script you wrote is creating a reference cycle in a less than obvious fashion. The non-obvious cycle is a result of all class declarations being inherently cyclic, so the simple existence of a class declaration in W means there will be some cyclic garbage. I'm not sure if this is a necessary condition of all Python interpreters, but it's definitely true of CPython's implementation (from at least 2.7 through 3.6, the interpreters I've checked).

The thing that loops in your Z instance and triggers the behavior you observe is that you use v (which is a reference to a Z instance) with closure scope when you declare Form.x as part of the class declaration. The closure scope means that as long as the class Form defined by the call to W exists, the closed upon variable, v (ultimately an instance of Z) will remain alive.

When you run a module with IDLE, it runs the module and dumps you to an interactive prompt after the code from the module has been executed, but Python is still running, so it doesn't perform any cleanup of the globals or run the cyclic GC immediately. The instance of Z will eventually be cleaned (at least on CPython 3.4+), but cyclic GC is normally run only after quite a number of allocations without matching deallocations (700 by default on my interpreters, though this is an implementation detail). But that collection may take an arbitrarily long time (there is a final cycle cleanup performed before the interpreter exits, but beyond that, there are no guarantees).

By commenting out the line referencing v, you're no longer closing on v, so the cyclic class is no longer keeping v alive, and v is cleaned up promptly (on CPython's reference counted interpreter anyway; no guarantees on Jython, PyPy, IronPython, etc.) when the last reference disappears.

If you want to force the cleanup, after running the module, you can run the following in the resulting interactive shell to force a generation 0 cleanup:

>>> import gc
>>> gc.collect(0)  # Or just gc.collect() for a full cycle collection of all generations

Or just add the same lines to the end of the script to trigger it automatically.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Your answer does not convince me that the problem was cause by cyclic references. During the execution only one object is instantiated and this object has no references to any other object. I am not saying you are wrong. I have be doing Python for only a few weeks and I probably have not learned enough to fully comprehend this part of your answer. I do agree that IDLE is interactive and problem was `gc.collect` does not automatically get called when my module finishes executing. – David Anderson Sep 05 '18 at 13:41
  • @DavidAnderson: Like I said, merely defining a class makes a reference cycle (the class itself is self-referencing). The fact that `gc.collect()` resolves the problem actually proves cyclic references were the problem; CPython is reference counted, so aside from references that are part of a reference cycle, all objects are cleaned immediately when the last reference to them disappears; if `gc.collect` cleans it, then it was part of, or referenced from, a referenced cycle, always. – ShadowRanger Sep 05 '18 at 15:26
  • If I'm tracing it correctly, the self-reference cycle in classes is due to the `__weakref__` and `__dict__` descriptors. It's harder to see with `__dict__` (because of weirdness with how `__dict__` works), but it's easy to see with `__weakref__`. Just make a trivial class, `class Foo: pass`. Then test `Foo.__weakref__.__objclass__ is Foo`, which will return `True`. So `Foo` stores a reference to its `__weakref__` `attribute`, which in turn stores a reference to `Foo` in its `__objclass__` attribute; a complete cycle. A similar issue occurs with the `__mro__`. – ShadowRanger Sep 05 '18 at 15:33
  • Point is, sure, you only made one instance of any of the classes you defined. But you also made the classes themselves, and their methods, and their closures (all of which are objects; everything is an object in Python). – ShadowRanger Sep 05 '18 at 15:37