Mailing List Archive

Module to detect/report Python object "leaks"
We have written a class to track Python objects and detect when objects are
NOT freed. To do this we have some standards. One is that all objects have
a Destroy() method that eliminates all references to any other objects and
does the cleanup necessary to be deleted (also handles circular destruction
problems). We created the class Debug to track these objects. All base
class objects call two methods; LogInit which holds a reference to the
object and LogDestroy that removes the reference from its internal
dictionary. A dictionary is used to track the objects for reasons of speed
and also that we have a stack trace we can optionally generate and hold for
reporting memory leaks. We don't want or allow for LogInit and LogDestroy
to be called multiple times for the same object. When the program ends it
calls the LogReport method to print out undestroyed objects.

Sample use:

class BaseClass
def __init__(self):
if __debug__:
Globals.debug.LogInit(self)
# rest of initialization

def Destroy(self):
if __debug__:
Globals.debug.LogDestroy(self)
# rest of initialization

We use the __debug__ flag to optionally compile out the calling debug code.

This code works and allowed us to track down a number of memory problems
with Python objects. I am sending out this code for comments, advice,
optimizations (takes up a lot of memory and time to execute) and
enhancements.

David Stidolph and Jason Asbahr
Origin

------------------- Python Code follows ---------------------------

#Debug Module
#
#This class is designed to track Python objects between their __init__() and
Destroy() method calls.
#There are different 'levels' to the tracking. If we do it at all, we will
track the objects by
#reference using a dictionary. If we have 'holdTraceback' active, then in
LogInit we get the current
#stack, translate it into a list of strings and use that list as the value
and the object reference as
#the key in the dictionary.
#
#As a dictionary we keep only one copy and don't allow for
LogInit/LogDestroy to be called more than
#once (have setting to throw exception if it occurs). This means you put
the calls to LogInit and
#LogDestroy in the base class .. not the derived classes. Proper class name
of the object is used
#by __class__.__name__.
#
#TODO:
# Fix output for MSVC IDE to use absolute paths to Python files for
double-clicking
# Add Python object for output in LogReport to allow output to file or
sending across net
# Add print formatted output to wrap output (allow to send across
net).
# Optimize use of stack trace to reduce memory requirements.

from sys import exc_info
from traceback import extract_stack, format_list

class Debug:
"""Only one instance allowed"""

__refDebugCount = 0 # class variable

def __init__(self, trackObjects = 1, holdTraceback = 1,
raiseExceptionOnDuplicate = 0, displayInit = 0, displayDestroy = 0):
if Debug.__refDebugCount != 0:
raise "Only one copy of debug allowed"

if __debug__:
self.trackObjects = trackObjects
self.holdTraceback = holdTraceback
self.raiseExceptionOnDuplicate = raiseExceptionOnDuplicate
self.displayInit = displayInit
self.displayDestroy = displayDestroy
else:
self.trackObjects = 0 #if we don't have debug mode, then
it doesn't matter what
self.holdTraceback = 0 #settings we use since nothing
SHOULD be calling us.
self.raiseExceptionOnDuplicate = 0 #Just in case we ignore and set
for nothing.
self.displayInit = 0
self.displayDestroy = 0

self.activeObjects = {} #clear dictionary
Debug.__refDebugCount = 1 #we are initialized

#Called from base object __init__(self) routines
def LogInit(self,object):
if self.trackObjects:
if self.displayInit:
#display that the object has been constructed and its __init__ has
been called
print 'MEM: ',repr(object),'of class',object.__class__.__name__,'is
being initialized'

#traceback of stack is generated using extract_stack() and used as the
value in the
#dictionary with the object reference used as the key.
if self.holdTraceback:
#We check for the object already being in the dictionary ONLY if we
will throw an
#exception, silent otherwise
if self.raiseExceptionOnDuplicate:
if self.activeObjects.haskey(object):
raise "Object has already called LogInit"
tb = extract_stack()
self.activeObjects[object] = tb
else:
self.activeObjects[object] = 1

#Called at program end to report on undestroyed Python objects.
def LogReport(self):
# For formatting that Visual Studio can recognize, we're adjusting
# output to look like:
# c:\test\gagob.cpp(236) : error C2065: 'Clear' : undeclared identifier
if self.trackObjects:
if len(self.activeObjects) > 0:
print
print 'MEM: UnDestroyed Python Objects Report:'
print ' %s python objects total.' % len(self.activeObjects)
print
#Get Object reference and stack traceback for each undestroyed
Python object
for object,value in self.activeObjects.items():
print 'MEM: ',repr(object), ' of ',object.__class__.__name__
if self.holdTraceback:
for tuple in value: #[:-2]: #-2 is to eliminate report of call
to LogInit()
print str(tuple[0])+'('+str(tuple[1])+'): in
'+str(tuple[2])+','
print ' '+str(tuple[3])
print #blank line between
else:
print 'MEM: Congratulations! No Python object leaks!'

#Called from each base object Destroy() method.
def LogDestroy(self,object):
if self.trackObjects:
if self.displayDestroy:
print 'MEM: ',repr(object),'of class',object.__class__.__name__,'is
being Destroyed'
#We wrap the destruction of the key inside a try..except block in case
the LogDestroy is called without
#calling the LogInit (rare) or LogDestroy is being called for a second
time (far more likely).
try:
del self.activeObjects[object]
except:
if self.raiseExceptionOnDuplicate:
raise "LogDestroy called twice on "+object.__class__.__name__+".
See if base class doing LogInit/LogDestroy (only one should)"
else:
print "LogDestroy called twice on ",object.__class__.__name__,".
See if base class doing LogInit/LogDestroy (only one should)"

def Destroy(self):
Debug.__refDebugCount = None
self.activeObjects = None