Python Language
発電機
サーチ…
前書き
ジェネレータは、ジェネレータ関数( yield
を使用)またはジェネレータ式(an_expression for x in an_iterator)
を使用)によって作成される遅延イテレータです。
構文
- yield
<expr>
-
<expr>
からの歩留まり -
<var>
= yield<expr>
- next(
<iter>
)
反復
ジェネレータオブジェクトはイテレータプロトコルをサポートします 。つまり、Python 3.xのnext()
メソッド( __next__()
を提供します。これは実行をステップ実行するために使用され、 __iter__
メソッドはそれ自身を返します。これはジェネレータが汎用的なiterableオブジェクトをサポートする任意の言語構造体で使用できることを意味します。
# naive partial implementation of the Python 2.x xrange()
def xrange(n):
i = 0
while i < n:
yield i
i += 1
# looping
for i in xrange(10):
print(i) # prints the values 0, 1, ..., 9
# unpacking
a, b, c = xrange(3) # 0, 1, 2
# building a list
l = list(xrange(10)) # [0, 1, ..., 9]
next()関数
next()
ビルトインは、任意のイテレータ(ジェネレータイテレータを含む)から値を受け取り、イテレータが使い果たされた場合のデフォルト値を提供するために使用できる便利なラッパーです。
def nums():
yield 1
yield 2
yield 3
generator = nums()
next(generator, None) # 1
next(generator, None) # 2
next(generator, None) # 3
next(generator, None) # None
next(generator, None) # None
# ...
構文はnext(iterator[, default])
です。イテレータが終了し、デフォルト値が渡された場合は、それが返されます。デフォルトが指定されていない場合は、 StopIteration
がStopIteration
ます。
オブジェクトをジェネレータに送信する
ジェネレータから値を受け取ることに加えて、 send()
メソッドを使用してジェネレータにオブジェクトを送信することもできます。
def accumulator():
total = 0
value = None
while True:
# receive sent value
value = yield total
if value is None: break
# aggregate values
total += value
generator = accumulator()
# advance until the first "yield"
next(generator) # 0
# from this point on, the generator aggregates values
generator.send(1) # 1
generator.send(10) # 11
generator.send(100) # 111
# ...
# Calling next(generator) is equivalent to calling generator.send(None)
next(generator) # StopIteration
ここでは、次のことが起こります。
- 最初に
next(generator)
呼び出すと、プログラムは最初のyield
ステートメントに進み、その時点でtotal
の値0を返します。この時点でジェネレーターの実行が中断されます。 -
generator.send(x)
呼び出すと、インタプリタは引数x
をとり、最後のyield
文の戻り値をvalue
に代入しvalue
。その後、ジェネレータは、次の値が得られるまで通常通り処理を続けます。 - 最後に
next(generator)
呼び出すと、プログラムはこれをジェネレーターにNone
を送信しているかのように扱います。None
について特別なことは何もありませんが、この例では、発電機を停止するように特別な値としてNone
を使用しています。
ジェネレータの式
理解のような構文を使用してジェネレータイテレータを作成することは可能です。
generator = (i * 2 for i in range(3))
next(generator) # 0
next(generator) # 2
next(generator) # 4
next(generator) # raises StopIteration
関数が必ずしもリストを渡す必要がない場合は、ジェネレータ式を関数呼び出しの中に置くことで、文字を節約でき(読みやすさを向上させる)ことができます。関数呼び出しのかっこは、式を暗黙的にジェネレータ式にします。
sum(i ** 2 for i in range(4)) # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14
さらに、上記の例では反復処理中のリスト全体( [0, 1, 2, 3]
)をロードするのではなく、ジェネレータはPythonが必要に応じて値を使用できるようにするため、メモリを節約します。
前書き
ジェネレータの式はlist、dictionary、set comprehensionsと似ていますが、かっこで囲まれています。カッコは、関数呼び出しの唯一の引数として使用されているときに存在する必要はありません。
expression = (x**2 for x in range(10))
この例では、0(x = 0)を含む10個の最初の完全な正方形を生成します。
Generator関数は、本体に1つ以上のyield
文がある点を除いて、通常の関数と似ています。このような関数は値をreturn
ことはできません(ただし、ジェネレータを早期に停止させたい場合は空のreturn
値を使用できます)。
def function():
for x in range(10):
yield x**2
このジェネレータ関数は、前のジェネレータ式と等価であり、同じものを出力します。
注意 :すべてのジェネレータ式はそれぞれ独自の同等の関数を持ちますが、その逆もありません。
両方の括弧を繰り返す場合は、括弧なしでジェネレータ式を使用できます。
sum(i for i in range(10) if i % 2 == 0) #Output: 20
any(x = 0 for x in foo) #Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1) #Output: <class 'generator'>
の代わりに:
sum((i for i in range(10) if i % 2 == 0))
any((x = 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))
だがしかし:
fooFunction(i for i in range(10) if i % 2 == 0,foo,bar)
return x = 0 for x in foo
barFunction(baz, a > b for a in foo if a % 2 == 1)
ジェネレータ関数を呼び出すと、ジェネレータオブジェクトが生成されます。 ジェネレータオブジェクトは後で反復処理できます 。他のタイプのイテレータとは異なり、ジェネレータオブジェクトは一度しかトラバースできません。
g1 = function()
print(g1) # Out: <generator object function at 0x1012e1888>
ジェネレータのボディはすぐに実行されないことに注意してください。上の例でfunction()
を呼び出すと、最初のprint文も実行せずにジェネレータオブジェクトをすぐに返します。これにより、ジェネレータは、リストを返す関数より少ないメモリを消費することができ、無限に長いシーケンスを生成するジェネレータを作成することができます。
このため、ジェネレータは、データサイエンスや大量のデータを含むその他のコンテキストでよく使用されます。別の利点は、完全なシーケンスが生成されるのを待たずに、他のコードがジェネレータによって生成された値をすぐに使用できることです。
ただし、ジェネレータで生成された値を複数回使用する必要があり、生成コストが格納以上の場合は、シーケンスを再生成するよりも、生成された値をlist
として格納する方が良い場合があります。詳細については、以下の「ジェネレータのリセット」を参照してください。
通常、ジェネレータオブジェクトは、ループ内で、または反復可能関数を必要とする関数で使用されます。
for x in g1:
print("Received", x)
# Output:
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81
arr1 = list(g1)
# arr1 = [], because the loop above already consumed all the values.
g2 = function()
arr2 = list(g2) # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
ジェネレータオブジェクトはイテレータなので、 next()
関数を使って手動で反復処理することができます。これを行うと、後続の呼び出しごとに1つずつ降伏値が返されます。
フードの中で、ジェネレータでnext()
を呼び出すたびに、Pythonは次のyield
文に当たるまでジェネレータ関数の本体で文を実行します。この時点で、 yield
コマンドの引数を返し、それが起こったポイントを記憶しています。 next()
もう一度呼び出すと、その時点から実行が再開され、次のyield
文まで続きます。
Pythonがyield
もたずにジェネレータ関数の終わりに達すると、 StopIteration
例外が発生します(これは正常ですが、すべてのイテレータは同じように動作します)。
g3 = function()
a = next(g3) # a becomes 0
b = next(g3) # b becomes 1
c = next(g3) # c becomes 2
...
j = next(g3) # Raises StopIteration, j remains undefined
Python 2のジェネレータオブジェクトには、生成された値を手動で繰り返し処理するために使用できる.next()
メソッドがあることに注意してください。 Python 3では、このメソッドはすべてのイテレータに対して.__next__()
標準に置き換えられました。
ジェネレータのリセット
ジェネレータで生成されたオブジェクトは一度しか反復できないことを覚えておいてください。既にスクリプト内のオブジェクトを反復処理している場合は、それ以上試みるとNone
が返されます。
ジェネレータで生成されたオブジェクトを複数回使用する必要がある場合は、ジェネレータ関数を再度定義して使用するか、または最初に使用するときにジェネレータ関数の出力をリストに格納することができます。大量のデータを扱う場合は、ジェネレータ関数を再定義することをお勧めします。すべてのデータ項目のリストを格納すると、多くのディスク領域が必要になります。逆に、最初にアイテムを生成するのに費用がかかる場合は、生成されたアイテムをリストに保存して再利用できるようにすることができます。
発電機を使ってフィボナッチ数を見つける
ジェネレータの実際の使用例は、無限級数の値を反復することです。ここでは、 フィボナッチシーケンスの最初の10の項を見つける例を示します。
def fib(a=0, b=1):
"""Generator that yields Fibonacci numbers. `a` and `b` are the seed values"""
while True:
yield a
a, b = b, a + b
f = fib()
print(', '.join(str(next(f)) for _ in range(10)))
0,1,1,2,3,5,8,13,21,34
無限のシーケンス
ジェネレータは、無限のシーケンスを表すために使用できます。
def integers_starting_from(n):
while True:
yield n
n += 1
natural_numbers = integers_starting_from(1)
上記のような数字の無限のシーケンスは、 itertools.count
の助けを借りて生成することもできます。上記のコードは以下のように書くことができます
natural_numbers = itertools.count(1)
無限のジェネレータでジェネレータを使用すると、新しいジェネレータを生成できます。
multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)
無限のジェネレータは終了しないので、ジェネレータを完全に消費しようとする関数に渡すと、 致命的な結果になることに注意してください。
list(multiples_of_two) # will never terminate, or raise an OS-specific error
代わりに、 range
(またはpython <3.0)の場合はxrange
を使用してリスト/集合のxrange
を使用します。
first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)]
# [3, 6, 9, 12, 15]
itertools.islice()
部分集合にスライスするにはitertools.islice()
を使用します。
from itertools import islice
multiples_of_four = (x * 4 for x in integers_starting_from(1))
first_five_multiples_of_four = list(islice(multiples_of_four, 5))
# [4, 8, 12, 16, 20]
元のジェネレータも同じ "ルート"から来ている他のすべてのジェネレータと同じように更新されていることに注意してください。
next(natural_numbers) # yields 16
next(multiples_of_two) # yields 34
next(multiples_of_four) # yields 24
無限のシーケンスはfor
ループを使って反復することもできます。ループが最終的に終了するように、条件付きbreak
文を必ず含めてください。
for idx, number in enumerate(multiplies_of_two):
print(number)
if idx == 9:
break # stop after taking the first 10 multiplies of two
古典的な例 - フィボナッチ数
import itertools
def fibonacci():
a, b = 1, 1
while True:
yield a
a, b = b, a + b
first_ten_fibs = list(itertools.islice(fibonacci(), 10))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
def nth_fib(n):
return next(itertools.islice(fibonacci(), n - 1, n))
ninety_nineth_fib = nth_fib(99) # 354224848179261915075
別の反復可能な値からすべての値を返す
別の反復可能な値からすべての値を取得する場合は、 yield from
使用します。
def foob(x):
yield from range(x * 2)
yield from range(2)
list(foob(5)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]
これはジェネレータも同様に動作します。
def fibto(n):
a, b = 1, 1
while True:
if a >= n: break
yield a
a, b = b, a + b
def usefib():
yield from fibto(10)
yield from fibto(20)
list(usefib()) # [1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]
コルーチン
ジェネレータは、コルーチンを実装するために使用できます。
# create and advance generator to the first yield
def coroutine(func):
def start(*args,**kwargs):
cr = func(*args,**kwargs)
next(cr)
return cr
return start
# example coroutine
@coroutine
def adder(sum = 0):
while True:
x = yield sum
sum += x
# example use
s = adder()
s.send(1) # 1
s.send(2) # 3
コルーチンは、ステートマシンを実装するためによく使用されます。ステートマシンは、ステートマシンが正常に機能するために必要なシングルメソッドプロシージャを作成する場合に主に役立ちます。それらは既存の状態で動作し、操作の完了時に取得された値を返します。
再帰による収穫:ディレクトリ内のすべてのファイルを再帰的にリストする
まず、ファイルで動作するライブラリをインポートします。
from os import listdir
from os.path import isfile, join, exists
ディレクトリからファイルを読み込むヘルパー関数:
def get_files(path):
for file in listdir(path):
full_path = join(path, file)
if isfile(full_path):
if exists(full_path):
yield full_path
サブディレクトリのみを取得するもう1つのヘルパー関数:
def get_directories(path):
for directory in listdir(path):
full_path = join(path, directory)
if not isfile(full_path):
if exists(full_path):
yield full_path
これらの関数を使用して、ディレクトリ内のすべてのファイルとそのすべてのサブディレクトリ(ジェネレータを使用)を再帰的に取得します。
def get_files_recursive(directory):
for file in get_files(directory):
yield file
for subdirectory in get_directories(directory):
for file in get_files_recursive(subdirectory): # here the recursive call
yield file
この機能はyield from
使用しyield from
簡略化できます。
def get_files_recursive(directory):
yield from get_files(directory)
for subdirectory in get_directories(directory):
yield from get_files_recursive(subdirectory)
ジェネレータを並行して反復する
複数のジェネレータを並列処理するには、組み込み関数zip
使用します。
for x, y in zip(a,b):
print(x,y)
結果:
1 x
2 y
3 z
Python 2ではitertools.izip
代わりに使うべきです。ここでも、すべてのzip
関数がタプルを生成することがわかります。
zipは、iterablesの1つが項目を使い切るとすぐに反復を停止することに注意してください。最も長いiterableまで繰り返したい場合は、 itertools.zip_longest()
使用してitertools.zip_longest()
。
リスト構築コードのリファクタリング
空リストから始めて繰り返し追加することで、リストを作成して返す複雑なコードがあるとします。
def create():
result = []
# logic here...
result.append(value) # possibly in several places
# more logic...
return result # possibly in several places
values = create()
内部論理をリストの理解に置き換えることが現実的でない場合は、関数全体をジェネレーターに置き換えて結果を収集することができます。
def create_gen():
# logic...
yield value
# more logic
return # not needed if at the end of the function, of course
values = list(create_gen())
ロジックが再帰的である場合は、 yield from
を使用して、再帰呼び出しのすべての値を「フラット化された」結果に含めます。
def preorder_traversal(node):
yield node.value
for child in node.children:
yield from preorder_traversal(child)
検索
next
関数は反復処理をしなくても便利です。ジェネレータ式をnext
渡すことは、ある述語にマッチする要素の最初のオカレンスを迅速に検索する方法です。プロシージャのようなコード
def find_and_transform(sequence, predicate, func):
for element in sequence:
if predicate(element):
return func(element)
raise ValueError
item = find_and_transform(my_sequence, my_predicate, my_func)
次のように置き換えることができます。
item = next(my_func(x) for x in my_sequence if my_predicate(x))
# StopIteration will be raised if there are no matches; this exception can
# be caught and transformed, if desired.
この目的のために、 first = next
ようなエイリアス、または例外を変換するためのラッパー関数を作成することが望ましい場合があります。
def first(generator):
try:
return next(generator)
except StopIteration:
raise ValueError