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