Python 2.7中异常抛出后对象未释放

5 投票
4 回答
2142 浏览
提问于 2025-04-17 10:01

我正在使用Python 2.7,想要保持内存的干净(因为我在写一个小服务器)。我遇到了一个问题,就是最后一次抛出的异常对象仍然被垃圾回收器保留着(所以在第一次尝试/异常处理后,__del__方法没有被调用)。

这里有一个小例子:

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)

这个例子返回了:

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

而我原本希望这两个值都是0。那这个A实例到底存在哪里呢?

非常感谢,

亚历克斯

4 个回答

1

在你的代码中添加一些调试信息:

import gc
gc.set_debug(gc.DEBUG_STATS)

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

print 'Tracking a:', gc.is_tracked(a)

try:
    a.raiser()
except:
    pass

a = None # should release the object from memory

print 'Tracking a:', gc.is_tracked(a)

返回结果是

1. Tracking a: True
2. Tracking a: False

这说明在执行 a = None 之后,这个对象就不再被追踪了,所以当需要空间时,它会从内存中释放掉。因此,a没有 被存储的,但Python并不觉得有必要完全去掉它(可能忽略它比从栈中清除它更省事)。

不过,用Python来解决对性能要求很高的问题其实是个 主意——因为Python的二进制文件很大,而且有很多你根本用不上的包会让它变得臃肿。为什么不试试学一点C语言呢?

4

经过一点调查,如果你导入了 sys 模块并放入

...
a = None

print sys.exc_info()

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

gc.collect()
...

你会发现,解释器仍然保持着对在 try...catch 中抛出的 ZeroDivisionError 的引用,即使代码已经执行到外面了。因为异常会保持对它们被抛出的地方的引用(至少是为了打印错误追踪信息),所以你的 A 实例仍然有非零的引用计数。

一旦抛出另一个异常(并被处理),解释器就会放弃对第一个异常的引用,并清理掉这个异常以及与之相关的对象。

7

这个实例至少存储在raiser函数的中断帧中,我们可以通过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)]

这会打印出:

<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 : 
[]

注意最后一个对象和raiser的帧是一样的。这也意味着如果你直接写的话,结果也是一样的:

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

我们可以再用同样的方法来看看是什么在持有这个帧对象:

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)]

结果是:

[(['<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'>])]

所以我们看到这个帧至少是被一个traceback对象持有的。我们可以在traceback模块中找到关于traceback对象的信息,里面提到:

这个模块使用traceback对象——这就是存储在sys.exc_traceback(已弃用)和sys.last_traceback变量中的对象类型,并作为sys.exc_info()的第三个返回值。

这意味着这些sys变量可能是保持帧存活的原因。实际上,如果我们调用sys.exc_clear()来清除异常信息,这个实例就会被释放:

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

撰写回答