iterableとiterator

for文からiterableiteratorについて触れて考えてみる。

また分かりやすくするため、できるだけ最小構成のクラスで試してみた。

注意として、__iter____next__などの特殊メソッドについての説明はしていない。

そしてHomeでも書いているが、自分なりにiterableiteratorの違いを調べたメモをまとめたものである。

for文が動く条件

__iter__

まず初めに、__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'

__next__

__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

疑問点1

__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__だけでも動かないらしい。

疑問点2

__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__が実装されていれば)

List型

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とは

ここまでを踏まえて、iterableiteratorについて考えてみる。

初めに、公式ドキュメントを見るに、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として紹介されているstrtuple__iter__で返されるクラスが、xxx_iteratorであることが分かるため、listだけが特殊では無いだろうと推測できる。

そのため、iterableiteratorを以下のように認識出来ると思う。

余計困惑するかもしれないが、水道水と蛇口のような関係だなと思った。

水道水(iterator)は水がいつでも供給可能で、蛇口(iterable)は水を一回の操作で少しずつ取り出せるみたいな。

Listに__next__が無い理由

ここまでで納得できるなら問題ないのだが、私には一つ疑問が生まれた。

「何故、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としても振る舞えるな〜位の気持ち。

mapはiterator

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