【问题标题】:Garbage collect a class with a reference to its instance?垃圾收集一个引用其实例的类?
【发布时间】:2013-03-09 22:41:27
【问题描述】:

考虑这段代码sn-p:

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

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

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

  1. 该类有一个没有参数的构造函数,它给出了一个“匿名实例”(我的定义)
  2. 该类具有一些特定于行为的方法(此处为something)。
  3. 类的实例可以在未定义的时间内保持活动状态。

它不假设任何关于:

  1. 构造一个对象需要多长时间
  2. 类是否实现del或其他魔术方法
  3. 预计班级会持续多久

此外,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 方法,而且我不知道如何处理覆盖 __new__ 的类。

【问题讨论】:

  • 让什么工作,究竟是什么?你想达到什么目的?完成实例后,为什么不直接删除属性Foo.bar
  • 假设引用是由外部代码添加的,它无法控制实例的生命周期。我在编写可以应用于任何类的描述符时首先偶然发现了这个问题,包括Foo
  • 所以,如果我理解的话,在您的真实案例中,您有一个本地类定义,它在某些时候超出了范围,需要被释放。但在其生命周期中,一些外部代码会为其添加属性。你能解释一下如何以及为什么?
  • @shx2 我添加了一个几乎真实的代码示例(在第一个水平中断之后)并进行了解释。在我现实生活中的问题中,我限制了合同以避免这种情况,但我留下了我的问题。

标签: python garbage-collection


【解决方案1】:

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

在您的情况下,我没有发现循环创建方式有任何异常,也就是说,您应该采用标准方法来避免一般问题。

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

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

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

【讨论】:

  • 很抱歉,这并不是我的问题的真正答案,而是一种避免问题的方法。这对社区仍然有价值,但它没有解决问题的核心。
  • 无论如何,我认为这个特定的参考周期问题有一些内在的特殊性。一些引用循环可以通过正确使用弱引用来打破。这不能在这里完成,因为对象显然必须包含对其自身类的强引用。
  • @DavideR.: 是的,但是为什么你的描述符必须给类一个对对象的强引用呢?为什么不将这些信息弱存储在其他地方(不在课堂上)?
  • @DavidR:此答案中的第 3 点和第 4 点是解决问题的方法。
  • 不,因为我不知道何时适合del(给出问题中的示例)。了解 AFAIK 的唯一方法是通过 weakref 回调。
【解决方案2】:

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

编辑:我认为您对在实例存在时收集类的可能性存在误解。 Python 中的方法存储在类上,而不是实例上(除非有特殊技巧)。如果你有一个 Class 类的对象 obj,并且你允许在 obj 仍然存在时对 Class 进行垃圾回收,那么在该对象上调用方法 obj.meth() 将失败,因为该方法会消失和班级一起。这就是为什么你唯一的选择是削弱你的 class->obj 引用;即使你可以让对象弱引用它们的类,它所要做的就是在弱点“生效”时破坏类(即,如果在实例仍然存在时收集类)。

【讨论】:

  • 是的,我考虑过这种可能性。这里的问题是,如果可以动态创建类(如示例中所示),则字典将变得拥挤。也许有一个死引用修剪步骤。现实生活中的问题?可能不是,但我不想受到语言实现细节的限制。 :)
  • 哦,对不起,我错过了您回答中的一个要点。需要对实例的强引用才能使其保持活动状态。否则我不会首先存储参考。
  • @DavideR.:“拥挤”?你是说你担心字典会变得太大吗?通过设置cls._my_instance,你所做的就是在类的字典中设置一个键,所以没有办法解决这个问题。
  • @DavideR.:我想我还没有真正理解你的问题。如果您坚持通过引用保持对象处于活动状态,那么您将无法避免引用循环。解决方案是让对象保持活动状态。从您的示例中,我认为您不需要让它保持活力。如果您只是将其保留用于缓存,那么让对象被收集只是在缓存死时使缓存无效,这似乎非常合理。
  • @DavideR.:我添加了更多解释。因为方法存储在类上,而不是实例上,所以您没有希望能够在实例还活着的时候对类进行垃圾收集。实例需要对该类的强引用,以确保它可以访问其方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-04-20
  • 2020-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-06
  • 1970-01-01
相关资源
最近更新 更多