Python 的迭代器解包(star unpacking)是如何实现的(或者,解包自定义迭代器涉及哪些神奇的方法?)

Ton*_* Wu 7 python iterator magic-methods python-3.x iterable-unpacking

我正在编写一个定义__iter__and的类__len__,其中 的值__len__取决于__iter__. 我得到了一个有趣的RecursionError.

语言版本:Python 3.8.6、3.7.6。 示例仅用于说明错误。

在以下示例中,Iter.__len__()尝试解包self,将结果存储在 a 中list,然后尝试调用该list.__len__()列表上的内置函数以获取长度。

>>> class Iter:
...     def __iter__(self):
...         return range(5).__iter__()
...     def __len__(self):
...         return list.__len__([*self])
...
>>> len(Iter())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __len__
  File "<stdin>", line 5, in __len__
  File "<stdin>", line 5, in __len__
  [Previous line repeated 993 more times]
  File "<stdin>", line 3, in __iter__
RecursionError: maximum recursion depth exceeded in comparison
Run Code Online (Sandbox Code Playgroud)

但是,如果我将类定义Iter如下,其中Iter.__len__()显式解包由 返回的迭代器Iter.__iter__()

>>> class Iter:
...     def __iter__(self):
...         return range(5).__iter__()
...     def __len__(self):
...         return list.__len__([*self.__iter__()])
...
>>> len(Iter())
5
Run Code Online (Sandbox Code Playgroud)

然后没有错误。

从回溯来看,似乎list.__len__()正在尝试调用Iter.__len__(),甚至认为提供的参数应该已经是本机list对象。的原因是RecursionError什么?


根据schwobaseggl,使用set而不是list不会导致RecursionError

>>> class Iter:
...     def __iter__(self):
...         return range(5).__iter__()
...     def __len__(self):
...         return set.__len__({*self})
...
>>> len(Iter())
5
Run Code Online (Sandbox Code Playgroud)

sch*_*ggl 7

它与解包无关,但与不同集合类型的实现有关,特别是它们的构造函数。

[*iterable]  # list
(*iterable,) # tuple
{*iterable}  # set
Run Code Online (Sandbox Code Playgroud)

所有触发调用它们的类各自的构造函数。

当前的 C 实现list(iterable)来看:

list___init___impl(PyListObject *self, PyObject *iterable) {
    /* ... */
    if (iterable != NULL) {
        if (_PyObject_HasLen(iterable)) {
            Py_ssize_t iter_len = PyObject_Size(iterable);
            if (iter_len == -1) {
                if (!PyErr_ExceptionMatches(PyExc_TypeError)) {
                    return -1;
                }
                PyErr_Clear();
            }
            if (iter_len > 0 && self->ob_item == NULL
                && list_preallocate_exact(self, iter_len)) {
                return -1;
            }
        }
        PyObject *rv = list_extend(self, iterable);
        /* ... */
}
Run Code Online (Sandbox Code Playgroud)

可以看出(即使像我这样有限的 C 知识),会测试迭代器的大小,以分配正确数量的内存,这是触发__len__对传递的迭代器的调用的原因。

不出所料,可以验证set没有这样的事情。毕竟,传递的可迭代对象的大小与结果集的大小之间的关系远不如列表或元组那么直接。例如,考虑set([1] * 10**5). 使用传递列表的大小信息为集合分配内存是愚蠢的。

在旁注中,正如本网站上的评论和许多其他问题/答案所指出的那样(例如这里):
如果您想确定 a 的长度iterable,有比收集更多(主要是节省空间)的方法将所有项目放入一个Sized集合中,例如:

def __len__(self):
    return sum(1 for _ in self)
Run Code Online (Sandbox Code Playgroud)