Python の yield
キーワードはジェネレータを作成するために使用されます。ジェネレータとは、その場その場でアイテムを生成し、一度だけ反復処理できるコレクションの一種です。ジェネレータを使用することで、アプリケーションのパフォーマンスを向上させることができ、通常のコレクションと比較してより少ないメモリを消費するため、パフォーマンスの良いブーストを提供します。
この記事では、Pythonの yield
キーワードの使い方と、それが具体的に何をするものなのかを説明します。その前に、単純なリストコレクションとジェネレータの違いを勉強し、次に yield
を使ってより複雑なジェネレータを作成する方法を見ていきましょう。
リストとジェネレータの違い
以下のスクリプトでは、リストとジェネレータの両方を作成し、両者の違いを確認します。まず、簡単なリストを作り、その型をチェックする。
# Creating a list using list comprehension
squared_list = [x**2 for x in range(5)]
# Check the type
type(squared_list)
このコードを実行すると、表示される型が「リスト」であることがわかるはずである。
では、squared_list
に含まれるすべての項目を反復処理してみましょう。
# Iterate over items and print them
for number in squared_list:
print(number)
上記のスクリプトは以下の結果を生成します。
$ python squared_list.py
0
1
4
9
16
次に、ジェネレーターを作成して、まったく同じタスクを実行してみましょう。
# Creating a generator
squared_gen = (x**2 for x in range(5))
# Check the type
type(squared_gen)
ジェネレータを作るには、リスト内包と全く同じように始めますが、角括弧の代わりに括弧を使わなければなりません。上のスクリプトでは、変数 squared_gen
の型として “generator” と表示されます。それでは、for-loopを使ってジェネレータを反復処理してみましょう。
for number in squared_gen:
print(number)
出力は次のようになる。
$ python squared_gen.py
0
1
4
9
16
出力はリストと同じです。では、何が違うのでしょうか?主な違いの1つは、リストとジェネレータがメモリに要素を格納する方法にあります。リストはすべての要素を一度にメモリに格納しますが、ジェネレータは各要素をその場で「生成」して表示し、次の要素に移動して前の要素をメモリから破棄します。
このことを確認する一つの方法は、今作成したリストとジェネレータの両方の長さをチェックすることです。len(squared_list)は5を返しますが、
len(squared_gen)` はジェネレータに長さがないというエラーを投げます。また、リストに対しては何度でも繰り返し処理を行うことができますが、ジェネレータに対しては一度しか繰り返し処理を行うことができません。もう一度繰り返すには、ジェネレータをもう一度作らなければなりません。
Yieldキーワードの使用
さて、単純なコレクションとジェネレータの違いがわかったところで、 yield
がジェネレータの定義にどのように役立つかを見てみましょう。
これまでの例では、リスト内包のスタイルを使って暗黙のうちにジェネレータを作成しました。しかし、より複雑なシナリオでは、ジェネレータを返す関数を作成することができます。yieldキーワードは
return` ステートメントとは異なり、通常の Python 関数をジェネレータに変換するために使用されます。これは、リスト全体を一度に返す代わりに使用します。これについても、いくつかの簡単な例を使って説明します。
もう一度、yield
キーワードを使わない場合、関数が何を返すか見てみましょう。次のスクリプトを実行してください。
def cube_numbers(nums):
cube_list =[]
for i in nums:
cube_list.append(i**3)
return cube_list
cubes = cube_numbers([1, 2, 3, 4, 5])
print(cubes)
このスクリプトでは、数字のリストを受け取り、その立方体を取り出して、リスト全体を呼び出し元に返す関数 cube_numbers
が作成されています。この関数が呼ばれると、立方体のリストが返され、変数 cubes
に格納される。出力から、返されたデータが実際に完全なリストであることがわかります。
$ python cubes_list.py
[1, 8, 27, 64, 125]
では、リストを返す代わりに、上のスクリプトを修正してジェネレータを返すようにしてみよう。
def cube_numbers(nums):
for i in nums:
yield(i**3)
cubes = cube_numbers([1, 2, 3, 4, 5])
print(cubes)
上のスクリプトでは、cube_numbers
関数は立方数のリストの代わりにジェネレータを返します。ジェネレータを作成するには、yield
キーワードを使用すると簡単です。ここでは、立方数を格納するための一時的な変数 cube_list
は必要ないので、 cube_numbers
メソッドもよりシンプルになります。また、return
文は必要なく、代わりに yield
キーワードを使って、for-loop の中で立方体数を返しています。
これで、cube_number
関数が呼ばれると、ジェネレータが返されるようになり、コードを実行することで確認することができます。
$ python cubes_gen.py
<generator 0x1087f1230="" at="" cube_numbers="" object="">
cube_numbers` 関数を呼び出しても、この時点ではまだ実行されていませんし、メモリに格納されている項目もありません。
関数を実行し、ジェネレータから次のアイテムを取得するために、組み込みの next
メソッドを使用します。ジェネレータの next
イテレータを初めて呼び出したとき、 yield
キーワードに出会うまで関数が実行されます。yield` が見つかると、それに渡された値が呼び出した関数に返され、ジェネレータ関数は現在の状態で一時停止します。
以下は、ジェネレータから値を取得する方法です。
next(cubes)
上の関数は “1” を返します。ここで再びジェネレータで next
を呼び出すと、cube_numbers
関数は yield
で停止していたところから実行を再開します。この関数は、再び yield
を見つけるまで実行し続けます。next` 関数は、リスト内のすべての値が反復処理されるまで、立方体の値を 1 つずつ返し続けます。
すべての値が反復処理されると、 next
関数は StopIteration 例外をスローします。重要なのは、cubes
ジェネレータはこれらのアイテムをメモリに保存していないことです。使用される余分なメモリはジェネレータ自体の状態データだけで、それは通常大きなリストよりもずっと少ないものです。このため、ジェネレータはメモリを大量に消費するタスクに理想的です。
常に next
イテレータを使用する代わりに、”for” ループを使用してジェネレータの値を反復処理することができます。for” ループを使用する場合、裏側では、ジェネレータ内のすべてのアイテムが反復処理されるまで next
イテレータが呼び出されます。
最適化されたパフォーマンス
前述したように、ジェネレータはメモリにコレクション項目をすべて保存する必要がなく、むしろその場で項目を生成し、イテレータが次の項目に移動するとすぐにそれを破棄するので、メモリ集約型のタスクになると非常に便利です。
前の例では、リストサイズが小さかったため、単純なリストとジェネレータの性能差は見えませんでした。このセクションでは、リストとジェネレータの性能を見分けることができるいくつかの例について確認します。
以下のコードでは、100万個のダミーオブジェクト car
を含むリストを返す関数を書いています。この関数を呼び出す前と呼び出した後(リストを作成する前)で、プロセスが占有するメモリを計算します。
以下のコードを見てください。
import time
import random
import os
import psutil
car_names = ['Audi', 'Toyota', 'Renault', 'Nissan', 'Honda', 'Suzuki']
colors = ['Black', 'Blue', 'Red', 'White', 'Yellow']
def car_list(cars):
all_cars = []
for i in range(cars):
car = {
'id': i,
'name': random.choice(car_names),
'color': random.choice(colors)
}
all_cars.append(car)
return all_cars
# Get used memory
process = psutil.Process(os.getpid())
print('Memory before list is created: ' + str(process.memory_info().rss/1000000))
# Call the car_list function and time how long it takes
t1 = time.clock()
cars = car_list(1000000)
t2 = time.clock()
# Get used memory
process = psutil.Process(os.getpid())
print('Memory after list is created: ' + str(process.memory_info().rss/1000000))
print('Took {} seconds'.format(t2-t1))
Note: このコードをあなたのマシンで動作させるには、 pip install psutil
が必要かもしれません。
このコードを実行したマシンでは、次のような結果が得られました(あなたのマシンでは少し違うかもしれません)。
$ python perf_list.py
Memory before list is created: 8
Memory after list is created: 334
Took 1.584018 seconds
リストを作成する前のプロセスメモリは8MBでしたが、100万項目のリストを作成した後、占有メモリは334MBに跳ね上がりました。また、リストの作成に要した時間は1.58秒でした。
では、上記の処理を繰り返しますが、リストをジェネレータに置き換えてみましょう。次のスクリプトを実行する。
import time
import random
import os
import psutil
car_names = ['Audi', 'Toyota', 'Renault', 'Nissan', 'Honda', 'Suzuki']
colors = ['Black', 'Blue', 'Red', 'White', 'Yellow']
def car_list_gen(cars):
for i in range(cars):
car = {
'id':i,
'name':random.choice(car_names),
'color':random.choice(colors)
}
yield car
# Get used memory
process = psutil.Process(os.getpid())
print('Memory before list is created: ' + str(process.memory_info().rss/1000000))
# Call the car_list_gen function and time how long it takes
t1 = time.clock()
for car in car_list_gen(1000000):
pass
t2 = time.clock()
# Get used memory
process = psutil.Process(os.getpid())
print('Memory after list is created: ' + str(process.memory_info().rss/1000000))
print('Took {} seconds'.format(t2-t1))
ここでは、1000000台すべての車が実際に生成されるように、for car in car_list_gen(1000000)
ループを使用する必要があります。
上記のスクリプトを実行した結果、以下のような結果が得られた。
$ python perf_gen.py
Memory before list is created: 8
Memory after list is created: 40
Took 1.365244 seconds
出力から、ジェネレータを使うことによって、メモリに項目を保存しないので、メモリの差が以前よりずっと小さくなっていることがわかる(8 MBから40 MB)。さらに、ジェネレータ関数の呼び出しにかかる時間も1.37秒と、リスト作成より14%ほど早くなっています。
結論
この記事から、yield
キーワードがどのように使われ、何のために使われ、なぜそれを使いたいのかを含めて、よりよく理解していただけたと思います。Pythonジェネレータはプログラムのパフォーマンスを向上させる素晴らしい方法であり、使うのはとても簡単ですが、いつ使うのかを理解することは多くの初心者プログラマーにとって課題です。
。