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
acceptジェネレーターなどの組み込み関数もあります。
と
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でのジェネレータのすべての使用法の完全なレビューではありません。特に、無限ジェネレーター、ステートマシン、値の受け渡し、およびそれらとコルーチンとの関係をスキップしました。
ジェネレーターを完全に統合された便利な言語機能として使用できることを実証するだけで十分だと思います。
yield
本質的に状態エンジンです。毎回同じ結果を返すことを意図していません。それは何でしょう絶対的確信を持ってやっていることは可算で、それが呼び出されるたびに、次の項目を返すことです。スレッドは必要ありません。現在の状態を維持するには、(多かれ少なかれ)クロージャが必要です。