itertools中的izip_longest:迭代器中引发IndexError是如何工作的?
在这个问题中,@lazyr问到以下代码的izip_longest
迭代器是怎么工作的,代码来自这里:
def izip_longest_from_docs(*args, **kwds):
# izip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
fillvalue = kwds.get('fillvalue')
def sentinel(counter = ([fillvalue]*(len(args)-1)).pop):
yield counter() # yields the fillvalue, or raises IndexError
fillers = repeat(fillvalue)
iters = [chain(it, sentinel(), fillers) for it in args]
try:
for tup in izip(*iters):
yield tup
except IndexError:
pass
当我试图理解它是如何工作的时,我碰到了一个问题:“如果在传给izip_longest
的那些迭代器中抛出了IndexError
,会发生什么?”
然后我写了一些测试代码:
from itertools import izip_longest, repeat, chain, izip
def izip_longest_from_docs(*args, **kwds):
# The code is exactly the same as shown above
....
def gen1():
for i in range(5):
yield i
def gen2():
for i in range(10):
if i==8:
raise IndexError #simulation IndexError raised inside the iterator
yield i
for i in izip_longest_from_docs(gen1(),gen2(), fillvalue = '-'):
print('{i[0]} {i[1]}'.format(**locals()))
print('\n')
for i in izip_longest(gen1(),gen2(), fillvalue = '-'):
print('{i[0]} {i[1]}'.format(**locals()))
结果发现,itertools
模块中的函数和izip_longest_from_docs
的工作方式是不同的。
上面代码的输出结果是:
>>>
0 0
1 1
2 2
3 3
4 4
- 5
- 6
- 7
0 0
1 1
2 2
3 3
4 4
- 5
- 6
- 7
Traceback (most recent call last):
File "C:/..., line 31, in <module>
for i in izip_longest(gen1(),gen2(), fillvalue = '-'):
File "C:/... test_IndexError_inside iterator.py", line 23, in gen2
raise IndexError
IndexError
所以,很明显,itertools
中的izip_longest
代码确实传播了IndexError
异常(我认为这是应该的),但是izip_longest_from_docs
却“吞掉”了IndexError
异常,因为它把这个异常当作来自sentinel
的信号,停止了迭代。
我的问题是,他们是如何在itertools
模块的代码中处理IndexError
传播的?
1 个回答
3
在 izip_longest_next
这个函数里,代码中提到的 izip_longest
并没有使用哨兵值。
相反,CPython 通过一个计数器来跟踪还有多少个迭代器是活跃的,当活跃的迭代器数量变为零时,就停止迭代。
如果发生错误,它会像没有任何活跃的迭代器一样结束迭代,并允许错误继续传播。
代码如下:
item = PyIter_Next(it);
if (item == NULL) {
lz->numactive -= 1;
if (lz->numactive == 0 || PyErr_Occurred()) {
lz->numactive = 0;
Py_DECREF(result);
return NULL;
} else {
Py_INCREF(lz->fillvalue);
item = lz->fillvalue;
PyTuple_SET_ITEM(lz->ittuple, i, NULL);
Py_DECREF(it);
}
}
我看到的最简单的解决方案是:
def izip_longest_modified(*args, **kwds):
# izip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
fillvalue = kwds.get('fillvalue')
class LongestExhausted(Exception):
pass
def sentinel(counter = ([fillvalue]*(len(args)-1)).pop):
try:
yield counter() # yields the fillvalue, or raises IndexError
except:
raise LongestExhausted
fillers = repeat(fillvalue)
iters = [chain(it, sentinel(), fillers) for it in args]
try:
for tup in izip(*iters):
yield tup
except LongestExhausted:
pass