【问题标题】:How to intercept the first value of a generator and transparently yield from the rest如何截取生成器的第一个值并透明地从其余值中产生
【发布时间】:2023-03-26 22:57:01
【问题描述】:

更新:我已经启动了一个 thread on python-ideas 来为此目的提出额外的语法或 stdlib 函数(即指定 yield from 发送的第一个值)。到目前为止 0 回复... :/


如何截取子生成器的第一个产生的值,但使用 yield from 将剩余的迭代委托给后者?

例如,假设我们有一个任意的双向生成器subgen,并且我们想将它包装在另一个生成器gen 中。 gen 的目的是拦截subgen 的第一个产生的值,并将其余的生成——包括发送的值、抛出的异常、.close() 等——委托给子-发电机。

首先想到的可能是:

def gen():
    g = subgen()

    first = next(g)
    # do something with first...
    yield "intercepted"

    # delegate the rest
    yield from g

但是这是错误的,因为当调用者.sends 得到第一个值后返回给生成器时,它最终会成为yield "intercepted" 表达式的值,它被忽略,取而代之的是g将接收None 作为第一个.send 值,作为yield from 语义的一部分。

所以我们可能会考虑这样做:

def gen():
    g = subgen()

    first = next(g)
    # do something with first...
    received = yield "intercepted"
    g.send(received)

    # delegate the rest
    yield from g

但是我们在这里所做的只是将问题向后移了一步:一旦我们调用g.send(received),生成器就会恢复执行并且不会停止,直到它到达下一个 yield 语句,其值变为.send 调用的返回值。所以我们还必须拦截并重新发送它。然后发送that,再发送that,以此类推……所以这样不行。

基本上,我要求的是yield from,它可以自定义发送到生成器的第一个值是什么:

def gen():
    g = subgen()

    first = next(g)
    # do something with first...
    received = yield "intercepted"

    # delegate the rest
    yield from g start with received  # pseudocode; not valid Python

...但不必自己重新实现yield from 的所有语义。也就是说,费力且维护性差的解决方案是:

def adaptor(generator, init_send_value=None):
    send = init_send_value
    try:
        while True:
            send = yield generator.send(send)
    except StopIteration as e:
        return e.value

这基本上是对yield from 的错误重新实现(它缺少对throwclose 的处理)。理想情况下,我想要一些更优雅、更少冗余的东西。

【问题讨论】:

  • x None 在你这样做之后:x = yield 42?
  • 不一定,x 可以是调用者发送的任何内容。使用 Python 3.9
  • 你用的是什么 Python?调用者发送的任何东西怎么能是 x 呢?
  • 我使用的是 Python 3.9。例如,如果直接使用subgeng = subgen(); v = next(g); v = g.send(123)。在上一条语句中,我们向subgen发送了123,所以x是123。然后生成器到达下一个yield语句并产生x + 2,即125;所以v 现在是125。请记住,第一个send 只是用于初始化生成器(即它的值不会出现在生成器中的任何位置)并且必须始终为.send(None),或等效的next()
  • here。 “当调用 send() 来启动生成器时,必须使用 None 作为参数调用它,因为没有 yield 表达式可以接收该值。”但是,在那之后,“value 参数变成了当前 yield 表达式的结果。”

标签: python generator yield-from


【解决方案1】:

如果您尝试使用yield from 将此生成器包装器实现为生成器函数,那么您的问题基本上归结为是否可以指定发送到“生成自”生成器的第一个值。它不是。

如果您查看PEP 380yield from 表达式的正式规范,您就会明白原因。该规范包含一段(令人惊讶的复杂)示例代码,其行为与yield from 表达式相同。前几行是:

_i = iter(EXPR)
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    ...

可以看到,对迭代器做的第一件事就是在其上调用next(),基本等价于.send(None)。无法跳过该步骤,并且无论何时使用 yield from,您的生成器都会收到另一个 None

我想出的解决方案是使用类而不是生成器函数来实现生成器协议:

class Intercept:
    def __init__(self, generator):
        self._generator = generator
        self._intercepted = False

    def __next__(self):
        return self.send(None)

    def send(self, value):
        yielded_value = self._generator.send(value)

        # Intercept the first value yielded by the wrapped generator and 
        # replace it with a different value.
        if not self._intercepted:
            self._intercepted = True

            print(f'Intercepted value: {yielded_value}')

            yielded_value = 'intercepted'

        return yielded_value

    def throw(self, type, *args):
        return self._generator.throw(type, *args)

    def close(self):
        self._generator.close()

__next__()send()throw()close()Python Reference Manual 中有描述。

该类包装了在创建时传递给它的生成器,它将模仿它的行为。它唯一改变的是生成器产生的第一个值在返回给调用者之前被替换为不同的值。

我们可以使用生成两个值的示例生成器f() 和一个将值发送到生成器直到生成器终止的函数main() 来测试行为:

def f():
    y = yield 'first'
    print(f'f(): {y}')

    y = yield 'second'
    print(f'f(): {y}')

def main():
    value_to_send = 0
    gen = f()

    try:
        x = gen.send(None)

        while True:
            print(f'main(): {x}')

            # Send incrementing integers to the generator.
            value_to_send += 1
            x = gen.send(value_to_send)
    except StopIteration:
        print('main(): StopIteration')    
      
main()

运行时,此示例将产生以下输出,显示哪些值到达生成器,哪些值由生成器返回:

main(): first
f(): 1
main(): second
f(): 2
main(): StopIteration

通过将语句 gen = f() 更改为 gen = Intercept(f()) 来包装生成器 f(),产生以下输出,表明第一个产生的值已被替换:

Intercepted value: first
main(): intercepted
f(): 1
main(): second
f(): 2

由于对任何生成器 API 的所有其他调用都直接转发到封装生成器,因此它的行为应该与封装生成器本身等效。

【讨论】:

  • “规范包含一段(令人惊讶的复杂)示例代码”——是的,这正是我不想重新实现 yield from 的原因 :) 我已经意识到,正如你比如说,目前没有使用yield from 的解决方案。而且我认为这种方法是我们可以在不(重新)编写太多代码的情况下实现的最佳方法。谢谢!
【解决方案2】:

如果我理解这个问题,我认为这可行吗?意思是,我运行了这个脚本,它完成了我的预期,即打印除了输入文件的第一行之外的所有内容。但只要作为参数传递给skip_first 函数的生成器可以迭代,它就可以工作。

def skip_first(thing):
    _first = True
    for _result in thing:
        if _first:
        _   first = False
            continue
        yield _result

inp = open("/var/tmp/test.txt")

for line in skip_first(inp):
    print(line, end="")

【讨论】:

  • 这完全忽略了.send.throw.close...Python generators are more than a simple iterator。 (目的是截取并转换第一个产生的值,而不是完全跳过它,但这是次要的。)
  • 我使用基于类的生成器,因此尝试将其压缩到函数中是一项挑战。 :)
猜你喜欢
  • 2016-01-30
  • 1970-01-01
  • 1970-01-01
  • 2015-08-05
  • 2018-07-29
  • 2019-05-11
  • 1970-01-01
  • 2011-01-26
  • 1970-01-01
相关资源
最近更新 更多