David Anderson
David Anderson

Reputation: 1158

Destructor not being called in a Python script

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

Upvotes: 0

Views: 1416

Answers (1)

ShadowRanger
ShadowRanger

Reputation: 155363

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.

Upvotes: 1

Related Questions