Python从或从中返回生成器?

Ale*_*Mal 21 python return function generator

我写了这段简单的代码:

def mymap(func, *seq):
  return (func(*args) for args in zip(*seq))
Run Code Online (Sandbox Code Playgroud)

现在我有一个疑问,最好使用上面的'return'语句返回一个生成器或使用'yield from'指令,如下所示:

def mymap(func, *seq):
  yield from (func(*args) for args in zip(*seq))
Run Code Online (Sandbox Code Playgroud)

超出"回报"和"收益率"之间的技术差异,这是一般情况下更好的方法吗?

Dar*_*aut 14

不同之处在于你的第一个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)
Run Code Online (Sandbox Code Playgroud)

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

我认为以下示例将使其更清晰.我们定义了应该用函数处理的数据包,捆绑在我们传递给生成器的作业中.

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

现在我们在像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]
Run Code Online (Sandbox Code Playgroud)

为一个构造提供一个更合理的用例示例,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
Run Code Online (Sandbox Code Playgroud)

现在我们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
Run Code Online (Sandbox Code Playgroud)

回到你的问题,哪个版本是更好的方法.gen_factory如果您需要为要创建的多个发电机完成相同的操作,或者在发电机的构造过程足够复杂以证明使用工厂而不是使用生成器理解来构建单个发电机的情况下,IMO之类的东西才有意义. .

注意:

上面对gen_generator功能(第二mymap)的描述表明"它发电机本身".这有点模糊,技术上并不是真正的正确,但有助于推理这个棘手的设置中的函数的差异,其中gen_factory还返回一个生成器,即由生成器理解内部构建的生成器.

事实上,在调用时, 任何函数(不仅仅是来自这个带有生成器理解的问题的函数!)都只返回一个生成器函数体构造的生成器对象.yield

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

因此,我们在上面观察到的gen_generator并且gen_coroutine 发生在这些生成器对象中的整个动作,yield内部的函数之前已经吐出.


Mar*_*lla 5

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

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

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

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

来源