Python `yield from`,还是返回一个生成器?

Posted

技术标签:

【中文标题】Python `yield from`,还是返回一个生成器?【英文标题】:Python `yield from`, or return a generator? 【发布时间】:2017-04-29 10:01:39 【问题描述】:

我写了这段简单的代码:

def mymap(func, *seq):
  return (func(*args) for args in zip(*seq))

我应该使用上面的'return'语句来返回一个生成器,还是使用这样的'yield from'指令:

def mymap(func, *seq):
  yield from (func(*args) for args in zip(*seq))

除了“return”和“yield from”之间的技术差异之外,一般情况下哪种方法更好?

【问题讨论】:

我自己偶然发现了这个问题,很好的问题。从函数调用者的角度来看,这没有任何区别,但我想知道 python 在这里做了什么。 【参考方案1】:

不同的是你的第一个mymap只是一个普通的函数, 在这种情况下,工厂返回一个生成器。一切 一旦你调用函数,体内就会被执行。

def gen_factory(func, seq):
    """Generator factory returning a generator."""
    # do stuff ... immediately when factory gets called
    print("build generator & return")
    return (func(*args) for args in seq)

第二个mymap也是工厂,但也是发电机 本身,由内部自建的子发电机产生。 因为它本身就是一个生成器,所以主体的执行确实 直到第一次调用 next(generator) 才开始。

def gen_generator(func, seq):
    """Generator yielding from sub-generator inside."""
    # do stuff ... first time when 'next' gets called
    print("build generator & yield")
    yield from (func(*args) for args in seq)

我认为下面的例子会更清楚。 我们定义了应该用函数处理的数据包, 捆绑在我们传递给生成器的作业中。

def add(a, b):
    return a + b

def sqrt(a):
    return a ** 0.5

data1 = [*zip(range(1, 5))]  # [(1,), (2,), (3,), (4,)]
data2 = [(2, 1), (3, 1), (4, 1), (5, 1)]

job1 = (sqrt, data1)
job2 = (add, data2)

现在我们在 IPython 等交互式 shell 中运行以下代码 看到不同的行为。 gen_factory 立即打印 出,而gen_generator 仅在next() 被调用后才会这样做。

gen_fac = gen_factory(*job1)
# build generator & return <-- printed immediately
next(gen_fac)  # start
# Out: 1.0
[*gen_fac]  # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]

gen_gen = gen_generator(*job1)
next(gen_gen)  # start
# build generator & yield <-- printed with first next()
# Out: 1.0
[*gen_gen]  # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]

为您提供一个更合理的构造用例示例 像gen_generator 我们将对其进行一些扩展并制作一个协程 通过将产量分配给变量来摆脱它,所以我们可以注入工作 使用send() 进入正在运行的生成器。

另外我们创建了一个辅助函数来运行所有的任务 在工作中并在完成后要求新的工作。

def gen_coroutine():
    """Generator coroutine yielding from sub-generator inside."""
    # do stuff... first time when 'next' gets called
    print("receive job, build generator & yield, loop")
    while True:
        try:
            func, seq = yield "send me work ... or I quit with next next()"
        except TypeError:
            return "no job left"
        else:
            yield from (func(*args) for args in seq)


def do_job(gen, job):
    """Run all tasks in job."""
    print(gen.send(job))
    while True:
        result = next(gen)
        print(result)
        if result == "send me work ... or I quit with next next()":
            break

现在我们使用辅助函数 do_job 和两个作业运行 gen_coroutine

gen_co = gen_coroutine()
next(gen_co)  # start
# receive job, build generator & yield, loop  <-- printed with first next()
# Out:'send me work ... or I quit with next next()'
do_job(gen_co, job1)  # prints out all results from job
# 1
# 1.4142135623730951
# 1.7320508075688772
# 2.0
# send me work... or I quit with next next()
do_job(gen_co, job2)  # send another job into generator
# 3
# 4
# 5
# 6
# send me work... or I quit with next next()
next(gen_co)
# Traceback ...
# StopIteration: no job left

回到您的问题,一般来说哪个版本更好。 IMO 之类的 gen_factory 仅在您需要为要创建的多个生成器完成相同的事情时才有意义,或者如果您的生成器构建过程足够复杂以证明使用工厂而不是在适当的位置构建单个生成器生成器理解。

注意:

上面对gen_generator 函数的描述(第二个mymap)状态 “它本身就是一个生成器”。这有点模糊,技术上不是 确实正确,但有助于推理功能的差异 在这个棘手的设置中,gen_factory 还返回一个生成器,即 由里面的生成器理解构建的。

事实上 any 函数(不仅是来自这个问题的那些内部带有生成器理解的函数!)在调用时,内部带有 yield 返回一个从函数体构造出来的生成器对象。

type(gen_coroutine) # functiongen_co = gen_coroutine(); type(gen_co) # generator

所以我们在上面观察到的gen_generatorgen_coroutine 的整个动作 发生在这些生成器对象中,内部带有yield 的函数之前已经吐出。

【讨论】:

【参考方案2】:

答案是:返回一个生成器。它更快:

marco@buzz:~$ python3.9 -m pyperf timeit --rigorous --affinity 3 --value 6 --loops=4096 -s '
a = range(1000)

def f1():
    for x in a:
        yield x

def f2():
    return f1()

' 'tuple(f2())'
........................................
Mean +- std dev: 72.8 us +- 5.8 us
marco@buzz:~$ python3.9 -m pyperf timeit --rigorous --affinity 3 --value 6 --loops=4096 -s '
a = range(1000)

def f1():
    for x in a:
        yield x

def f2():
    yield from f1()

' 'tuple(f2())'
........................................
WARNING: the benchmark result may be unstable
* the standard deviation (12.6 us) is 10% of the mean (121 us)

Try to rerun the benchmark with more runs, values and/or loops.
Run 'python3.9 -m pyperf system tune' command to reduce the system jitter.
Use pyperf stats, pyperf dump and pyperf hist to analyze results.
Use --quiet option to hide these warnings.

Mean +- std dev: 121 us +- 13 us

如果你读过PEP 380,引入yield from的主要原因是为了将一个生成器的一部分代码用于另一个生成器,而无需复制代码或更改API:

上面介绍的大多数语义背后的基本原理源于 希望能够重构生成器代码。它应该是 可以取一段包含一个或多个yield的代码 表达式,将其移动到一个单独的函数中(使用通常的 处理对周围变量的引用的技术 范围等),并使用 yield from 表达式调用新函数。

Source

【讨论】:

【参考方案3】:

最重要的区别(我不知道yield from generator 是否经过优化)是returnyield from 的上下文不同。


[ins] In [1]: def generator():
         ...:     yield 1
         ...:     raise Exception
         ...:

[ins] In [2]: def use_generator():
         ...:     return generator()
         ...:

[ins] In [3]: def yield_generator():
         ...:     yield from generator()
         ...:

[ins] In [4]: g = use_generator()

[ins] In [5]: next(g); next(g)
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-5-3d9500a8db9f> in <module>
----> 1 next(g); next(g)

<ipython-input-1-b4cc4538f589> in generator()
      1 def generator():
      2     yield 1
----> 3     raise Exception
      4

Exception:

[ins] In [6]: g = yield_generator()

[ins] In [7]: next(g); next(g)
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-7-3d9500a8db9f> in <module>
----> 1 next(g); next(g)

<ipython-input-3-3ab40ecc32f5> in yield_generator()
      1 def yield_generator():
----> 2     yield from generator()
      3

<ipython-input-1-b4cc4538f589> in generator()
      1 def generator():
      2     yield 1
----> 3     raise Exception
      4

Exception:

【讨论】:

【参考方案4】:

生成器使用yield函数使用return

生成器 通常在for 循环中用于重复迭代由生成器自动提供的值,但也可以在其他上下文中使用,例如。 G。在 list() 函数中创建列表 - 再次从生成器自动提供的值

函数被调用来提供返回值每次调用只有一个值。

【讨论】:

【参考方案5】:

我更喜欢带有yield from 的版本,因为它更容易处理异常和上下文管理器。

以文件行的生成器表达式为例:

def with_return(some_file):
    with open(some_file, 'rt') as f:
        return (line.strip() for line in f)

for line in with_return('/tmp/some_file.txt'):
    print(line)

return 版本引发ValueError: I/O operation on closed file.,因为在return 语句之后文件不再打开。

另一方面,yield from 版本按预期工作:

def with_yield_from(some_file):
    with open(some_file, 'rt') as f:
        yield from (line.strip() for line in f)


for line in with_yield_from('/tmp/some_file.txt'):
    print(line)

【讨论】:

【参考方案6】:

真的要视情况而定。 yield 主要适用于您只想遍历返回值然后对其进行操作的情况。 return 主要适用于当您想要将函数生成的所有值存储在内存中而不是只遍历它们一次时。请注意,您只能迭代生成器(yield 返回的内容)一次,有些算法绝对不适合。

【讨论】:

虽然被接受,但这并没有解决核心问题,这与“存储所有值”或“迭代返回值”无关。我希望得到一个带有风格论点或一些基准的答案。 这与问题中的案例无关。这将适用于决定是 yield 值还是 return 它们的列表,而不是 returnyield from 另一个生成器。

以上是关于Python `yield from`,还是返回一个生成器?的主要内容,如果未能解决你的问题,请参考以下文章

Python连载41-yield from详解委派生成器

python yield 和 yield from用法总结

python的关键字yield有啥作用

python yield,yield from,深浅拷贝

理解Python协程:从yield/send到yield from再到async/await

理解Python协程:从yield/send到yield from再到async/await