4

正如我们(或至少我)在此答案中了解到的那样,python 的垃圾收集器不会跟踪仅包含不可变值的简单元组,一旦发现它们永远不会参与引用循环:

>>> import gc
>>> x = (1, 2)
>>> gc.is_tracked(x)
True
>>> gc.collect()
0
>>> gc.is_tracked(x)
False

为什么namedtuple不是这种情况,它是具有命名字段的集合模块的元组的子类?

>>> import gc
>>> from collections import namedtuple
>>> foo = namedtuple('foo', ['x', 'y'])
>>> x = foo(1, 2)
>>> gc.is_tracked(x)
True
>>> gc.collect()
0
>>> gc.is_tracked(x)
True

他们的实现中是否有一些固有的东西可以防止这种情况发生,或者只是被忽视了?

4

2 回答 2

7

我能找到的唯一评论是在gcmodule.cPython 源文件中:

注意:关于不跟踪可变对象。某些类型的容器不能参与引用循环,因此不需要被垃圾收集器跟踪。取消跟踪这些对象可以降低垃圾收集的成本。但是,确定哪些对象可能未被跟踪并不是免费的,必须权衡成本与垃圾收集的好处。

何时取消跟踪容器有两种可能的策略:

  1. 创建容器时。
  2. 当垃圾收集器检查容器时。

不需要跟踪仅包含不可变对象(整数、字符串等,以及递归地包含不可变对象的元组)的元组。解释器创建了大量的元组,其中许多在垃圾收集之前将无法生存。因此,在创建时取消跟踪符合条件的元组是不值得的。

相反,除了空元组之外的所有元组在创建时都会被跟踪。在垃圾收集期间,确定是否可以不跟踪任何幸存的元组。如果一个元组的所有内容都没有被跟踪,那么它就可以被取消跟踪。在所有垃圾收集周期中检查元组是否取消跟踪。取消跟踪一个元组可能需要一个以上的周期。

只包含不可变对象的字典也不需要被跟踪。创建时不跟踪字典。如果将跟踪项插入字典(作为键或值),则字典将被跟踪。在完整的垃圾收集(所有代)期间,收集器将取消跟踪任何内容未被跟踪的字典。

该模块提供了python函数is_tracked(obj),返回对象当前的跟踪状态。后续的垃圾回收可能会改变对象的跟踪状态。在 issue 中引入了某些容器​​的 Untracking #4688,并针对 issue 改进了算法#14775

(请参阅链接问题以查看引入以允许取消跟踪的真实代码)

这个评论有点模棱两可,但是它没有说明选择“取消跟踪”哪个对象的算法适用于通用容器。这意味着代码只检查tuples ( 和dicts),而不检查它们的子类。

您可以在文件的代码中看到这一点:

/* Try to untrack all currently tracked dictionaries */
static void
untrack_dicts(PyGC_Head *head)
{
    PyGC_Head *next, *gc = head->gc.gc_next;
    while (gc != head) {
        PyObject *op = FROM_GC(gc);
        next = gc->gc.gc_next;
        if (PyDict_CheckExact(op))
            _PyDict_MaybeUntrack(op);
        gc = next;
    }
}

注意调用PyDict_CheckExact, 和:

static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
    PyGC_Head *gc = young->gc.gc_next;

  /* omissis */
            if (PyTuple_CheckExact(op)) {
                _PyTuple_MaybeUntrack(op);
            }

请注意调用PyTuple_CheckExact.

另请注意,子类tuple不必是不可变的。这意味着如果您想将此机制扩展到外部tuple并且dict您需要一个通用is_immutable函数。这将是非常昂贵的,如果可能的话,由于 Python 的动态性(例如,类的方法可能会在运行时改变,而这是不可能的,tuple因为它是一个内置类型)。因此,开发人员选择坚持少数特殊情况,只使用一些知名的内置插件。


这就是说,我相信它们也可以是特殊情况namedtuple,因为它们是非常简单的类。会有一些问题,例如当您调用namedtuple您正在创建一个类时,因此 GC 应该检查子类。这可能是以下代码的问题:

class MyTuple(namedtuple('A', 'a b')):
    # whatever code you want
    pass

因为MyTuple类不需要不可变的,所以 GC 应该检查该类是安全的直接子类namedtuple。但是我很确定这种情况有解决方法。

他们可能没有,因为namedtuples 是标准库的一部分,而不是 python 核心。也许开发人员不想让核心依赖于标准库的模块。

所以,回答你的问题:

  • 不,它们的实现中没有任何东西可以固有地阻止对namedtuples 的取消跟踪
  • 不,我相信他们并没有“简单地忽视”这一点。然而,只有 python 开发人员才能明确回答他们为什么选择不包含它们。我的猜测是,他们认为这不会为更改提供足够大的好处,并且他们不想让核心依赖于标准库。
于 2013-11-04T15:50:13.507 回答
3

@Bakunu 给出了一个很好的答案 - 接受它:-)

这里有一个亮点:没有不跟踪的噱头是“免费的”:在运行时和需要维护的棘手代码的爆炸中都有实际成本。基本 tuple 和 dict 类型被用户程序和 CPython 实现大量使用,并且通常可以取消跟踪它们。因此,对它们进行特殊处理是值得一些痛苦的,并且有益于“几乎所有”程序。虽然当然可以找到可以从 untracking namedtuples(或 ...)中受益的程序示例,但它不会使 CPython 实现或大多数用户程序受益。但这会给所有程序带来成本(gc 代码中有更多条件来询问“这是一个namedtuple吗?”等)。

请注意,所有容器对象都受益于 CPython 的“世代”循环 gc 噱头:给定容器存活的集合越多,该容器被扫描的频率就越低(因为容器被移动到“老一代”,扫描频率较低)。因此,除非容器类型大量出现(通常适用于元组,很少适用于 dicts)或容器包含大量对象(通常适用于 dicts,很少适用于元组),否则几乎没有潜在的收益。

于 2013-11-04T18:25:27.200 回答