web-dev-qa-db-ja.com

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

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などの一部の言語からは根絶されました)。プログラミング言語のコンパイラ/インタープリターは、このような機能を実装するための規則に違反する必要がありますか?たとえば、言語はこの機能を機能させるためにマルチスレッドを実装する必要がありますか、それともスレッド化テクノロジなしで実行できますか?

9
Dennis

最初に注意事項-C#は私が最もよく知っている言語であり、yieldは他の言語のyieldと非常によく似ているように見えますが、気づかない微妙な違いがあるかもしれません。

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

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

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

はい、yieldtry/catch/finally、および許可されていません( https://blogs.msdn.Microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ 詳細については)。

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

この機能を持つことは確かに良い考えです。 C#のLINQのようなものはreallyいいです-コレクションを遅延して評価することは大きなパフォーマンス上の利点を提供し、yieldはそのようなことを実行できるようにしますコードのほんの一部で、手作業のイテレータが行うバグのほんの一部です。

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

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

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

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

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

16
Telastyn

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

ご覧のとおり、構文が簡潔で読みやすいだけでなく、sumなどの組み込み関数はジェネレーターを受け入れます。

With

Python Withステートメント の拡張提案)を確認してください。これは、他の言語の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でのジェネレーターのすべての使用法の完全なレビューではありません。特に、無限ジェネレーター、ステートマシン、値の受け渡し、およびそれらとコルーチンとの関係をスキップしました。

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

12
Joel Harmon

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

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

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

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

つまり、yieldはクロージャをサポートする言語では自然ですが、Java where mutable stateonlyはオブジェクトレベルに存在します。

6
JacquesB

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

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

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

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

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
0
user320927