【Python】for文で回せるオブジェクト

昨日の[@nyaxさんの記事](https://qiita.com/nyax)は、GPT-3についてでした!
[MYJLab Advent Calendar 2021](https://qiita.com/advent-calendar/2021/myjlab)の4日目はPythonのfor文についてです。

# はじめに
Pythonのfor文は、range、リスト、辞書、文字列など様々なオブジェクトを回すことができます。
では、for文で回すことのできるオブジェクトとはどんなオブジェクトなのでしょうか。

今までなんとなーく雰囲気で使っていたfor文について整理してみました。

PythonのバージョンはPython3.8.2です。
# for文で回せるのはイテラブルなオブジェクト
いきなり結論を書いてしまいますが、for文で回せるのは「イテラブルなオブジェクト」です。
「イテラブルなオブジェクト」とは次のいずれかに該当するオブジェクトです。
1. \_\_iter__ メソッドを定義したオブジェクト
2. \_\_getitem__ メソッドをシーケンスとして定義したオブジェクト

# \_\_iter__ メソッドを定義したオブジェクト
このようなfor文を書いたとき

```python:
iterable_object = [0, 1, 2]
for elem in iterable_object:
    print(elem)
# 0
# 1
# 2
```
内部ではこのように動いています。

```python:
iterable_object = [0, 1, 2]
iterator = iterable_object.__iter__() # __iter__メソッドを呼び出し、イテレータを生成
while True:
    try:
        elem = iterator.__next__() # __next__メソッドを呼び出し、イテレータから要素を取り出す。要素がない場合はStopIteration例外を投げる。
    except StopIteration:
        break
    print(elem)
# 0
# 1
# 2
```
ここで「イテレータ」という単語がでてきました。
「イテレータ」とは、データの流れを表現するオブジェクトで、内部に\_\_next\_\_メソッドを持ちます。
\_\_next\_\_メソッドを繰り返し呼び出すことで、流れの中の要素を一つずつ取り出すことができます。
データがなくなると、要素を取り出す代わりにStopIteration例外を投げます。

オブジェクト内部にイテレータを生成する\_\_iter\_\_メソッドを実装すれば、for文で回せるオブジェクトを自作することができます。

ここでは、「クラスによる実装」と「yieldを使った実装」の２通りの方法で自作してみます。

## クラスによる実装
```python:
class MyIterableObject():
    def __init__(self):
        self._elements = ['あいうえお', 'かきくけこ', 'さしすせそ']
        self._i = 0
    def __iter__(self):
        # __next__()はselfが実装してるのでそのままselfを返す
        return self
    def __next__(self):
        if self._i == len(self._elements):
            raise StopIteration()
        elem = self._elements[self._i]
        self._i += 1
        return elem

iterable_object = MyIterableObject()
for elem in iterable_object:
    print(elem)
# あいうえお
# かきくけこ
# さしすせそ
```
## yieldを使った実装
yieldを使うことで\_\_iter\_\_メソッドや\_\_next\_\_メソッドを明示的に定義することなくイテラブルなオブジェクトを作成することができます。

```python:
def my_iterable_object():
    yield 'あいうえお'
    yield 'かきくけこ'
    yield 'さしすせそ'

iterable_object = my_iterable_object()
for elem in iterable_object:
    print(elem)
# あいうえお
# かきくけこ
# さしすせそ
```

# \_\_getitem\_\_ メソッドをシーケンスとして定義したオブジェクト
\_\_getitem\_\_メソッドは、Pythonの特殊メソッドの一つで、オブジェクトに角括弧[ ]でアクセスしたときの挙動を定義できます。

```python:
class Sample:
    def __getitem__(self, index):
        if 1 <= index < 4: # [1],[2],[3]でアクセスしたときの値
            return 'index is ' + str(index)
        else:
            raise IndexError

sample = Sample()
print(sample[1])
print(sample[2])
print(sample[3])
# index is 1
# index is 2
# index is 3
```

\_\_getitem\_\_メソッドをシーケンスとして（0から始まる連続する整数でアクセスできるように）定義すると、for文で回すことができるオブジェクトになります。

```python:
class MyIterableObject:
    def __init__(self):
        self._elements = ['あいうえお', 'かきくけこ', 'さしすせそ']
    def __getitem__(self, index):
        if 0 <= index < 3:
            return self._elements[index]
        else:
          raise IndexError

iterable_object = MyIterableObject()
for elem in iterable_object:
    print(elem)
# あいうえお
# かきくけこ
# さしすせそ
```

# 2つ以上の変数を受け取るfor文
for文で回せるオブジェクトの中には、enumerate()や辞書型.items()など、for文で回したときに変数を二つ受け取るものがあります。
これらのオブジェクトと変数を一つしか受け取らないオブジェクトは何が違うのでしょうか。

```python:
list = ['Alice', 'Bob', 'Charlie']
for index, elem in enumerate(list):
    print(index, elem)
# 0 Alice
# 1 Bob
# 2 Charlie

dict = {'key1': 1, 'key2': 2, 'key3': 3}
for key, value in dict.items():
    print(key, value)
# key1 1
# key2 2
# key3 3
```
ここで、enumerateについて見ていきます。
enumerateオブジェクトは内部に\_\_iter\_\_メソッドを持っています。
enumerateオブジェクトから\_\_iter\_\_メソッドを呼び出し、\_\_next\_\_メソッドで取り出される要素を見てみます。

```python:
list = ['Alice', 'Bob', 'Charlie']

enumerate_object = enumerate(list)
enumerate_object.__iter__()

print(enumerate_object.__next__())
print(type(enumerate_object.__next__()))
# (0, 'Alice')
# <class 'tuple'>
```
\_\_next\_\_メソッドでタプルが取り出されているのが分かります。

for文で回したリテラブルなオブジェクトから取り出された要素は、Pythonの標準の代入規則によって代入されます。
標準の代入規則では、シーケンス型(リスト, 文字列, タプル, range)は複数の変数への代入を一度に行えるので、その規則に則って変数が代入されます。

つまり、for文で回したオブジェクトから取り出された要素がシーケンス型(リスト, 文字列, タプル, range)であれば、for文で複数の変数を受け取ることができます。

最後に、enumerate関数もどきを自作してみたいと思います。
※enumerate関数の第一引数のiteratorは、\_\_iter__メソッドを持つオブジェクトであることを前提とします。
※本来のenumerate関数は第二引数にstartを持ちますがここでは無視します。

```python:
class MyEnumerate():
    def __init__(self, iterable):
        self._iterator = iterable.__iter__()
        self._i = 0
    def __iter__(self):
        return self
    def __next__(self):
        elem = (self._i, self._iterator.__next__())
        self._i += 1
        return elem

def my_enumerate(iterable):
    return MyEnumerate(iterable)

list = ['Alice', 'Bob', 'Charlie']

for index, elem in my_enumerate(list):
    print(index, elem)
# 0 Alice
# 1 Bob
# 2 Charlie
```

# おわりに
なんとなーく雰囲気で使っていて特に困ることもなかったfor文ですが、調べてみると知らなかったことがいろいろあって面白かったです。

# 参考
[Pythonドキュメント for文](https://docs.python.org/ja/3/reference/compound_stmts.html#the-for-statement) 
[Pythonドキュメント 用語集 iterable](https://docs.python.org/ja/3/glossary.html#term-iterable) 
[Pythonドキュメント イテレータ型](https://docs.python.org/ja/3/library/stdtypes.html#iterator-types) 
[Pythonドキュメント \_\_getitem__](https://docs.python.org/ja/3/reference/datamodel.html#object.__getitem__) 
[イテラブル, iterable ってなに？](https://python.ms/iterable/) 
[Pythonのイテレータとジェネレータ](https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09)