垃圾收集一个引用其实例的类?

时间:2013-03-20 17:13:36

标签: python garbage-collection

请考虑以下代码段:

import gc
from weakref import ref


def leak_class(create_ref):
    class Foo(object):
        # make cycle non-garbage collectable
        def __del__(self):
            pass

    if create_ref:
        # create a strong reference cycle
        Foo.bar = Foo()
    return ref(Foo)


# without reference cycle
r = leak_class(False)
gc.collect()
print r() # prints None

# with reference cycle
r = leak_class(True)
gc.collect()
print r() # prints <class '__main__.Foo'>

它创建了一个无法收集的引用循环,因为引用的实例具有__del__方法。循环在此处创建:

# create a strong reference cycle
Foo.bar = Foo()

这只是一个概念证明,可以通过一些外部代码,描述符或任何东西添加引用。如果您不清楚这一点,请记住每个对象都保留对其类的引用:

  +-------------+             +--------------------+
  |             |  Foo.bar    |                    |
  | Foo (class) +------------>| foo (Foo instance) |
  |             |             |                    |
  +-------------+             +----------+---------+
        ^                                |
        |         foo.__class__          |
        +--------------------------------+

如果我可以保证Foo.bar只能从Foo访问,那么这个循环就没有必要,因为理论上该实例只能保留对其类的弱引用。

你能想出一种实用的方法来使这项工作没有泄漏吗?


有些人在问为什么外部代码会修改一个类而不能控制它的生命周期,请考虑这个例子,类似于我正在努力的现实生活中的例子:

class Descriptor(object):
    def __get__(self, obj, kls=None):
        if obj is None:
            try:
                obj = kls._my_instance
            except AttributeError:
                obj = kls()
                kls._my_instance = obj
        return obj.something()


# usage example #
class Example(object):
    foo = Descriptor()

    def something(self):
        return 100


print Example.foo

在此代码中,只有Descriptor(a non-data descriptor)是我正在实施的API的一部分。 Example类是如何使用描述符的示例。

为什么描述符存储对类本身内部实例的引用?基本上用于缓存目的。 Descriptor需要与实现者签订此合同:它将在任何类中使用,假设

  1. 该类有一个没有args的构造函数,它给出了一个“匿名实例”(我的定义)
  2. 该类有一些特定于行为的方法(something)。
  3. 该类的实例可以在不确定的时间内保持活动状态。
  4. 它没有假设任何关于:

    1. 构建对象需要多长时间
    2. 该类是否实现 del 或其他魔术方法
    3. 预计课程将持续多长时间
    4. 此外,API旨在避免类实现者的任何额外负载。我本可以将缓存对象的责任移交给实现者,但我想要一个标准的行为。

      实际上有一个简单的解决方案:使用默认行为来缓存实例(就像在此代码中一样),但如果必须实现__del__,则允许实现者覆盖它。

      当然,如果我们假设类状态在调用之间保留,那就不会那么简单了。


      作为一个起点,我编写了一个“弱对象”,object的一个实现只保留了对它的类的弱引用:

      from weakref import proxy
      
      def make_proxy(strong_kls):
          kls = proxy(strong_kls)
          class WeakObject(object):
              def __getattribute__(self, name):
                  try:
                      attr = kls.__dict__[name]
                  except KeyError:
                      raise AttributeError(name)
      
                  try:
                      return attr.__get__(self, kls)
                  except AttributeError:
                      return attr
              def __setattr__(self, name, value):
                  # TODO: implement...
                  pass
          return WeakObject
      
      Foo.bar = make_proxy(Foo)()
      

      它似乎适用于有限数量的用例,但我必须重新实现整套object方法,而且我不知道如何处理覆盖{{1}的类}。

2 个答案:

答案 0 :(得分:2)

对于您的示例,为什么不将_my_instance存储在描述符类的dict中,而不是存放描述符的类?您可以在该dict中使用weakref或WeakValueDictionary,这样当对象消失时,dict将丢失其引用,并且描述符将在下次访问时创建一个新的。

编辑:我认为你误解了在实例存在时收集课程的可能性。 Python中的方法存储在类中,而不是实例上(禁止特殊的技巧)。如果您有一个obj类的对象Class,并且您在Class仍然存在时允许obj进行垃圾回收,那么在obj.meth()上调用方法{{1}}对象会失败,因为该方法会随着类一起消失。这就是为什么你唯一的选择是弱化你的class-&gt; obj引用;即使你可以让对象弱引用他们的类,如果弱点“生效”(即,如果在实例仍然存在的情况下收集了类),它所做的就是打破类。

答案 1 :(得分:1)

您遇到的问题只是一般的ref-cycle-with - __del__问题的一个特例。

在你的案例中我没有看到创建周期的方式有什么异常,也就是说,你应该采用避免一般问题的标准方法。

我认为实现和使用weak object很难做到正确,您仍然需要记住在定义__del__的所有地方使用它。这听起来不是最好的方法。

相反,您应该尝试以下方法:

  1. 考虑不在班级中定义__del__(推荐)
  2. 在定义__del__的类中,避免引用循环(通常,可能很难/不可能确保在代码中的任何位置都不会创建循环。在您的情况下,似乎您希望循环存在)
  3. 使用del明确地打破周期(如果您的代码中有适当的点可以执行此操作)
  4. 扫描gc.garbage列表,并明确断开参考周期(使用del