Почему исключения в генераторе Python не пойманы?
У меня есть следующий экспериментальный код, функция которого аналогична встроенному zip
. То, что он пытается сделать, должно было быть простым и ясным, пытаясь вернуть заархивированные кортежи по одному, пока не произойдет IndexError
, когда мы остановим генератор.
def my_zip(*args):
i = 0
while True:
try:
yield (arg[i] for arg in args)
except IndexError:
raise StopIteration
i += 1
Однако, когда я попытался выполнить следующий код, IndexError
не был пойман, а вместо него был добавлен генератором:
gen = my_zip([1,2], ['a','b'])
print(list(next(gen)))
print(list(next(gen)))
print(list(next(gen)))
IndexError Traceback (most recent call last)
I:\Software\WinPython-32bit-3.4.2.4\python-3.4.2\my\temp2.py in <module>()
12 print(list(next(gen)))
13 print(list(next(gen)))
---> 14 print(list(next(gen)))
I:\Software\WinPython-32bit-3.4.2.4\python-3.4.2\my\temp2.py in <genexpr>(.0)
3 while True:
4 try:
----> 5 yield (arg[i] for arg in args)
6 except IndexError:
7 raise StopIteration
IndexError: list index out of range
Почему это происходит?
Изменить:
Спасибо @thefourtheye за предоставление приятного объяснения того, что происходит выше. Теперь возникает другая проблема:
list(my_zip([1,2], ['a','b']))
Эта строка никогда не возвращается и, кажется, повесит машину. Что происходит сейчас?
Ответы
Ответ 1
yield
дает объект-генератор каждый раз, и когда генераторы были созданы, проблем не было вообще. Вот почему try...except
в my_zip
ничего не ловит. В третий раз, когда вы его выполнили,
list(arg[2] for arg in args)
вот так оно сводилось к (более упрощенному для нашего понимания) и теперь, внимательно наблюдайте, list
выполняет итерацию генератора, а не фактический генератор my_zip
. Теперь list
вызывает next
объекта-генератора и оценивается arg[2]
, только чтобы найти, что 2
не является допустимым индексом для arg
(который в этом случае равен [1, 2]
), поэтому IndexError
, и list
не справляется с этим (у него нет причин справляться с этим в любом случае), и поэтому он терпит неудачу.
Как и в случае редактирования,
list(my_zip([1,2], ['a','b']))
будет оцениваться следующим образом. Сначала вызывается my_zip
, и это даст вам объект-генератор. Затем повторите его с помощью list
. Он называет next
на нем, и он получает еще один объект-генератор list(arg[0] for arg in args)
. Поскольку не встречается никакого исключения или return
, он вызывает next
, чтобы получить еще один объект-генератор list(arg[1] for arg in args)
, и он продолжает итерацию. Помните, что полученные генераторы никогда не повторяются, поэтому мы никогда не получим IndexError
. Вот почему код работает бесконечно.
Вы можете подтвердить это следующим образом:
from itertools import islice
from pprint import pprint
pprint(list(islice(my_zip([1, 2], ["a", 'b']), 10)))
и вы получите
[<generator object <genexpr> at 0x7f4d0a709678>,
<generator object <genexpr> at 0x7f4d0a7096c0>,
<generator object <genexpr> at 0x7f4d0a7099d8>,
<generator object <genexpr> at 0x7f4d0a709990>,
<generator object <genexpr> at 0x7f4d0a7095a0>,
<generator object <genexpr> at 0x7f4d0a709510>,
<generator object <genexpr> at 0x7f4d0a7095e8>,
<generator object <genexpr> at 0x7f4d0a71c708>,
<generator object <genexpr> at 0x7f4d0a71c750>,
<generator object <genexpr> at 0x7f4d0a71c798>]
Таким образом, код пытается построить бесконечный список объектов генератора.
Ответ 2
def my_zip(*args):
i = 0
while True:
try:
yield (arg[i] for arg in args)
except IndexError:
raise StopIteration
i += 1
IndexError
не пойман, потому что (arg[i] for arg in args)
- это генератор, который не выполняется немедленно, но когда вы начинаете повторять его. И вы перебираете его в другой области, когда вы вызываете list((arg[i] for arg in args))
:
# get the generator which yields another generator on each iteration
gen = my_zip([1,2], ['a','b'])
# get the second generator `(arg[i] for arg in args)` from the first one
# then iterate over it: list((arg[i] for arg in args))
print(list(next(gen)))
- В первом
list(next(gen))
i
равен 0.
- На втором
list(next(gen))
i
равен 1.
- На третьем
list(next(gen))
i
равно 2. И здесь вы получаете IndexError
- во внешней области. Линия рассматривается как list(arg[2] for arg in ([1,2], ['a','b']))
Ответ 3
Извините, я не могу предложить последовательное объяснение, касающееся неспособности поймать исключение, однако, есть легкий путь вокруг него; используйте цикл for по длине кратчайшей последовательности:
def my_zip(*args):
for i in range(min(len(arg) for arg in args)):
yield (arg[i] for arg in args)
>>> gen = my_zip([1,2], ["a",'b','c'])
>>> print(list(next(gen)))
[1, 'a']
>>> print(list(next(gen)))
[2, 'b']
>>> print(list(next(gen)))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Ответ 4
Попробуйте заменить yield (arg[i] for ...)
следующим.
for arg in args:
yield arg[i]
Но в случае чисел, вызывающих исключение как 1[1]
, нет смысла. Я предлагаю заменить arg[i]
только на arg
.