为什么Python生成器中的异常没有被捕获?

时间:2015-03-06 13:39:44

标签: python python-3.x generator

我有以下实验代码,其功能类似于内置的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']))

此行永不返回,似乎挂起了机器。现在发生了什么?

4 个答案:

答案 0 :(得分:13)

yield每次产生一个生成器对象,当创建生成器时,根本没有问题。这就是为什么try...except中的my_zip没有抓到任何东西的原因。第三次执行时,

list(arg[2] for arg in args)

这就是它被简化为(我们理解的简化),现在,仔细观察, list正在迭代生成器,而不是实际的my_zip生成器。现在,list调用生成器对象上的next并评估arg[2],但发现2不是arg的有效索引(即[1, 2]在这种情况下{1}},所以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>]

因此代码尝试构建无限的生成器对象列表。

答案 1 :(得分: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']))

答案 2 :(得分:1)

对不起,我无法提供有关未能捕获异常的连贯解释,但是,有一个简单的方法可以解决它;在最短序列的长度上使用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

答案 3 :(得分:0)

尝试使用以下内容替换yield (arg[i] for ...)

for arg in args:
    yield arg[i]

如果数字导致异常1[1]没有任何意义。我建议只使用arg[i]替换arg