イテレーターは反復処理できるオブジェクトです。Python言語の共通機能として、ループやリスト内包表記の裏側で活躍します。イテレーターを生成できるオブジェクトはイテラブルと呼ばれます。
イテレーターの構築には多くの作業が伴います。たとえば、各イテレーターオブジェクトの実装には __iter__() と __next__() メソッドが必須です。さらにオブジェクトの内部状態を追跡し、返す値がなくなったら StopIteration 例外を送出する仕組みも必要です。これらの規則はイテレーター・プロトコル として知られています。
独自のイテレーターを実装するのは手間がかかり、常に必要というわけではありません。より簡単な代替がジェネレーターです。ジェネレーターは yield キーワードを使って、1回に1つずつ値を返せるイテレーターを生成する特別な種類の関数です。
どの場面でイテレーターを実装し、どの場面でジェネレーターを使うべきかを見極められるようになると、Pythonプログラマーとしてのスキルが向上します。本チュートリアルの残りでは両者の違いに焦点を当て、状況に応じて最適な選択ができるようにします。
用語集
用語
定義
イテラブル
ループで繰り返し処理(反復)できるPythonオブジェクト。リスト、セット、タプル、辞書、文字列などが例として挙げられます。
イテレーター
反復処理できるオブジェクト。つまり、イテレーターは数え上げ可能な値の集合を持ちます。
ジェネレーター
単一の値を返さず、値の列を持つイテレーターオブジェクトを返す特別な種類の関数。
遅延評価
必要になったときにだけ特定のオブジェクトを生成する評価戦略。そのため、一部の開発者コミュニティでは「call-by-need」とも呼ばれます。
イテレーター・プロトコル
Pythonでイテレーターを定義する際に従うべき一連の規則。
next()
イテレーターから次の要素を返すための組み込み関数。
iter()
イテラブルをイテレーターに変換するための組み込み関数。
yield()
return に似たPythonのキーワードですが、値ではなくジェネレーターオブジェクトを返します。
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のイテレーター
したがって、リストを手動で反復したい場合は、まずイテレーターオブジェクトを生成する必要があります。そうして初めて、リストの値を順に取り出せます。
list_instance = [ 1 , 2 , 3 , 4 ]
iterator = iter ( list_instance)
print ( next ( iterator) )
print ( next ( iterator) )
print ( next ( iterator) )
print ( next ( iterator) )
"""
1
2
3
4
""" 提供:
Pythonは、イテラブルオブジェクトをループしようとすると自動的にイテレーターオブジェクトを生成します。
list_instance = [ 1 , 2 , 3 , 4 ]
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() など)を呼び出せば、すべての値を一度に取り出すこともできます。これによりイテレーターに全要素の生成を強制します。
list_instance = [ 1 , 2 , 3 , 4 ]
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__() 以外のメソッドも公開したい場合は、ジェネレーターより独自のイテレーターを選ぶほうがよいでしょう。一方で、内容をメモリに保持しないため大規模データの処理や、わざわざイテレーターを実装する必要がない場面では、ジェネレーターのほうが適しています。
イテレーターは反復処理できるオブジェクトです。Python言語の共通機能として、ループやリスト内包表記の裏側で活躍します。イテレーターを生成できるオブジェクトはイテラブルと呼ばれます。
イテレーターの構築には多くの作業が伴います。たとえば、各イテレーターオブジェクトの実装には
__iter__()と__next__()メソッドが必須です。さらにオブジェクトの内部状態を追跡し、返す値がなくなったらStopIteration例外を送出する仕組みも必要です。これらの規則はイテレーター・プロトコルとして知られています。独自のイテレーターを実装するのは手間がかかり、常に必要というわけではありません。より簡単な代替がジェネレーターです。ジェネレーターは
yieldキーワードを使って、1回に1つずつ値を返せるイテレーターを生成する特別な種類の関数です。どの場面でイテレーターを実装し、どの場面でジェネレーターを使うべきかを見極められるようになると、Pythonプログラマーとしてのスキルが向上します。本チュートリアルの残りでは両者の違いに焦点を当て、状況に応じて最適な選択ができるようにします。
用語集
用語
定義
イテラブル
ループで繰り返し処理(反復)できるPythonオブジェクト。リスト、セット、タプル、辞書、文字列などが例として挙げられます。
イテレーター
反復処理できるオブジェクト。つまり、イテレーターは数え上げ可能な値の集合を持ちます。
ジェネレーター
単一の値を返さず、値の列を持つイテレーターオブジェクトを返す特別な種類の関数。
遅延評価
必要になったときにだけ特定のオブジェクトを生成する評価戦略。そのため、一部の開発者コミュニティでは「call-by-need」とも呼ばれます。
イテレーター・プロトコル
Pythonでイテレーターを定義する際に従うべき一連の規則。
next()
イテレーターから次の要素を返すための組み込み関数。
iter()
イテラブルをイテレーターに変換するための組み込み関数。
yield()
returnに似たPythonのキーワードですが、値ではなくジェネレーターオブジェクトを返します。Pythonのイテレーターとイテラブル
イテラブルは、その要素を1つずつ返せるオブジェクトです。つまり反復処理が可能です。リスト、タプル、セットのような一般的なPythonのデータ構造はイテラブルに該当します。文字列や辞書もイテラブルです。文字列は文字の反復ができ、辞書はキーを反復できます。経験則として、forループで反復できるオブジェクトはイテラブルと考えてください。
例で学ぶPythonのイテラブル
定義から、すべてのイテレーターはイテラブルでもあると結論づけられます。しかし、すべてのイテラブルがイテレーターとは限りません。イテラブルは、実際に反復が始まって初めてイテレーターを生成します。
この機能を示すため、イテラブルであるリストをインスタンス化し、組み込み関数
iter()を呼び出してイテレーターを生成します。リスト自体はイテレーターではありませんが、
iter()を呼ぶとイテレーターに変換され、そのイテレーターオブジェクトが返されます。すべてのイテラブルがイテレーターではないことを示すため、同じリストオブジェクトを生成し、イテレーターの次の要素を返す
next()関数を呼び出してみます。上のコードでは、リストに対して
next()を呼び出そうとしてTypeErrorが発生しています(Pythonの例外とエラー処理も参照)。これは、リストオブジェクトはイテラブルであってイテレーターではないという単純な理由によるものです。例で学ぶPythonのイテレーター
したがって、リストを手動で反復したい場合は、まずイテレーターオブジェクトを生成する必要があります。そうして初めて、リストの値を順に取り出せます。
Pythonは、イテラブルオブジェクトをループしようとすると自動的にイテレーターオブジェクトを生成します。
StopIteration例外が捕捉されると、ループは終了します。イテレーターから得られる値は左から右へ順にしか取得できません。Pythonにはイテレーターを逆方向に進める
previous()関数はありません。イテレーターの遅延性
同じイテラブルオブジェクトから複数のイテレーターを定義することができます。各イテレーターは独自の進捗状態を保持します。したがって、同じイテラブルのイテレーターを複数作れば、一方のインスタンスを最後まで進めても、もう一方は開始位置のままにしておけます。
iterator_bが系列の最初の要素を出力していることに注目してください。このように、イテレーターには遅延的な性質があると言えます。イテレーターが作成されても、要素は要求されるまで生成されません。言い換えると、
next(iter(list_instance))のように明示的に要求したときにだけ、リストの要素が返されます。ただし、イテレーターに対して組み込みのイテラブルコンテナ(
list()、set()、tuple()など)を呼び出せば、すべての値を一度に取り出すこともできます。これによりイテレーターに全要素の生成を強制します。しかし大きなイテレーターに対しては推奨されません。すべての要素を一度に生成してメモリに保持することになり、遅延評価の利点が損なわれるためです。
データセットがメモリに収まりきらないほど大きい場合や、完全なイテレータークラスを書かずに遅延的な反復を行いたい場合は、通常ジェネレーターの方が適しています。
Pythonのジェネレーター
イテレーターを実装する最も手っ取り早い代替手段はジェネレーターを使うことです。ジェネレーターは見た目こそ普通のPython関数に似ていますが、性質は異なります。まず、ジェネレーターオブジェクトは項目を直接「返す」のではありません。代わりに
yieldキーワードを使って、その場で項目を生成します。つまり、ジェネレーターは遅延評価を活用する特別な関数だと言えます。ジェネレーターは、一般的なイテラブルのように内容をメモリに保持しません。たとえば、正の整数のすべての約数を求めたい場合、通常は次のような従来の関数(詳しくはこのPython関数のチュートリアルを参照)を実装するでしょう。
上のコードは約数のリスト全体を返します。これを従来の関数ではなくジェネレーターで実装すると違いがわかります。
returnの代わりにyieldを使ったため、関数は即座に終了しません。実質的にPythonに対し、従来の関数ではなくジェネレーターオブジェクトを作成するよう指示しており、その状態が追跡されます。その結果、遅延的なイテレーターに対して
next()関数を呼び出し、系列の要素を1つずつ表示できます。ジェネレーターを作るもう1つの方法はジェネレーター内包表記を使うことです。構文はリスト内包表記に似ていますが、角かっこではなく丸かっこを使います。
Pythonのyieldキーワードを探る
yieldキーワードはジェネレーター関数の制御フローを司ります。returnのように関数を終了させるのではなく、yieldは関数を一旦抜けつつもローカル変数の状態を保持します。yieldの呼び出しから返されるジェネレーターは変数に代入でき、next()関数で反復できます。これは関数を最初に出現するyieldまで実行します。yieldに到達すると関数の実行は中断され、その時点の状態が保存されます。したがって、任意のタイミングで実行を再開できます。関数は
yieldの位置から処理を続行します。例を示します。上のコードでは、ジェネレーターに4つの
yield呼び出しがありますが、5回nextを呼ぼうとしているためStopIteration例外が発生しました。これは、ジェネレーターが無限列ではないため、想定回数を超えて呼び出すと尽きてしまうためです。まとめ
要点を振り返ると、イテレーターは反復可能なオブジェクトであり、ジェネレーターは遅延評価を活用する特別な関数です。独自のイテレーターを実装する場合は
__iter__()と__next__()を作る必要がありますが、ジェネレーターはPython関数や内包表記でyieldキーワードを用いれば実装できます。複雑な状態管理を伴うオブジェクトが必要な場合、または
__next__()、__iter__()、__init__()以外のメソッドも公開したい場合は、ジェネレーターより独自のイテレーターを選ぶほうがよいでしょう。一方で、内容をメモリに保持しないため大規模データの処理や、わざわざイテレーターを実装する必要がない場面では、ジェネレーターのほうが適しています。