この記事は【Python】ジェネレータについてという記事の補足として書いたものです。
といっても自分もイテレータについて結構ふんわりとしか理解していなかったのでそれも含めて書いていこうと思います。
目次
- イテレータとは
- イテレータをクラスとして実装するとどうなるのか
- Pythonの内部ではどのように振舞っているのか
使用環境
- Python 3.8.8
イテレータとは
- イテレータ: 要素を反復して取り出すことのできるインタフェース
(引用:Pythonのイテレータとジェネレータ)
イテレータとは値に含まれている要素を順に1個ずつ取り出せるオブジェクトです。
(引用:詳細! Python 3 入門ノート)
ざっくりいうと、並んでるデータ(リストや、タプル)を順番に取り出すことができるモノ
リスト、文字列、タプル、辞書などはイテラブルといい、イテレータではまだありません。
hoge = [1, 2, 3]
hoge_iterator = iter(hoge)
print(next(hoge_iterator))
print(next(hoge_iterator))
print(next(hoge_iterator))
このプログラムの実行結果は1 2 3と数字が連続で表示されます。
イテラブルはiter()関数でイテレータになります。そして、イテレータはnext()関数で順番に要素を取り出すことができます。
hoge = [1, 2, 3]
for i in hoge:
print(i)
実際にはこのようにfor文を使うことでiter()やnext()を明示的に使わなくても実行することもできます。ちなみに、for文はイテラブルでもイテレータでも値を順に取り出すことができます。
イテレータをクラスとして実装するとどうなるのか
では、イテレータが内部でどのような処理を行なっているのかを理解するためにクラスとして定義してみます。
class MyIterator(object):
def __init__(self, *numbers):
self._numbers = numbers
self._i = 0
def __iter__(self):
return self
def __next__(self):
if self._i == len(self._numbers):
raise StopIteration()
value = self._numbers[self._i]
self._i += 1
return value
(引用:Pythonのイテレータとジェネレータ)
こちらのプログラムは引用元の記事からお借りをしました。
特殊な文法があるのでいくつか解説しておきます(分かってる人は飛ばして大丈夫です)。
*args
def func(*args):
print(args, type(args))
for i in args:
print(i)
func(1, 2, 3)
実行結果:
(1, 2, 3) <class 'tuple'>
1
2
3
こちらのプログラムを見てもらうとわかる通り、関数の引数に『*(アスタリスク)』を先頭につけてあげることで、複数の引数を受け取ることができる。
受け取った引数はタプル形式で、*を除いた変数名として扱われる。
raise
raise ValueError('ごめんね!')
print('hoge')
実行結果:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
in
----> 1 raise ValueError('ごめんね!')
2 print('hoge')
ValueError: ごめんね!
このプログラムでは、raiseの後にエラー名と、引数としてエラーメッセージを渡すことで、任意のエラーを任意の場所で起こすことができる。
エラーが起きるため、その時点でプログラムの処理は停止する。
そのため、その後のhogeは表示されない。
以上で特殊な文法の解説を終わります。
クラスの解説
イテレータクラスの解説に移ります。
まず、__init__メソッドにてクラスの初期化処理を行うわけですが、そこで_numbers、_iというプロパティを定義しています。_numbersは引数にもあるとおり、*がついた可変長引数になってます。
そのため、_numbersは引数として渡された値のタプル(イテラブル)を保持していることになります。
続いて、__iter__メソッドです。ここでは、自身のインスタンスのみを返しています。実際の処理とは違うでしょうが、今回はこれで大丈夫です。現時点でこの処理の必要性が理解できなくても問題ないです。後で説明します。
最後に、__next__メソッドにてイテレータとして振る舞うための処理を書きます。まず、if文を通り過ぎると、valueにその回の要素を入れます。そして_iプロパティに1加算します。そして戻り値でvalueを返す。
これを繰り返すことによって、持っている要素を順番に送り出すことができます。
そしてif分のところですが、エラーを表示させています。そのエラーとは以下のプログラム例を見るとわかりますが過剰にイテレーションすると、StopIterationというエラーが出ます。それを再現するためにこのif分の処理が必要です。
hoge = [1, 2, 3]
hoge_iterator = iter(hoge)
print(next(hoge_iterator))
print(next(hoge_iterator))
print(next(hoge_iterator))
print(next(hoge_iterator))
実行結果:
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
in
5 print(next(hoge_iterator))
6 print(next(hoge_iterator))
----> 7 print(next(hoge_iterator))
StopIteration:
イテレータのクラスを利用してみる
my_iter = MyIterator(1, 2, 3)
my_iter = iter(my_iter)
print(next(my_iter))
print(next(my_iter))
print(next(my_iter))
print(next(my_iter))
実行結果:
1
2
3
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
in
6 print(next(my_iter))
7 print(next(my_iter))
----> 8 print(next(my_iter))
<ipython-input-1-6ddc4f6f0785> in __next__(self)
9 def __next__(self):
10 if self._i == len(self._numbers):
---> 11 raise StopIteration()
12 value = self._numbers[self._i]
13 self._i += 1
StopIteration:
ちゃんと、イテレーションをして、エラーも正しく実装できました。
これはfor文でも問題なく動作します。
my_iter = MyIterator(1, 2, 3)
for i in my_iter:
print(i)
なぜこのような結果になるのかというと、iter()関数というのは__iter__メソッドを呼び出す関数で、next()関数は__next__メソッドを呼び出す関数だからです。
今回は__next__に直接イテレータとしての処理を書いたので、イテラブルをイテレータとする__iter__で自分のインスタンスを返していたのです。
Pythonの内部ではどのように振舞っているのか
最後に、Pythonの中で、オブジェクトたちがどのように振舞っているのかを確認してみましょう。
hoge = [1, 2, 3]
print(iter(hoge), hoge.__iter__())
#どちらもイテレータオブジェクトが返される
実行結果:
<list_iterator object at 0x7f0ad9a0b700> <list_iterator object at 0x7f0ad9a0bee0>
iter()関数と、__iter__メソッドどちらを実行してもイテレータオブジェクトができてるのがわかると思います。
hoge = [1, 2, 3]
hoge_str = 'hoge'
print(iter(tuple(hoge)), iter(set(hoge)), iter(hoge_str))
#タプルとセットは違うイテレータオブジェクトを返される
実行結果:
<tuple_iterator object at 0x7f0ae8624ac0> <set_iterator object at 0x7f0ad9ad5dc0> <str_iterator object at 0x7f0ae8624130>
このようにタプルやセット、文字列のイテレータオブジェクトは区別される(同じ種類のイテレータオブジェクトではない)。
print(iter(MyIterator(hoge)))
実行結果:
<__main__.MyIterator object at 0x7f0ae86243a0>
定義したイテレータクラスはiterを使っても自身のインスタンスを返しているだけなので、インスタンスとして振る舞う。
実際にはイテレータとして振る舞うが、システム上はあくまでインスタンスという部分をおさえておいてほしい。
まとめ
今回はPythonのイテレータについてまとめました。
- 要素を一つずつ取り出すためのオブジェクト
- for文を使って順番に取り出せる
- iter()関数と、next()関数でイテラブルをイテレータにして、イテレーションできる
この辺りが重要だと思います。
この記事は【Python】ジェネレータについてを書くために書いた記事になるので、ジェネレータの方をみてもらえると嬉しいです。