Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Object not freed after an exception raise in Python 2.7

I am using Python 2.7 and trying to have a clean memory (as I am writing a small server). I am facing an issue where the object of the last raise are still kept in the garbage collector (and then __ del __ is not called after the first try/except).

Here is a small example:

import gc

class A(object):
    def raiser(self):
        0/0 # will raise an exception
a = A()

try:
    a.raiser()
except:
    pass

a = None # should release the object from memory

gc.collect() # just to be sure, but doesn't do anything


print '1. Nbr of instance of A in gc : '
print len([o for o in gc.get_objects() if isinstance(o, A)]) # get 1 but expected 1


try:
    0/0
except:
    pass
print '2. Nbr of instance of A in gc : '
print len([o for o in gc.get_objects() if isinstance(o, A)]) # get 0 (finally)

and this returns:

1. Nbr of instance of A in gc : 
1
2. Nbr of instance of A in gc : 
0

while I was waiting to have 0 for both. Where Is the A instance stored ?

Thanks a lot Alex

like image 637
user1143523 Avatar asked Jan 11 '12 16:01

user1143523


2 Answers

That instance is stored (at least) in the interrupted frame of the raiser function, as we can check using gc.get_referrers:

import gc
import inspect

class A(object):
    def raiser(self):
        print inspect.currentframe()
        0/0
a = A()

try:
    a.raiser()
except:
    pass

a = None # should release the object from memory
gc.collect() # just to be sure, but doesn't do anything

print 'b. Nbr of instance of A in gc : '
print [map(lambda s: str(s)[:64], gc.get_referrers(o)) for o in gc.get_objects() if isinstance(o, A)]

try:
    0/0
except:
    pass

print '---'

print 'c. Nbr of instance of A in gc : '
print [map(lambda s: str(s)[:64], gc.get_referrers(o)) for o in gc.get_objects() if isinstance(o, A)]

This prints:

<frame object at 0x239fa70>
---
b. Nbr of instance of A in gc : 
[["[[], ('Return a new Arguments object replacing specified fields ",
  "{'A': <class '__main__.A'>, 'a': None, '__builtins__': <module '",
  '<frame object at 0x239fa70>']]
---
c. Nbr of instance of A in gc : 
[]

Note the last object is the same as the frame of raiser. This also means you'll get the same result too if you just write

try:
    A().raiser()
except:
    pass

We could do the same trick again to see what is holding the frame object:

class A(object):
    def raiser(self):
        0/0

try:
    A().raiser()
except:
    pass

print [(map(lambda x: str(x)[:64], gc.get_referrers(o)),  # Print the referrers
        map(type, gc.get_referents(o)))  # Check if it's the frame holding an A
           for o in gc.get_objects()
           if inspect.isframe(o)]

The result is

[(['<traceback object at 0x7f07774a3bd8>',
   '[<function getblock at 0x7f0777462cf8>, <function getsourcelines',
   "{'A': <class '__main__.A'>, '__builtins__': <module '__builtin__"
  ], [<type 'frame'>, <type 'code'>, <type 'dict'>, <type 'dict'>,
      <class '__main__.A'>]),
 (['<frame object at 0xe3d3c0>',
   '<traceback object at 0x7f07774a3f38>',
   '[<function getblock at 0x7f0777462cf8>, <function getsourcelines',
   "{'A': <class '__main__.A'>, '__builtins__': <module '__builtin__"
  ], [<type 'code'>, <type 'dict'>, <type 'dict'>, <type 'dict'>,
      <type 'NoneType'>])]

So we see that the frame is at least held by a traceback object. We can find info about the traceback object in the traceback module, which mentions:

The module uses traceback objects — this is the object type that is stored in the variables sys.exc_traceback (deprecated) and sys.last_traceback and returned as the third item from sys.exc_info().

Which means these sys variables may be the one that is holding the frame alive. Indeed, if we call sys.exc_clear() to clear the exception info, the instance will be deallocated:

import gc
import sys

class A(object):
    def raiser(self):
        0/0

try:
    A().raiser()
except:
    pass

print len([o for o in gc.get_objects() if isinstance(o, A)])  # prints 1
sys.exc_clear()
print len([o for o in gc.get_objects() if isinstance(o, A)])  # prints 0
like image 62
kennytm Avatar answered Nov 09 '22 06:11

kennytm


After a little investigation. If you import sys and put

...
a = None

print sys.exc_info()

#sys.exc_clear() # if you add this your A instance will get gc as expected 

gc.collect()
...

you'll notice that the interpreter still holds a reference to the ZeroDivisionError trown inside try...catch even though the code is already outside. Since exception hold references to frames where they are thrown (if only to print traceback), your A instance still has non-zero refcount.

Once another exception is thrown (and handled) interpreter drops the reference to the first one and collects both the exception and objects connected to it.

like image 4
soulcheck Avatar answered Nov 09 '22 05:11

soulcheck