微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

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

我正在编写一个定义 __iter____len__ 的类,其中 __len__ 的值取决于 __iter__ 返回的迭代器。我得到了一个有趣的 RecursionError

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

在以下示例中,Iter.__len__() 尝试解包 self,将结果存储在 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>",in __len__
  [PrevIoUs line repeated 993 more times]
  File "<stdin>",line 3,in __iter__
RecursionError: maximum recursion depth exceeded in comparison

但是,如果我将类 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

那么就没有错误了。

从回溯来看,似乎 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

解决方法

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

[*iterable]  # list
(*iterable,) # tuple
{*iterable}  # set

所有触发对其类的各自构造函数的调用。

来自current C implementation for 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);
        /* ... */
}

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

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

附带说明,正如本网站上的评论和许多其他问题/答案(例如 here)中所指出的:
如果您想确定 iterable 的长度,有比将所有项目收集到 Sized 集合中更多(主要是节省空间)的方法,例如:

def __len__(self):
    return sum(1 for _ in self)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。