for文からiterable
とiterator
について触れて考えてみる。
また分かりやすくするため、できるだけ最小構成のクラスで試してみた。
注意として、__iter__
や__next__
などの特殊メソッドについての説明はしていない。
そしてHomeでも書いているが、自分なりにiterable
とiterator
の違いを調べたメモをまとめたものである。
まず初めに、__iter__
のみを実装したクラスでfor文を回してみる
class Test1:
def __init__(self):
pass
def __iter__(self):
return 5
test1 = Test1()
for item in test1:
print(item)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[57], line 10
7 return 5
9 test1 = Test1()
---> 10 for item in test1:
11 print(item)
TypeError: iter() returned non-iterator of type 'int'
__iter__
のみのクラス実装だと上手く動かなかった。
次に、__iter__
と__next__
を実装したクラスで動かしてみる。
class Test2:
def __init__(self):
self.i = 0
def __iter__(self):
return self
def __next__(self):
if self.i >= 5:
raise StopIteration() # これが呼び出されるまでfor文は続く仕組み
else:
self.i += 1
return self.i
test2 = Test2()
for item in test2:
print(item)
1
2
3
4
5
__iter__
のみでは動かなかったが、__next__
だけでは動くのか?
試してみる。
class Test3:
def __init__(self):
self.i = 0
def __next__(self):
if self.i >= 5:
raise StopIteration()
else:
self.i += 1
return self.i
test3 = Test3()
for item in test3:
print(item)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[84], line 13
10 return self.i
12 test3 = Test3()
---> 13 for item in test3:
14 print(item)
TypeError: 'Test3' object is not iterable
TypeError
が出た。どうやら、__iter__
だけでも__next__
だけでも動かないらしい。
__iter__
で返されるクラスは、self
ではなく__next__
を実装した別クラスでも良いのか?
class Test4:
def __init__(self):
pass
def __iter__(self):
return Test5()
class Test5:
def __init__(self):
self.i = 0
def __next__(self):
if self.i >= 5:
raise StopIteration()
else:
self.i += 1
return self.i
test4 = Test4()
for item in test4:
print(item)
1
2
3
4
5
__iter__
で__next__
が実装されている別のクラスのインスタンスを返しても動くことが分かった。
for文は __iter__
で __next__
が実装されているオブジェクトを呼び出す必要がある。
別のクラスのインスタンスを呼び出しても問題は無い。(__next__
が実装されていれば)
for文が動く条件が分かったので、次はList型はどのようにしてfor文上で使えるようになっているか見てみる。
まずは、List型の特殊メソッドに何が含まれているか確認する。
print(dir(list))
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
ここで、上記の出力をよーく確認してほしいのだが、__iter__
は実装されているが、「__next__
」が実装されていない。
じゃあ、for文で使えないのでは?と思うかもしれないが実際のところList型は使えている。
では、どうしてfor文で使用することが出来るのか…を知るために以下のコードを実行してみる。
a = [1, 2, 3, 4, 5]
b = a.__iter__()
print(type(b))
<class 'list_iterator'>
出力を見ると分かるのだが、List型は__iter__
の戻り値を、list_iterator
というクラスのインスタンスにしているのである。
実際に、list_iterator
クラスの特殊メソッドを確認してみる。
a = [1, 2, 3, 4, 5]
b = a.__iter__()
print(b.__dir__())
print(b.__next__())
print(b.__next__())
['__getattribute__', '__iter__', '__next__', '__length_hint__', '__reduce__', '__setstate__', '__doc__', '__new__', '__repr__', '__hash__', '__str__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__reduce_ex__', '__getstate__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']
1
2
list_iterator
にしっかり__next__
が実装されていることが分かった。
つまりList型は、for文を動かす際にList型の__iter__
で、list_iterator
クラスのインスタンスを呼び、list_iterator
の特殊メソッドである__next__
でfor文のループ変数に一つづつ値を入れているのである。
ここまでを踏まえて、iterable
とiterator
について考えてみる。
初めに、公式ドキュメントを見るに、List型はiterable
であることが分かる。
そして、List型の__iter__
で返された値は、list_iterator
クラスであり、名前からしてiterator
であることに間違いは無い。
str_iter = str().__iter__()
tuple_iter = tuple().__iter__()
print(type(str_iter))
print(type(tuple_iter))
<class 'str_ascii_iterator'>
<class 'tuple_iterator'>
また、同様に先程の公式ドキュメントでiterable
として紹介されているstr
とtuple
も__iter__
で返されるクラスが、xxx_iterator
であることが分かるため、list
だけが特殊では無いだろうと推測できる。
そのため、iterable
とiterator
を以下のように認識出来ると思う。
iterable
は、特殊メソッド__iter__
で、iterator
を返すクラス。iterator
は、特殊メソッド__next__
で、値を一つずつ返すことが出来るクラス。余計困惑するかもしれないが、水道水と蛇口のような関係だなと思った。
水道水(iterator)は水がいつでも供給可能で、蛇口(iterable)は水を一回の操作で少しずつ取り出せるみたいな。
ここまでで納得できるなら問題ないのだが、私には一つ疑問が生まれた。
「何故、List型には直接__next__
を入れずわざわざlist_iterator
を返しているのか?」という疑問である。
これは、あくまで理由の一つに過ぎないが「複数のiterator
を独立させたいから」というのが大きい理由では無いかと思う。
実際に、以下のコードを動かしてlist_iterator
を返すメリットを考えてみる。
class Test5:
def __init__(self):
self.i = 0
def __iter__(self):
return self
def __next__(self):
if self.i >= 5:
raise StopIteration() # これが呼び出されるまでfor文は続く仕組み
else:
self.i += 1
return self.i
test5 = Test5()
test5_iter1 = test5.__iter__()
test5_iter2 = test5.__iter__()
l_list = list([1, 4, 5, 2, 6])
l_iter1 = l_list.__iter__()
l_iter2 = l_list.__iter__()
print(test5_iter1.__next__())
print(test5_iter2.__next__())
print(l_iter1.__next__())
print(l_iter2.__next__())
1
2
1
1
Test5
クラスは、__iter__
を呼び出すと、同じインスタンスを返すクラスである。
ここで、出力をみてみると、Test5
のクラスは複数のiterator
が独立していないことが分かり、List型は複数のiterator
が独立していることが分かった!
これが、List型などが__iter__
でselfを返していない理由の一つである。
また、Test5
クラスは自分自身を返しているので、iterable
とは言いにくい。一度しかiterator
を呼び出せないiterable
としても振る舞えるな〜位の気持ち。
Pythonで普段使うようなデータ型で、iterator
はあまり存在しない。list
,str
,set
,tuple
…などはすべてiterable
である。
しかし、普段から何かと使うmap
クラスはiterator
であるので少し紹介する。
m = map(lambda x: x **2, [1, 2, 3, 4, 5])
for item in m: print(item)
for item in m: print(item)
1
4
9
16
25
map
クラスは、上のコードのように__iter__
でselfを返すため、上記の例などでは上手く使用することが出来ない。
二回以上for文で使いたい場合は、List型などiterable
なオブジェクトに型変換するか、下記のコードのようにcopy.deepcopy
メソッドを使用するなどしよう。
import copy
m = map(lambda x: x **2, [1, 2, 3])
for item in copy.deepcopy(m): print(item)
for item in copy.deepcopy(m): print(item)
1
4
9
1
4
9
公式ドキュメント(iterable)
https://docs.python.org/ja/3/glossary.html#term-iterable
Python でイテラブルとイテレータの使い分け
https://zenn.dev/shizukakokoro/articles/d634f8ddad833c