izip_longest in itertools: как работает рассылка IndexError внутри итератора? - PullRequest
4 голосов
/ 12 сентября 2011

В этот вопрос @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

Когда я пытался понять, как это работает, я наткнулся на вопрос: «Что если IndexError будет вызвано внутри одного из тех итераторов, которые отправляются на izip_longest в качестве параметров?».

Тогда я написал тестовый код:

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

Итак, ясно видно, что код izip_longes из itertools действительно распространял IndexError исключение (как мне кажется, должно), но izip_longes_from_docs "проглотил" IndexError исключение, так как он воспринимался как сигнал от sentinel прекратить итерации.

У меня вопрос, как они работали вокруг распространения IndexError в коде в модуле itertools?

1 Ответ

3 голосов
/ 13 сентября 2011

in 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
...