web-dev-qa-db-ja.com

Python `yield from`、またはジェネレータを返しますか?

私はこの簡単なコードを書きました:

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))

そして、「リターン」と「降伏」の技術的な違いを超えて、一般的なケースでどちらがより良いアプローチですか?

26
AleMal

違いは、最初の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)
_

2番目の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などのインタラクティブシェル内で次のコードを実行して、さまざまな動作を確認します。 _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_のような構造のより合理的な使用例を提供するために、少し拡張し、変数にyieldを割り当てることによってコルーチンを作成します。これにより、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_と2つのジョブを使用して_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
_

一般的に、どのバージョンがより良いアプローチであるかという質問に戻ります。 _gen_factory_のようなIMOは、作成する複数のジェネレーターに対して同じことを行う必要がある場合、またはジェネレーターの構築プロセスが、個々のジェネレーターをビルドする代わりにファクトリの使用を正当化するほど複雑な場合にのみ意味があります発電機の理解を備えた場所。

注意:

上記の_gen_generator_関数(2番目のmymap)の説明には、「itisジェネレータ自体」と記載されています。これは少しあいまいで、技術的には正確ではありませんが、_gen_factory_がジェネレーター、つまり内部のジェネレーターの理解によって作成されたジェネレーターを返すこのトリッキーなセットアップでの関数の違いについての推論を容易にします。

実際any関数(この質問からのジェネレータ内包表記だけではありません!)中にyieldがあり、呼び出し時にreturns関数本体から作成されるジェネレーターオブジェクト。

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

したがって、上記で_gen_generator_および_gen_coroutine_について観察したアクション全体は、これらのジェネレーターオブジェクト内で行われ、yieldが内部にある関数は以前に吐き出しました。

13
Darkonaut

答えは、ジェネレーターを返すことです。より高速です:

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 38 を読んだ場合、yield fromが導入された主な理由は、コードを複製したりAPIを変更したりすることなく、ジェネレーターのコードの一部を別のジェネレーターに使用することです:

上記のほとんどのセマンティクスの背後にある理論的根拠は、ジェネレーターコードをリファクタリングできるようにしたいという欲求に由来しています。 1つ以上のyield式を含むコードのセクションを取り、それを別の関数に移動し(通常の手法を使用して、周囲のスコープ内の変数への参照を処理するなど)、新しい関数を式からの収量。

ソース

1
Marco Sulla

最も重要な違い(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:
1
Apalala

Generatorsyieldを使用しますfunctionsreturnを使用します。

Generatorsは一般にforループで値を繰り返し反復するために使用されますgeneratorによって自動的に提供されるですが、別のコンテキストでも使用できます。 g。 in list()リストを作成する関数-再び値からジェネレータによって自動的に提供される

関数は、すべての呼び出しに対して戻り値1つのみ値を提供するために呼び出されます。

0
MarianD