发生了什么?
当您使用next(iter(Class2的实例))
时,iter()
在迭代器(而不是生成器!)超出作用域(并被删除)时调用内部生成器上的.close()
, 而使用Class1
时,iter()
仅关闭其实例。
>>> g = (i for i in range(3))
>>> b = Class2(g)
>>> i = iter(b)
>>> next(i)
0
>>> next(i)
1
>>> del(i)
>>> next(iter(b))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
这种行为在PEP 342中分为两部分进行描述
当出现多个生成器委托时,发生的情况会更清晰一些(尽管可能令人惊讶);仅在删除其包装iter
时才关闭被委托的生成器。
>>> g1 = (a for a in range(10))
>>> g2 = (a for a in range(10, 20))
>>> def test3():
... yield from g1
... yield from g2
...
>>> next(test3())
0
>>> next(test3())
10
>>> next(test3())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
修复Class2
有哪些选项可以使Class2
的行为更符合您的期望?
值得注意的是,其他策略虽然没有yield from
的视觉上令人愉悦的sugar或其潜在好处之一,但它们提供了一种与值交互的方式,这似乎是主要的好处。
- 尽量不要创建这样的结构(“不要这么做!”)
如果您不与生成器交互,也不打算保留对迭代器的引用,那么干嘛还要将其包装起来呢?(请参见上面关于交互的评论)
- 在内部自己创建迭代器(这可能是您期望的)
>>> class Class3:
... def __init__(self, gen):
... self.iterator = iter(gen)
...
... def __iter__(self):
... return self.iterator
...
>>> c = Class3((i for i in range(3)))
>>> next(iter(c))
0
>>> next(iter(c))
1
- 将整个类作为“proper”生成器
测试时,它可能会突显一些iter()
的不一致性-请参见下面的注释(即为什么没有关闭e
?)
使用 itertools.chain.from_iterable
传递多个生成器的机会。
>>> class Class5(collections.abc.Generator):
... def __init__(self, gen):
... self.gen = gen
... def send(self, value):
... return next(self.gen)
... def throw(self, value):
... raise StopIteration
... def close(self):
... self.gen.close()
...
>>> e = Class5((i for i in range(10)))
>>> next(e)
0
>>> next(e)
1
>>> next(iter(e))
2
>>> next(iter(e))
3
>>> e.close()
>>> next(e)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.9/_collections_abc.py", line 330, in __next__
return self.send(None)
File "<stdin>", line 5, in send
StopIteration
追寻谜团
更好的线索是,如果您直接再次尝试,next(iter(instance))
会引发StopIteration
,表明生成器已经永久关闭(通过耗尽或.close()
),并且为什么使用for
循环遍历它不会产生更多的值。
>>> a = Class1((i for i in range(3)))
>>> next(iter(a))
0
>>> next(iter(a))
1
>>> b = Class2((i for i in range(3)))
>>> next(iter(b))
0
>>> next(iter(b))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
然而,如果我们对迭代器进行命名,它会按预期工作。
>>> b = Class2((i for i in range(3)))
>>> i = iter(b)
>>> next(i)
0
>>> next(i)
1
>>> j = iter(b)
>>> next(j)
2
>>> next(i)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
在我看来,这意味着当迭代器没有名称时,它会在超出范围时调用
.close()
。
>>> def gen_test(iterable):
... yield from iterable
...
>>> g = gen_test((i for i in range(3)))
>>> next(iter(g))
0
>>> g.close()
>>> next(iter(g))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
拆解结果后,我们发现内部有些不同。
>>> a = Class1((i for i in range(3)))
>>> dis.dis(a.__iter__)
6 0 LOAD_FAST 0 (self)
2 LOAD_ATTR 0 (gen)
4 GET_ITER
>> 6 FOR_ITER 10 (to 18)
8 STORE_FAST 1 (el)
7 10 LOAD_FAST 1 (el)
12 YIELD_VALUE
14 POP_TOP
16 JUMP_ABSOLUTE 6
>> 18 LOAD_CONST 0 (None)
20 RETURN_VALUE
>>> b = Class2((i for i in range(3)))
>>> dis.dis(b.__iter__)
6 0 LOAD_FAST 0 (self)
2 LOAD_ATTR 0 (gen)
4 GET_YIELD_FROM_ITER
6 LOAD_CONST 0 (None)
8
10 POP_TOP
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
值得注意的是,yield from
版本具有GET_YIELD_FROM_ITER
。
如果TOS
是生成器迭代器或协程对象,则保持不变。否则,实现TOS = iter(TOS)
。
(微妙的是,YIELD_FROM
关键字似乎在3.11中被删除)
因此,如果给定的可迭代对象(传递给类)是生成器迭代器,则将直接传递,从而产生我们(可能)期望的结果。
额外内容
传递一个不是生成器的迭代器(iter()
在两种情况下每次都会创建一个新的迭代器)
>>> a = Class1([i for i in range(3)])
>>> next(iter(a))
0
>>> next(iter(a))
0
>>> b = Class2([i for i in range(3)])
>>> next(iter(b))
0
>>> next(iter(b))
0
明确地关闭 Class1
的内部生成器
>>> g = (i for i in range(3))
>>> a = Class1(g)
>>> next(iter(a))
0
>>> next(iter(a))
1
>>> a.gen.close()
>>> next(iter(a))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
只有在实例被弹出时,iter
才会关闭生成器。
>>> g = (i for i in range(10))
>>> b = Class2(g)
>>> i = iter(b)
>>> next(i)
0
>>> j = iter(b)
>>> del(j)
>>> next(i)
1
>>> j = iter(b)
>>> next(j)
2
>>> del(j)
>>> next(i)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Class2
,如果你将iter(a)
提取到一个变量中(b = iter(a); print(next(b))
),这将与Class1
相同,即打印所有数字。这很令人困惑,也很有趣。 - Yevhen Kuzmovychdel b
,它只会打印第一个。@YevhenKuzmovych - erzya