【初心者にわかりやすく】Pythonのyield文とジェネレータの基本的な考え方を解説

Pythonのyield文とジェネレータの基本を初心者にわかりやすく解説

今回もプログラミング言語Python(パイソン)におけるわかりにく文法事項の1つ、yield文とジェネレータについてその基本的な考え方を解説します。

(1) 必須の前提知識:イテレータ

yield文とジェネレータを理解するためには、まず、以下の2つの理解が必要です。

  • イテレータオブジェクト
  • イテラブルなオブジェクト

言葉は似ていますが、両者をしっかりと区別して理解していることが、yield文とジェネレータの理解に必須となります。

ですが、この2つの違いについては「Pythonのイテレータについて、その基本とfor文の仕組みをわかりやすく解説」という記事でしっかりと解説していますので、こちらを読んで見てください。

簡単にその内容をまとめると、

イテレータオブジェクトとは、

  • __iter__()メソッド
  • __next__()メソッド

この2つを同時に持っているオブジェクトのことです。一方、乱暴な説明ではありますが、__iter__()メソッドは持っているが、__next__()メソッドは持っていないものが「イテラブルなオブジェクト」です。

この違いをはっきりと認識しているだけで、yield文とジェネレータの基本的理解がかなり容易になります。

(2) イメージ:作業は小分けにしたほうが効率的なことがある

まずプログラミングに限らずなんでも、世の中の作業というのは一気にまとめてやったほうがいい場合と、小分けにして少しずつ進めたほうがいい場合の2通りの場合がありますよね。

たとえば、こまごまとした小物を運ぶ場合は1つの大きな箱にいれて一気に運んだほうが効率的・合理的ですよね。

しかし、小さいものの重いアイテムを運ぶ場合はどうでしょう?そういったものを複数まとめて運ぶのはそもそも負担が大きすぎて不可能だったり、途中で休憩をいれてゆっくりと運ばないと体力がなくなり倒れてしまいますよね。

これはコンピュータや機械の世界でも同じで、パソコンや機械に負担の大きな作業というのは、1回1回小分けにして少しずつ処理をさせたほうが結果的に効率的・合理的なことがあります。あまり負担の大きなことを一気にやらせると機械そのものが壊れることもありますよね。

このように作業を小分けにして1回1回少しずつ進めるための仕組みが、Pythonにも用意されています。それがyield文とジェネレータです。

今回はまず以下の簡単なコードを例にして考えていきましょう。

(3) コード例

#1 てきとうな関数を定義:整数を二倍する関数
def fnc_generated(number):
    for i in range(number):
        i=i*2
        yield i

#2 新しいイテレータオブジェクト(変数名new_itrtr)を作成
new_itrtr=fnc_generated(5)

#3 new_itrtrが、__next__()メソッドを持っているかの確認
dir(new_itrtr) #メソッド一覧に、__next__が出てくる

#4 上でつくったイテレータオブジェクトの型を調べる

print(type(new_itrtr))

#5 以下のものを1つずつ実行していくと出力される数字が変わる。最後はエラーが出る

next(new_itrtr) # 0(ゼロ)が出力される
next(new_itrtr) # 2が出力される
next(new_itrtr) # 4が出力される
next(new_itrtr) # 6が出力される
next(new_itrtr) # 8が出力される
next(new_itrtr) # StopIterationエラーがでる

引数で指定した「5」が、関数のnumberに代入され、for文でrange(5)となり、「0, 1, 2, 3, 4」という整数のリストが作成されます。

そしてその各数字ごとに順番に2倍された数字が出力されるというものです。つまり、「0, 2, 4, 6, 8」と順番に出力されます。

(4) 解説

yield文は、通常の関数をジェネレータ関数という特殊な関数へ変化させる

まずyield文は関数内で使います。今回はfnc_generatedという関数内で使っています。そしてそこからどういった処理がなされているのかを今から書いていきます。

まずyield文によって、 fnc_generated関数が「ジェネレータ関数」という特別な関数へ変化します。

ジェネレータ関数はジェネレータイテレータオブジェクトを作る


そしてジェネレータ関数は、イテレータの一種である「ジェネレータイテレータオブジェクト」を作成します。これがコード例でいうと#3の部分です。(ジェネレータイテレータオブジェクトという名称が正式名かは不明です。イテレータオブジェクトの1種であると強調したいために便宜的にそういう表現にしています)

#2 新しいイテレータオブジェクト(変数名new_itrtr)を作成
new_itrtr=fnc_generated(5)

ここではnew_itrtrというジェネレータイテレータオブジェクトを作成しています。引数には「5」を設定しています。

ここで大切なのは、ジェネレータイテレータオブジェクトはあくまでイテレータオブジェクトの1種ですから、上述(1)で書いたように

  • __iter__()メソッド
  • __next__()メソッド

の2つを持っているということです。

特に__next__()メソッドを持っているという意識は非常に重要です。

実際にそれを確かめているのが、#3の

#3 new_itrtrが、__next__()メソッドを持っているかの確認
dir(new_itrtr) #メソッド一覧に、__next__が出てくる


という部分です。何度もいいますが、このnew_itrtrは、イテラータの一種ですので、

  • __iter__()メソッド
  • __next__()メソッド

の2つを持っています。

ジェネレータイテレータオブジェクトが持つ__next__()メソッドを呼び出して使う

さてあとはもうコード例の#4~#5だけが残っていますが、#4についてはもうそのままです。

次に最後のブロック#5について解説していきます。

ここで重要なのは、ジェネレータ関数は、一度にyield文までしか実行しないという点です。
上のコード例でいうと、

   yield i 

最初はここまでしか実行されません。今回は関数内のこのyield文より下に何も書いていませんが、どんな処理を書いていてもこのyield文までです。ここで一旦止まります。そういう点ではbreak文に似ている部分がありますね。

そして次の点にも注意しましょう。

ジェネレータイテレータオブジェクトは、一旦停止した場所を記憶している

これは表現として正しいものではないでしょうが、イメージとしてつかんでおいてください。

この性質があるため、

#5 以下のものを1つずつ実行していくと出力される数字が変わる。最後はエラーが出る

next(new_itrtr) # 0(ゼロ)が出力される
next(new_itrtr) # 2が出力される
next(new_itrtr) # 4が出力される
next(new_itrtr) # 6が出力される
next(new_itrtr) # 8が出力される
next(new_itrtr) # StopIterationエラーがでる

最初の「 next(new_itrtr) # 0(ゼロ)が出力される 」の処理が終わったあと、次の「 next(new_itrtr) # 2が出力される 」が可能となるのです。

どこまで処理を進めたか?これを記憶していないと、2回目、3回目・・・の作業で正しい結果を導くのは無理ですよね。

「あれ?どこまでやったかな?」となるとまた同じことを繰り返してしまうハメにもなりますし、「あれ前回の結果は2だったかな?4だったかな?」となります。

__next__()メソッドは、複数のデータについて、

まず最初の1つのデータについて、処理をして結果を返す。

次に、その次のデータについて、また処理をして結果を返す。

次に、その次のデータについて、また処理をして結果を返す。

・・・・

というメソッドです。

そして、next()関数は、この__next__()メソッドを呼び出すための組み込み関数です。

よって、

最初にnext()関数が__next__()メソッドを呼び出す。i=0が代入され、それが2倍され0が返される。

次にまたnext()関数により、__next__()メソッドが呼ばれるが、どこまで進んだか(つまり次にどこから始めるべきか)という情報が保持されているため、次にi=1が代入され、それが2倍され2が返される。

以下、i=2、i=3・・・となるわけです。

リストオブジェクト(list)との違い

メモリの節約

上の説明で、場合によってはいろんなものを一気にまとめてやるよりも小分けにしたほうがいい場合もあると書きました。

そしてyieldとジェネレータはそれを実現するための仕組みです。

さてリストオブジェクトとの違いですが、リストは最初にまずリストを用意する必要があります。

この点について1人で運ぶのが困難な重い荷物を複数運ぶ場合を考えてください。一度に運べないのに、目の前にドカーンと荷物が置かれていても邪魔なだけじゃないですか。場所を取ります。

これはデータ量の多いリスト(数ギガにもなるようなテキスト情報を持つリストとか)が、最初にメモリを大量に消費してしまうということと同じです。そのために、あとで他のアプリを立ち上げたいと思っても、すでにメモリが大量に占有されている状態のためメモリ不足になる可能性があります。

それを避けるために、そもそもリストを用意しないで、1つずつデータを選んで運んできて、そのうえでそのデータについて処理する、というのがyield文とジェネレータの考え方です。

よって、ジェネレータイテレータオブジェクトはリストオブジェクトではありません。リストオブジェクトの不都合を回避するための仕組みなのに、それがリストオブジェクトと同じだったら無意味になりますよね。

これは、上のコード例だと、

print(new_itrtr[1])

とした場合にエラーが出ることで確認できます。リストオブジェクトだとこういった書き方が可能ですよね。

しかしジェネレータイテレータオブジェクトは添字(サブスクリプト/インデックス)使った書き方はできません。リストオブジェクトではないので、そもそも最初は値を持っていないのです。あくまで呼び出されて処理が進んだときにある値が初めて生成され、それが返ってくるだけです。

__next__()メソッドの有無

またジェネレータイテレータオブジェクトは、あくまでイテレータオブジェクトですから、__iter__()メソッドはもちろん、__next__()メソッドも持っています。しかし、リストオブジェクトは、「イテラブルなオブジェクト」でしかないため、__next__()メソッドは持っていません。

大雑把なまとめですが、このような違いが両者にはあります。

やっぱりプログラミングは独学だとわかりにくいですよね、そこでスクールや動画による解説を利用しましょう!

Tech Academy

厚切りジェイソンのCMでおなじみのプログラミングスクール。自宅でオンライン受講OK。現役のプロが一人一人に専属メンターになってくれ、質問すればすぐに回答。転職保証あり

Udemy

世界最大級のオンライン学習動画サービス。AI・データサイエンスなど最先端のプログラミング講座からビジネススキル講座まで10万以上の講座から選び放題。講師に掲示板から直接に質問もできる。