「yield」などのジェネレーター言語機能を使用するのは良い考えですか?


9

PHP、C#、Python、およびおそらく他のいくつかの言語には、yieldジェネレーター関数の作成に使用されるキーワードがあります。

PHPの場合:http : //php.net/manual/en/language.generators.syntax.php

Pythonの場合:https : //www.pythoncentral.io/python-generators-and-yield-keyword/

C#の場合:https : //docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield

言語の機能/機能として、yieldいくつかの規則に違反していることを心配しています。それらの1つは、「確実性」と呼んでいるものです。呼び出すたびに異なる結果を返すメソッドです。通常の非ジェネレーター関数を使用してそれを呼び出すことができ、同じ入力が与えられた場合、同じ出力を返します。yieldでは、内部状態に基づいて異なる出力を返します。したがって、以前の状態を知らないで生成関数をランダムに呼び出すと、特定の結果を返すことは期待できません。

このような関数はどのように言語パラダイムに適合しますか?それは実際に慣習に違反していますか?この機能を使用することは良い考えですか?(良い点と悪い点の例を示すために、gotoかつては多くの言語の機能でしたが、現在もそうですが、それは有害であると見なされ、Javaなどの一部の言語からは根絶されました)。プログラミング言語のコンパイラ/インタープリターは、そのような機能を実装するために何らかの規則を破る必要がありますか?たとえば、言語はこの機能を動作させるためにマルチスレッドを実装する必要がありますか、それともスレッド化テクノロジなしで実行できますか?


4
yield本質的に状態エンジンです。毎回同じ結果を返すことを意図していません。それは何でしょう絶対的確信を持ってやっていることは可算で、それが呼び出されるたびに、次の項目を返すことです。スレッドは必要ありません。現在の状態を維持するには、(多かれ少なかれ)クロージャが必要です。
Robert Harvey、

1
「確実性」の品質については、同じ入力シーケンスが与えられた場合、イテレータへの一連の呼び出しがまったく同じアイテムをまったく同じ順序で生成することを考慮してください。
Robert Harvey、

4
C ++にはPythonのようなyield キーワードがないため、ほとんどの質問の出所がわかりません。静的メソッドがありますが、それはstd::this_thread::yield()キーワードではありません。そのため、this_threadほとんどすべての呼び出しが先頭に追加され、制御フローの生成に関する言語機能ではなく、スレッドを生成するためのライブラリ機能であることは明らかです。
Ixrec 2017

リンクをC#に更新、C ++の1つを削除
Dennis

回答:


16

最初の警告-C#は私が最もよく知っている言語です。yield他の言語と非常によく似ているようyieldですが、気付かない微妙な違いがあるかもしれません。

言語の機能/機能として、yieldがいくつかの規則を破ることを心配しています。それらの1つは、「確実性」と呼んでいます。呼び出すたびに異なる結果を返すメソッドです。

戯言。あなたが本当に それらを呼び出すたびに同じ結果を期待しますRandom.NextそれともConsole.ReadLine返すでしょうか?残りの通話はどうですか?認証?コレクションからアイテムを取得しますか?純粋ではない、あらゆる種類の(良い、便利な)関数があります。

このような関数はどのように言語パラダイムに適合しますか?それは実際に慣習に違反していますか?

はい、とのyield相性は非常に悪く、try/catch/finally許可されていません(https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ forより詳しい情報)。

この機能を使用することは良い考えですか?

この機能を持つことは確かに良い考えです。C#のLINQのようなものは本当に素晴らしいです。コレクションを遅延評価することでパフォーマンスが大幅に向上yieldし、手作業の反復子が行うバグのほんの一部で、コードのほんの一部でそのようなことを行うことができます。

とはいえ、yieldLINQスタイルのコレクション処理以外の用途はそれほど多くありません。検証処理、スケジュール生成、ランダム化、その他いくつかの目的で使用しましたが、ほとんどの開発者がこれを使用したことがない(または誤用した)と思います。

プログラミング言語のコンパイラ/インタープリターは、そのような機能を実装するために何らかの規則を破る必要がありますか?たとえば、言語はこの機能を動作させるためにマルチスレッドを実装する必要がありますか、それともスレッド化テクノロジなしで実行できますか?

ではない正確に。コンパイラーは、停止した場所を追跡するステートマシンイテレーターを生成し、次回呼び出されたときに再び開始できるようにします。コード生成のプロセスは、Continuous Passing Styleに似ています。つまり、の後のコードyieldが独自のブロックにプルされます(それに、yields、別のサブブロックなどがある場合)。これは、関数型プログラミングで頻繁に使用されるよく知られたアプローチであり、C#の非同期/待機コンパイルにも現れます。

スレッド化は必要ありませんが、ほとんどのコンパイラではコード生成に別のアプローチが必要であり、他の言語機能といくつかの競合があります。

ただし、全体的に見て、yield比較的影響の少ない機能であり、特定の問題のサブセットを実際に支援します。


私はC#を真剣に使用したことがありませんが、このyieldキーワードはコルーチンに似ています。もしそうなら、私はCで1つ持っていればいいのに!私は、そのような言語機能で書くのがずっと簡単だったであろうコードの適切なセクションを少なくともいくつか考えることができます。

2
@DrunkCoder-同様ですが、私が理解しているように、いくつかの制限があります。
Telastyn 2017

1
また、利回りの悪用を見たくないでしょう。言語が持つ機能が多ければ多いほど、その言語でひどく書かれたプログラムを見つける可能性が高くなります。親しみやすい言語を書くための正しいアプローチがあなたにそれをすべて投げつけて何が固執するかを確かめることであるかどうか私はわかりません。
Neil

1
@DrunkCoder:セミコルーチンの限定バージョンです。実際には、一連のメソッド呼び出し、クラス、オブジェクトに展開されるコンパイラーによって構文パターンとして扱われます。(基本的に、コンパイラーはフィールド内の現在のコンテキストを取り込む継続オブジェクトを生成します。)コレクションのデフォルトの実装はセミコルーチンですが、コンパイラーが使用する「マジック」メソッドをオーバーロードすることで、実際に動作をカスタマイズできます。たとえば、async/ awaitが言語に追加される前に、誰かがを使用してそれを実装しましたyield
イェルクWミッターク

1
@Neil一般に、ほとんどすべてのプログラミング言語機能を誤用する可能性があります。あなたが言っていることが真実なら、PythonやC#よりもCを使ってひどくプログラムするのははるかに難しいでしょうが、これらの言語には、非常に簡単な多くの間違いからプログラマーを保護するツールがたくさんあるので、そうではありませんCで作成します。実際には、悪いプログラムの原因は悪いプログラマです-それは言語にとらわれない問題です。
Ben Cottrell 2017

12

yield良いアイデアなどのジェネレーター言語機能はありますか?

私はこれをPythonの観点から答えたいと思います。そうですね、それは素晴らしいアイデアです。

まず、質問のいくつかの質問と仮定に対処することから始め、次にジェネレーターの普及と、Pythonでのそれらの不合理な有用性を示します。

通常の非ジェネレーター関数を使用してそれを呼び出すことができ、同じ入力が与えられた場合、同じ出力を返します。yieldでは、内部状態に基づいて異なる出力を返します。

これは誤りです。オブジェクトのメソッドは、それ自体が内部状態を持つ関数そのものと考えることができます。Pythonでは、すべてがオブジェクトであるため、実際にはオブジェクトからメソッドを取得し、そのメソッドを渡すことができます(メソッドは元のオブジェクトにバインドされているため、状態を記憶しています)。

その他の例には、意図的にランダムな関数や、ネットワーク、ファイルシステム、端末などの入力方法が含まれます。

このような関数はどのように言語パラダイムに適合しますか?

言語パラダイムがファーストクラスの関数のようなものをサポートし、ジェネレーターがIterableプロトコルのような他の言語機能をサポートする場合、それらはシームレスに適合します。

それは実際に慣習に違反していますか?

いいえ。それは言語に組み込まれているため、規約は作成され、ジェネレーターの使用が含まれています(または必要です!)。

プログラミング言語のコンパイラ/インタープリターは、そのような機能を実装するために、あらゆる規則を破る必要がありますか?

他の機能と同様に、コンパイラはその機能をサポートするように設計する必要があるだけです。Pythonの場合、関数はすでに状態を持つオブジェクトです(デフォルトの引数や関数の注釈など)。

この機能を動作させるには、言語でマルチスレッドを実装する必要がありますか、それともスレッド化テクノロジなしで実行できますか?

おもしろい事実:デフォルトのPython実装はスレッド化をまったくサポートしていません。グローバルインタープリターロック(GIL)を備えているため、2番目のプロセスを起動してPythonの別のインスタンスを実行しない限り、実際には同時に何も実行されていません。


注:例はPython 3にあります

収量を超えて

一方でyield、キーワードは、発電機にそれを回すために任意の関数で使用することができ、それがものを作るための唯一の方法ではありません。Pythonにはジェネレーター式があり、ジェネレーターを別の反復可能オブジェクト(他のジェネレーターを含む)で明確に表現する強力な方法です。

>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155

ご覧のとおり、構文が簡潔で読みやすいだけでなく、sumacceptジェネレーターなどの組み込み関数もあります。

WithステートメントのPython拡張提案をご覧ください。これは、他の言語のWithステートメントから予想されるものとは大きく異なります。標準ライブラリの助けを借りて、Pythonのジェネレーターはそれらのコンテキストマネージャーとして美しく機能します。

>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
        print("preprocessing", arg)
        yield arg
        print("postprocessing", arg)


>>> with debugWith("foobar") as s:
        print(s[::-1])


preprocessing foobar
raboof
postprocessing foobar

もちろん、印刷はここでできる最も退屈な作業ですが、目に見える結果が表示されます。より興味深いオプションには、リソースの自動管理(ファイル/ストリーム/ネットワーク接続のオープンとクローズ)、同時実行性のロック、関数の一時的なラップまたは置換、データの圧縮解除と再圧縮が含まれます。関数の呼び出しがコードにコードを挿入するようなものである場合、withステートメントはコードの一部を他のコードでラップするようなものです。どのように使用しても、言語構造への簡単なフックの確かな例です。Yieldベースのジェネレーターは、コンテキストマネージャーを作成する唯一の方法ではありませんが、確かに便利なものです。

部分的消耗

Pythonのforループは興味深い方法で機能します。それらの形式は次のとおりです。

for <name> in <iterable>:
    ...

最初に、呼び出した式を<iterable>評価して、反復可能なオブジェクトを取得します。次に、イテラブルが__iter__呼び出され、結果のイテレーターがバックグラウンドで保管されます。その後、__next__イテレータで呼び出され、入力した名前にバインドする値を取得します<name>。このステップは、を__next__スローする呼び出しが終了するまで繰り返されStopIterationます。例外はforループによって飲み込まれ、そこから実行が続行されます。

ジェネレータに戻ると、ジェネレータを呼び出す__iter__と、それ自体が返されます。

>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272

これが意味することは、何かを繰り返し処理することから、それを使ってやりたいことを分離し、その動作を途中で変更できるということです。以下では、同じジェネレーターが2つのループでどのように使用されているか、2番目のループでは最初のループから中断したところから実行を開始することに注意してください。

>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
        print(ord(letter))
        if letter > 'p':
                break


109
111
114
>>> for letter in generator:
        print(letter)


e

b
o
r
i
n
g

s
t
u
f
f

遅延評価

リストと比較した場合のジェネレーターのマイナス面の1つは、ジェネレーターでアクセスできる唯一のものは、その次に出てくるものです。前の結果と同じように戻ったり、中間結果を経由せずに後の結果にジャンプしたりすることはできません。この利点は、ジェネレータが同等のリストと比較してメモリをほとんど消費しないことです。

>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    sys.getsizeof([x for x in range(10000000000)])
  File "<pyshell#10>", line 1, in <listcomp>
    sys.getsizeof([x for x in range(10000000000)])
MemoryError

ジェネレータをレイジーチェーンにすることもできます。

logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))

1行目、2行目、3行目はそれぞれジェネレータを定義するだけですが、実際の作業は行いません。最後の行が呼び出されると、sumはnumericcolumnに値を要求し、numericcolumnはlastcolumnからの値を必要とし、lastcolumnはlogfileから値を要求し、実際にファイルから行を読み取ります。このスタックは、合計が最初の整数になるまで巻き戻されます。次に、2行目でプロセスが再び発生します。この時点で、sumには2つの整数があり、それらを加算します。3行目はまだファイルから読み込まれていないことに注意してください。次に、Sumは、numericcolumnから値を要求し(残りのチェーンにはまったく気づかない)、numericcolumnがなくなるまでそれらを追加します。

ここで本当に興味深い部分は、行が個別に読み取られ、消費され、破棄されることです。一度にメモリ内のファイル全体が一度に存在することはありません。このログファイルがたとえばテラバイトの場合はどうなりますか?一度に1行しか読み取らないため、機能します。

結論

これは、Pythonでのジェネレータのすべての使用法の完全なレビューではありません。特に、無限ジェネレーター、ステートマシン、値の受け渡し、およびそれらとコルーチンとの関係をスキップしました。

ジェネレーターを完全に統合された便利な言語機能として使用できることを実証するだけで十分だと思います。


6

古典的なOOP言語に慣れている場合yield、可変レベルの状態がオブジェクトレベルではなく関数レベルでキャプチャされるため、ジェネレーターが不快に思えるかもしれません。

「確実性」の問題はレッドニシンですが。これは通常、参照透過性と呼ばれ、基本的に、関数が同じ引数に対して常に同じ結果を返すことを意味します。変更可能な状態になるとすぐに、参照の透明性が失われます。OOPでは、オブジェクトに変更可能な状態があることがよくあります。つまり、メソッド呼び出しの結果は、引数だけでなく、オブジェクトの内部状態にも依存します。

問題は、変更可能な状態をどこに取り込むかです。従来のOOPでは、変更可能な状態はオブジェクトレベルで存在します。ただし、言語がクロージャをサポートしている場合、関数レベルで変更可能な状態になる可能性があります。たとえばJavaScriptの場合:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

要するに、yieldクロージャをサポートする言語では自然ですが、古いバージョンのJavaのように、変更可能な状態がオブジェクトレベルでのみ存在する言語では不適切です。


言語機能にスペクトルがある場合、歩留まりは関数型関数から可能な限り離れていると思います。それは必ずしも悪いことではありません。OOPはかつては非常にファッショナブルで、その後再び関数型プログラミングになりました。危険性は、yieldなどの機能と、プログラムが予期しない動作をするように機能する設計とを組み合わせることに相当すると思います。
Neil

0

私の意見では、それは良い機能ではありません。それは主にそれが非常に注意深く教えられる必要があり、誰もがそれを間違って教えているので、それは悪い特徴です。人々は「ジェネレーター」という言葉を使い、ジェネレーター関数とジェネレーターオブジェクトを区別しません。問題は、実際の利回りを誰または何がしているだけなのか、です。

これは単なる私の意見ではありません。彼がこれを支配しているPEP速報でGuidoでさえ、ジェネレーター関数はジェネレーターではなく「ジェネレーターファクトリー」であることを認めています。

それは重要なことだと思いませんか?しかし、そこにあるドキュメントの99%を読むと、ジェネレーター関数が実際のジェネレーターであるという印象が得られ、ジェネレーターオブジェクトも必要であるという事実を無視する傾向があります。

グイドは、これらの関数の「gen」を「def」に置き換えることを検討し、「いいえ」と述べました。しかし、とにかくそれでは十分ではなかったでしょう。それは本当に:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.