yield from 与 for 循环中的yield

erz*_*zya 31 python yield generator python-internals yield-from

我的理解yield from是,它类似于yield从可迭代对象中获取每个项目。然而,我在以下示例中观察到不同的行为。

我有Class1

class Class1:
    def __init__(self, gen):
        self.gen = gen
        
    def __iter__(self):
        for el in self.gen:
            yield el
Run Code Online (Sandbox Code Playgroud)

和 Class2 的不同之处仅在于yield将 for 循环替换为yield from

class Class2:
    def __init__(self, gen):
        self.gen = gen
        
    def __iter__(self):
        yield from self.gen
Run Code Online (Sandbox Code Playgroud)

下面的代码从给定类的实例中读取第一个元素,然后在 for 循环中读取其余元素:

a = Class1((i for i in range(3)))
print(next(iter(a)))
for el in iter(a):
    print(el)
Run Code Online (Sandbox Code Playgroud)

Class1这会为和产生不同的输出Class2。对于Class1输出是

0
1
2
Run Code Online (Sandbox Code Playgroud)

Class2输出为

0
Run Code Online (Sandbox Code Playgroud)

现场演示

yield from产生不同行为的背后机制是什么?

ti7*_*ti7 27

发生了什么?

当您使用 时next(iter(instance_of_Class2)),当内部生成器(迭代器,而不是生成器!)超出范围(并被删除)时调用内部生成器,而使用 时iter(),仅关闭其实例.close()Class1iter()

>>> g = (i for i in range(3))
>>> b = Class2(g)
>>> i = iter(b)     # hold iterator open
>>> next(i)
0
>>> next(i)
1
>>> del(i)          # closes g
>>> next(iter(b))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
Run Code Online (Sandbox Code Playgroud)

PEP 342 中分两部分描述了此行为

  • .close()方法(Python 2.5 的新方法)
  • 来自规范摘要
    1. 添加支持以确保在生成器迭代器被垃圾收集时调用 close()。

当多个生成器委托发生时,发生的情况会更清楚一些(也许令人惊讶);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
Run Code Online (Sandbox Code Playgroud)

定影Class2

有哪些选择可以让Class2行为更加符合您的期望?

值得注意的是,其他策略虽然没有视觉上令人愉悦的糖分yield from也没有一些潜在的好处,但它们为您提供了一种与价值观互动的方式,这似乎是主要好处

  • 完全避免创建这样的结构(“只是不要这样做!”)
    如果您不与生成器交互并且不打算保留对迭代器的引用,为什么还要费心包装它呢?(参见上面关于交互的评论)
  • 自己在内部创建迭代器(这可能是您所期望的)
    >>> 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
    
    Run Code Online (Sandbox Code Playgroud)

  • 在测试时使整个类成为“正确的”生成器,它似乎突出了一些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):          # optional, but more complete
    ...         self.gen.close()
    ... 
    >>> e = Class5((i for i in range(10)))
    >>> next(e)        # NOTE iter is not necessary!
    0
    >>> next(e)
    1
    >>> next(iter(e))  # but still works
    2
    >>> next(iter(e))  # doesn't close e?? (should it?)
    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
    
    Run Code Online (Sandbox Code Playgroud)

寻找谜团

更好的线索是,如果您直接重试,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
Run Code Online (Sandbox Code Playgroud)

但是,如果我们命名迭代器,它就会按预期工作

>>> 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
Run Code Online (Sandbox Code Playgroud)

对我来说,这表明当迭代器没有名称时,它会.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
Run Code Online (Sandbox Code Playgroud)

拆解结果,发现内部有些不一样

>>> 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
Run Code Online (Sandbox Code Playgroud)

值得注意的是,该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
Run Code Online (Sandbox Code Playgroud)

快速关闭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
Run Code Online (Sandbox Code Playgroud)

iter如果实例被弹出,生成器仅在删除时关闭

>>> g = (i for i in range(10))
>>> b = Class2(g)
>>> i = iter(b)
>>> next(i)
0
>>> j = iter(b)
>>> del(j)        # next() not called on j
>>> next(i)
1
>>> j = iter(b)
>>> next(j)
2
>>> del(j)        # generator closed
>>> next(i)       # now fails, despite range(10) above
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
Run Code Online (Sandbox Code Playgroud)

  • 这只是我的主观意见,但这感觉像是一个错误,即使它符合规范。如果规范强制执行此操作,那么我会说规范强制执行错误的行为。也就是说,我不能 100% 确定规范确实强制执行此行为,因为它取决于对象何时被垃圾收集,而且据我所知,规范并没有确切说明何时应该发生这种情况。延迟垃圾收集直到生成器以正常方式耗尽应该符合规范。 (4认同)
  • @ti7 如果生成器迭代器仍然被 self.gen 引用,为什么它会被垃圾收集? (3认同)