Pythonのyieldが分からない新人エンジニア必見!ジェネレータの基本を徹底解説
こんにちは。ゆうせいです。
Pythonを学び始めると、yieldというキーワードに出会って「これってreturnと何が違うの?」と首をかしげた経験はありませんか?関数から値を返すという点では似ていますが、その裏側にある仕組みは全くの別物なんです。
このyieldを理解すると、メモリを効率的に使ったスマートなコードが書けるようになります。特に、大きなデータを扱う際には必須の知識と言えるでしょう。
今回は、そんなyieldの謎を解き明かし、皆さんが自信を持って使いこなせるようになることを目指します!
yieldを学ぶ前に:関数のreturnをおさらい
まず、yieldと比較するために、おなじみのreturnの働きを思い出してみましょう。
returnは、関数の中での処理を完全に終了させ、指定した値を呼び出し元に返す役割を持っています。
例えるなら、returnは「自動販売機」のようなものです。
あなたがボタンを押して(関数を呼び出して)100円を入れると、ジュースがガコンと出てきます(値が返ってきます)。ジュースを受け取ったら、その自動販売機とのやり取りは完全に終わりですよね。自動販売機は、あなたが次にいつ来るかなんて覚えていません。
def get_numbers_return():
numbers = []
for i in range(3):
numbers.append(i)
return numbers
# 関数を呼び出してリストを受け取る
my_list = get_numbers_return()
# リストの内容を表示
for num in my_list:
print(num)
# 出力結果:
# 0
# 1
# 2
このコードでは、get_numbers_return関数は、まず空のリストnumbersを用意し、そこに0, 1, 2という3つの数字をすべて追加してから、最後に完成したリストをreturnしています。関数の処理は、このreturnの時点できっぱり終了です。
本題:yieldとは?処理を一時停止する魔法のしおり
さて、いよいよ本題のyieldです。
yieldも関数から値を返しますが、returnと決定的に違うのは「関数の処理を終了させずに、一時停止する」という点です。
これを先ほどの例えで言うなら、yieldは「長編小説としおり」の関係に似ています。
あなたは小説を1章読み終えるたびに(yieldで値を1つ返すたびに)、しおりを挟んで(処理を一時停止して)、一旦本を閉じます。そして、また読みたくなったら、しおりを挟んだページから(停止した次の行から)続きを読むことができますよね。小説(関数)は、あなたがどこまで読んだか(処理の状態)を覚えています。
このように、yieldが使われた関数は「ジェネレータ関数」と呼ばれ、呼び出すと「ジェネレータ」という特別なオブジェクトを返します。このジェネレータが、処理の状態を記憶し、値を一つずつ生成してくれるのです。
def get_numbers_yield():
for i in range(3):
yield i
# 関数を呼び出すとジェネレータが返ってくる
my_generator = get_numbers_yield()
# ジェネレータから値を一つずつ取り出す
for num in my_generator:
print(num)
# 出力結果:
# 0
# 1
# 2
get_numbers_yield関数を見てください。returnのようにリストを作っていませんね。
この関数は、forループで呼び出されるたびにyield iを実行し、iの値を1つだけ返してその場で処理を中断します。そして、次の値が要求されると、中断した場所から処理を再開するのです。
yieldを使うメリット:なぜ便利なの?
yieldを使う最大のメリットは、メモリの節約です。
メモリ効率が非常に良い
もし、1から100万までの数字が入ったリストが必要になったと想像してみてください。
returnを使う方法だと、まず100万個の数字をすべて格納するための巨大なリストをメモリ上に確保しなければなりません。これは、たとえるなら「100万本のペットボトルを一気に買ってきて、自分の部屋に全部保管しようとする」ようなものです。部屋がパンクしてしまいますよね。
一方で、yieldを使うジェネレータならどうでしょう?
ジェネレータは、一度に1つの数字しか作り出しません。次の数字が必要になったときに、その場で計算して値を返します。これは、「喉が渇くたびに、蛇口をひねってコップ1杯分の水を飲む」ようなものです。保管場所はコップ1杯分で済みます。
このように、yieldは巨大なデータセットや、終わりのないデータ(例えば、サーバーのログなど)を扱う際に、メモリを圧迫することなく効率的に処理できる、非常に強力な武器なのです。
yieldを使うデメリット:注意すべき点
もちろん、yieldにも注意すべき点があります。
一度しか使えない使い捨て
ジェネレータは、一度最後まで値を取り出してしまうと、もう空っぽになってしまいます。
先ほどの「長編小説」の例えで言えば、最後まで読み終えてしまったら、もう続きを読むページはありませんよね。もう一度最初から読みたい場合は、新しい本(新しいジェネレータオブジェクト)を用意する必要があります。
my_generator = get_numbers_yield()
# 1回目:最後まで値を取り出す
print("1回目のループ")
for num in my_generator:
print(num)
# 2回目:もう値は出てこない
print("2回目のループ")
for num in my_generator:
print(num) # 何も出力されない
特定の要素に直接アクセスできない
リストであればmy_list[10]のようにインデックスを指定して10番目の要素を直接取り出せますが、ジェネレータではそれができません。5番目の要素が欲しければ、1番目から順番に4つ値を取り出して、5番目にたどり着くしかないのです。
今後の学習の指針
yieldとジェネレータの基本が分かったら、次は何を学ぶと良いでしょうか?
- ジェネレータ式:リスト内包表記に似た、より簡潔な書き方でジェネレータを作成する方法です。コードがスッキリしますよ。my_generator = (i for i in range(10))
- itertoolsモジュール:Pythonの標準ライブラリには、ジェネレータを効率的に扱うための便利なツールがたくさん詰まったitertoolsというモジュールがあります。組み合わせや順列を簡単に作れるなど、強力な機能が満載です。
- コルーチンと非同期処理:少し高度なトピックになりますが、yieldの「処理を一時停止・再開する」という概念は、async/awaitといった非同期処理の根幹をなす技術にも繋がっています。いつか挑戦してみてください。
yieldは、一見すると少しとっつきにくいかもしれませんが、その仕組みとメリットを理解すれば、あなたの書くコードをよりエレガントで効率的なものに変えてくれます。
ぜひ、実際のコードで小さなジェネレータをたくさん作って、その動きを体感してみてください!