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

2
  • I tried both of your codes in CPython 3.6.5 and they both produce the same output of Z deleted.. Commented Sep 4, 2018 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? Commented Sep 5, 2018 at 2:18

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

4 Comments

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.
@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.
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__.
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).

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.