[Python-Dev] A surprising case of cyclic trash

Tim Peters tim_one@email.msn.com
Fri, 31 Mar 2000 19:55:54 -0500


This comes (indirectly) from a user of my doctest.py, who noticed that
sometimes tempfiles created by his docstring tests got cleaned up (via
__del__), but other times not.  Here's a hard-won self-contained program
illustrating the true cause:

class Critical:
    count = 0
    def __init__(self):
        Critical.count = Critical.count + 1
        self.id = Critical.count
        print "acquiring Critical", self.id
    def __del__(self):
        print "releasing Critical", self.id

good = "temp = Critical()\n"
bad = "def f(): pass\n" + good

basedict = {"Critical": Critical}

for test in good, bad, good:
    print "\nStarting test case:"
    print test
    exec compile(test, "<string>", "exec") in basedict.copy()

And here's output:

D:\Python>python misc\doccyc.py

Starting test case:
temp = Critical()

acquiring Critical 1
releasing Critical 1

Starting test case:
def f(): pass
temp = Critical()

acquiring Critical 2

Starting test case:
temp = Critical()

acquiring Critical 3
releasing Critical 3

D:\Python>


That is, in the "bad" case, which differs from the "good" case merely in
defining an unreferenced function, temp.__del__ not only doesn't get
executed "when expected", it never gets executed at all.

This appears to be due to a cycle between the function object and the
anonymous dict passed to exec, causing the entire dict to become immortal,
thus making "temp" immortal too.

I can fiddle the doctest framework to manually nuke the temp dict it creates
for execution context; the same kind of leak likely occurs in any exec'ed
string that contains a function defn.

For future reference, note that the finalizer in question belongs to an
object not itself in a cycle, it's an object reachable only from a dead
cycle.

the-users-don't-stand-a-chance<wink>-ly y'rs  - tim