Python`从`收益,还是返回一个生成器?

时间:2016-12-14 06:48:13

标签: python function return generator

我写了这段简单的代码:

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'之间的技术差异,这是一般情况下更好的方法吗?

6 个答案:

答案 0 :(得分:10)

不同之处在于你的第一个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也是一个工厂,但它也是一个发电机 本身,从内部自建的子发电机产生。 因为它是一个发电机本身,所以身体的执行确实如此 直到下一次(发电机)的第一次调用才开始。

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立即打印 out 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

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

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)状态的描述 &#34;它 一个发电机本身&#34;。这有点模糊,技术上没有 真的很正确,但有助于推断功能的差异 在这个棘手的设置中gen_factory也返回一个生成器,即那个 一个由生成器理解构建的内部。

实际上任何函数(不仅仅是那个内置生成器理解的问题的那些函数!),在调用时只有yield,只是 返回一个生成器对象,它由函数体构成。

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

因此,我们在上面针对gen_generatorgen_coroutine观察到的整个行动 在这些生成器对象中发生,yield内部的函数在之前吐出。

答案 1 :(得分:2)

最重要的区别(我不知道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:

答案 2 :(得分: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:

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

Source

答案 3 :(得分:0)

生成器使用yield函数使用return

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

调用

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

答案 4 :(得分:0)

我更喜欢带有 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)

答案 5 :(得分:-3)

真的取决于具体情况。 yield主要适用于您只想迭代返回值然后操纵它们的情况。 return主要适用于您希望存储函数在内存中生成的所有值而不是仅迭代它们一次的情况。请注意,您只能迭代一个生成器(产量返回)一次,有一些算法绝对不适合。