Courses
イテレーターは反復処理できるオブジェクトです。Python言語の共通機能として、ループやリスト内包表記の裏側で活躍します。イテレーターを生成できるオブジェクトはイテラブルと呼ばれます。
イテレーターの構築には多くの作業が伴います。たとえば、各イテレーターオブジェクトの実装には __iter__() と __next__() メソッドが必須です。さらにオブジェクトの内部状態を追跡し、返す値がなくなったら StopIteration 例外を送出する仕組みも必要です。これらの規則はイテレーター・プロトコルとして知られています。
独自のイテレーターを実装するのは手間がかかり、常に必要というわけではありません。より簡単な代替がジェネレーターです。ジェネレーターは yield キーワードを使って、1回に1つずつ値を返せるイテレーターを生成する特別な種類の関数です。
どの場面でイテレーターを実装し、どの場面でジェネレーターを使うべきかを見極められるようになると、Pythonプログラマーとしてのスキルが向上します。本チュートリアルの残りでは両者の違いに焦点を当て、状況に応じて最適な選択ができるようにします。
用語集
|
用語 |
定義 |
|
イテラブル |
ループで繰り返し処理(反復)できるPythonオブジェクト。リスト、セット、タプル、辞書、文字列などが例として挙げられます。 |
|
イテレーター |
反復処理できるオブジェクト。つまり、イテレーターは数え上げ可能な値の集合を持ちます。 |
|
ジェネレーター |
単一の値を返さず、値の列を持つイテレーターオブジェクトを返す特別な種類の関数。 |
|
遅延評価 |
必要になったときにだけ特定のオブジェクトを生成する評価戦略。そのため、一部の開発者コミュニティでは「call-by-need」とも呼ばれます。 |
|
イテレーター・プロトコル |
Pythonでイテレーターを定義する際に従うべき一連の規則。 |
|
next() |
イテレーターから次の要素を返すための組み込み関数。 |
|
iter() |
イテラブルをイテレーターに変換するための組み込み関数。 |
|
yield() |
|
Pythonのイテレーターとイテラブル
イテラブルは、その要素を1つずつ返せるオブジェクトです。つまり反復処理が可能です。リスト、タプル、セットのような一般的なPythonのデータ構造はイテラブルに該当します。文字列や辞書もイテラブルです。文字列は文字の反復ができ、辞書はキーを反復できます。経験則として、forループで反復できるオブジェクトはイテラブルと考えてください。
例で学ぶPythonのイテラブル
定義から、すべてのイテレーターはイテラブルでもあると結論づけられます。しかし、すべてのイテラブルがイテレーターとは限りません。イテラブルは、実際に反復が始まって初めてイテレーターを生成します。
この機能を示すため、イテラブルであるリストをインスタンス化し、組み込み関数 iter() を呼び出してイテレーターを生成します。
list_instance = [1, 2, 3, 4]
print(iter(list_instance))
"""
<list_iterator object at 0x7fd946309e90>
"""
リスト自体はイテレーターではありませんが、iter() を呼ぶとイテレーターに変換され、そのイテレーターオブジェクトが返されます。
すべてのイテラブルがイテレーターではないことを示すため、同じリストオブジェクトを生成し、イテレーターの次の要素を返す next() 関数を呼び出してみます。
list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
3 print(iter(list_instance))
4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""
上のコードでは、リストに対して next() を呼び出そうとして TypeError が発生しています(Pythonの例外とエラー処理も参照)。これは、リストオブジェクトはイテラブルであってイテレーターではないという単純な理由によるものです。
例で学ぶPythonのイテレーター
したがって、リストを手動で反復したい場合は、まずイテレーターオブジェクトを生成する必要があります。そうして初めて、リストの値を順に取り出せます。
# instantiate a list object
list_instance = [1, 2, 3, 4]
# convert the list to an iterator
iterator = iter(list_instance)
# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
Pythonは、イテラブルオブジェクトをループしようとすると自動的にイテレーターオブジェクトを生成します。
# instantiate a list object
list_instance = [1, 2, 3, 4]
# loop through the list
for item in list_instance:
print(item)
"""
1
2
3
4
"""
StopIteration 例外が捕捉されると、ループは終了します。
イテレーターから得られる値は左から右へ順にしか取得できません。Pythonにはイテレーターを逆方向に進める previous() 関数はありません。
イテレーターの遅延性
同じイテラブルオブジェクトから複数のイテレーターを定義することができます。各イテレーターは独自の進捗状態を保持します。したがって、同じイテラブルのイテレーターを複数作れば、一方のインスタンスを最後まで進めても、もう一方は開始位置のままにしておけます。
list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""
iterator_b が系列の最初の要素を出力していることに注目してください。
このように、イテレーターには遅延的な性質があると言えます。イテレーターが作成されても、要素は要求されるまで生成されません。言い換えると、next(iter(list_instance)) のように明示的に要求したときにだけ、リストの要素が返されます。
ただし、イテレーターに対して組み込みのイテラブルコンテナ(list()、set()、tuple() など)を呼び出せば、すべての値を一度に取り出すこともできます。これによりイテレーターに全要素の生成を強制します。
# instantiate iterable
list_instance = [1, 2, 3, 4]
# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
しかし大きなイテレーターに対しては推奨されません。すべての要素を一度に生成してメモリに保持することになり、遅延評価の利点が損なわれるためです。
データセットがメモリに収まりきらないほど大きい場合や、完全なイテレータークラスを書かずに遅延的な反復を行いたい場合は、通常ジェネレーターの方が適しています。
Pythonのジェネレーター
イテレーターを実装する最も手っ取り早い代替手段はジェネレーターを使うことです。ジェネレーターは見た目こそ普通のPython関数に似ていますが、性質は異なります。まず、ジェネレーターオブジェクトは項目を直接「返す」のではありません。代わりに yield キーワードを使って、その場で項目を生成します。つまり、ジェネレーターは遅延評価を活用する特別な関数だと言えます。
ジェネレーターは、一般的なイテラブルのように内容をメモリに保持しません。たとえば、正の整数のすべての約数を求めたい場合、通常は次のような従来の関数(詳しくはこのPython関数のチュートリアルを参照)を実装するでしょう。
def factors(n):
factor_list = []
for val in range(1, n+1):
if n % val == 0:
factor_list.append(val)
return factor_list
print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""
上のコードは約数のリスト全体を返します。これを従来の関数ではなくジェネレーターで実装すると違いがわかります。
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
print(factors(20))
"""
<generator object factors at 0x7fd938271350>
"""
return の代わりに yield を使ったため、関数は即座に終了しません。実質的にPythonに対し、従来の関数ではなくジェネレーターオブジェクトを作成するよう指示しており、その状態が追跡されます。
その結果、遅延的なイテレーターに対して next() 関数を呼び出し、系列の要素を1つずつ表示できます。
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
factors_of_20 = factors(20)
print(next(factors_of_20))
"""
1
"""
ジェネレーターを作るもう1つの方法はジェネレーター内包表記を使うことです。構文はリスト内包表記に似ていますが、角かっこではなく丸かっこを使います。
factor_gen = (val for val in range(1, 21) if 20 % val == 0)
print(list(factor_gen))
"""
[1, 2, 4, 5, 10, 20]
"""
Pythonのyieldキーワードを探る
yield キーワードはジェネレーター関数の制御フローを司ります。return のように関数を終了させるのではなく、yield は関数を一旦抜けつつもローカル変数の状態を保持します。
yield の呼び出しから返されるジェネレーターは変数に代入でき、next() 関数で反復できます。これは関数を最初に出現する yield まで実行します。yield に到達すると関数の実行は中断され、その時点の状態が保存されます。したがって、任意のタイミングで実行を再開できます。
関数は yield の位置から処理を続行します。例を示します。
def yield_multiple_statements():
yield "This is the first statement"
yield "This is the second statement"
yield "This is the third statement"
yield "This is the last statement. Don't call next again!"
example = yield_multiple_statements()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statement
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
11 print(next(example))
12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""
上のコードでは、ジェネレーターに4つの yield 呼び出しがありますが、5回 next を呼ぼうとしているため StopIteration 例外が発生しました。これは、ジェネレーターが無限列ではないため、想定回数を超えて呼び出すと尽きてしまうためです。
まとめ
要点を振り返ると、イテレーターは反復可能なオブジェクトであり、ジェネレーターは遅延評価を活用する特別な関数です。独自のイテレーターを実装する場合は __iter__() と __next__() を作る必要がありますが、ジェネレーターはPython関数や内包表記で yield キーワードを用いれば実装できます。
複雑な状態管理を伴うオブジェクトが必要な場合、または __next__()、__iter__()、__init__() 以外のメソッドも公開したい場合は、ジェネレーターより独自のイテレーターを選ぶほうがよいでしょう。一方で、内容をメモリに保持しないため大規模データの処理や、わざわざイテレーターを実装する必要がない場面では、ジェネレーターのほうが適しています。
FAQS
Pythonにおけるイテレーターとジェネレーターの違いは何ですか?
イテレーターは __iter__() と __next__() を実装する任意のオブジェクトです。ジェネレーターは yield キーワードを使った関数でイテレーターを簡単に作る方法です。すべてのジェネレーターはイテレーターですが、すべてのイテレーターがジェネレーターとは限りません。
Pythonでリストの代わりにジェネレーターを使うのはいつですか?
大きな(あるいは無限の)シーケンスやメモリ効率が重要な場合はジェネレーターを使ってください。リストはすべての要素を一度にメモリに保持しますが、ジェネレーターは1つずつ生成します。再利用する小さなデータセットなら、リストで問題ありません。
Pythonのyieldキーワードは何をしますか?
yield キーワードは関数をジェネレーターに変えます。yield は値を返して関数を一時停止し、状態を記憶して次回の呼び出し時に続きから実行できるようにします。
Pythonでジェネレーターはどうやって作りますか?
return の代わりに yield を使う関数を書くか、リスト内包表記と同じ構文で括弧だけを丸かっこにしたジェネレーター式を使います(例:(x * 2 for x in range(10)))。
Pythonでジェネレーターはイテレーターより速いですか?
生の実行速度では速くありませんが、必要なときに値を生成するためメモリ効率が高いです。大規模データではその分トータルの性能が向上することが多く、小規模データでは差はほとんどありません。