Python的迭代器解包(star unpacking)是如何实现的(或者说,自定义迭代器的解包有哪些神奇的方法?)
How is Python's iterator unpacking (star unpacking) implemented (or, what magic methods are involved in unpacking a custom iterator?)
我正在写一个定义 __iter__
和 __len__
的 class,其中 __len__
的值取决于 __iter__
返回的迭代器。我得到了一个有趣的 RecursionError
.
语言版本:Python3.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>", 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
但是,如果我将 class 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)
。使用传递的列表的大小信息为集合分配内存是愚蠢的。
旁注,正如本网站的评论和许多其他 questions/answers 所指出的(例如 here):
如果要确定 iterable
的长度,有比将所有项目收集到 Sized
集合中更有效的方法(主要是 space-),例如:
def __len__(self):
return sum(1 for _ in self)
我正在写一个定义 __iter__
和 __len__
的 class,其中 __len__
的值取决于 __iter__
返回的迭代器。我得到了一个有趣的 RecursionError
.
语言版本:Python3.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>", 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
但是,如果我将 class 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)
。使用传递的列表的大小信息为集合分配内存是愚蠢的。
旁注,正如本网站的评论和许多其他 questions/answers 所指出的(例如 here):
如果要确定 iterable
的长度,有比将所有项目收集到 Sized
集合中更有效的方法(主要是 space-),例如:
def __len__(self):
return sum(1 for _ in self)