基于try / finally + yield的Python析构函数?

时间:2014-01-23 00:30:11

标签: python garbage-collection generator destructor yield

我一直在测试受此http://docs.python.org/2/library/contextlib.html启发的肮脏黑客攻击。 主要思想是将try / finally想法带入类级别,并获得可靠而简单的类析构函数。

class Foo():
  def __init__(self):
    self.__res_mgr__ = self.__acquire_resources__()
    self.__res_mgr__.next()

  def __acquire_resources__(self):
    try:
      # Acquire some resources here
      print "Initialize"
      self.f = 1
      yield
    finally:
      # Release the resources here
      print "Releasing Resources"
      self.f = 0

f = Foo()
print "testing resources"
print f.f

但它总是给我:

Initialize
testing resources
1

永远不会“释放资源”。我的希望是:

  

从Python 2.5版开始,现在允许使用yield语句   尝试一个尝试的子句...终于构造。如果发电机不是   在最终确定之前恢复(通过达到零参考计数或   通过垃圾收集),generator-iterator的close()方法   将被调用,允许任何挂起的finally子句执行。 Source link

但似乎当类成员与类一起被垃圾收集时,它们的引用计数不会减少,因此结果生成器close(),因此最终永远不会被调用。至于报价的第二部分

  

“或通过垃圾收集”

我只是不知道为什么不是这样。有机会让这个乌托邦有效吗? :)

BTW这适用于模块级别:

def f():
  try:
    print "ack"
    yield
  finally:
    print "release"

a = f()
a.next()
print "testing"

输出将如我所料:

ack
testing
release

注意:在我的任务中,我无法使用WITH管理器,因为我在线程的end_callback中释放资源(它将不在任何WITH中)。所以我希望得到一个可靠的析构函数,用于因某些原因不会调用回调的情况

1 个答案:

答案 0 :(得分:3)

您遇到的问题是由您的生成器上定义的引用周期和隐式__del__引起的(它是如此隐式,CPython doesn't actually show __del__ when you introspect, because only the C level tp_del exists, no Python-visible __del__ is created)。基本上,当生成器内部有yield时:

  • try块,或等效
  • with阻止

它有一个隐含的__del__ - 类似的实现。在Python 3.3及更早版本中,如果引用循环包含一个对象,其类实现__del__(技术上,CPython中有tp_del),除非循环被手动破坏,循环垃圾收集器无法清除它,并将其粘贴在gc.garbageimport gc以获取访问权限),因为它不知道必须首先收集哪些对象(如果有的话)才能“很好地”清理。

由于您的班级__acquire_resources__(self)包含对实例self的引用,因此您会形成一个参考周期:

self - > self.__res_mgr__(生成器对象) - >生成器框架(引用包括的本地) - > self

由于此参考周期以及生成器中包含try / finally(创建tp_del等效于__del__)的事实,循环无法收集,除非你手动推进finally(这违背了整个目的),否则你的self.__res_mgr__块永远不会被执行。

您的实验恰好会自动显示此问题,因为引用周期是隐式/自动的,但是周期中的对象具有__del__类的任何意外引用周期都会触发相同的问题,所以即使您刚做了:

class Foo():
    def __init__(self):
        # Acquire some resources here
        print "Initialize"
        self.f = 1

    def __del__(self):
        # Release the resources here
        print "Releasing Resources"
        self.f = 0

如果所涉及的“资源”可能导致带有Foo实例的参考周期,则您会遇到同样的问题。

此处的解决方案是以下一项或两项:

  1. 制作班级a context manager,以便用户提供确定性最终确定所需的信息(通过使用with块),并为{close提供明确的清理方法(例如with) 1}}块不可行(通过自己的资源管理清理的另一个对象的状态的一部分)。这也是在大多数非CPython解释器上提供确定性清理的唯一方法,其中从未使用引用计数语义(因此所有终结器都被称为非确定性,如果有的话)
  2. 转到Python 3.4或更高版本,其中PEP 442使用无法收集的循环垃圾解决问题(技术上仍然可以在CPython上生成此类循环,但仅限于继续使用tp_del的第三方扩展而不是更新以使用允许正确清理循环垃圾的tp_finalize槽。它仍然是非确定性的清理(如果存在参考周期,你正在等待循环gc运行,有时),但它可能,其中3.4之前的这种循环垃圾不能完全清理。