12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

MYJLabAdvent Calendar 2021

Day 4

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

Last updated at Posted at 2021-12-03

昨日の@nyaxさんの記事は、GPT-3についてでした!
MYJLab Advent Calendar 2021の4日目はPythonのfor文についてです。

はじめに

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

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

PythonのバージョンはPython3.8.2です。

for文で回せるのはイテラブルなオブジェクト

いきなり結論を書いてしまいますが、for文で回せるのは「イテラブルなオブジェクト」です。
「イテラブルなオブジェクト」とは次のいずれかに該当するオブジェクトです。

  1. __iter__ メソッドを定義したオブジェクト
  2. __getitem__ メソッドをシーケンスとして定義したオブジェクト

__iter__ メソッドを定義したオブジェクト

このようなfor文を書いたとき

iterable_object = [0, 1, 2]
for elem in iterable_object:
    print(elem)
# 0
# 1
# 2

内部ではこのように動いています。

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を使った実装」の2通りの方法で自作してみます。

クラスによる実装

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__メソッドを明示的に定義することなくイテラブルなオブジェクトを作成することができます。

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

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

__getitem__ メソッドをシーケンスとして定義したオブジェクト

__getitem__メソッドは、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文で回すことができるオブジェクトになります。

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文で回したときに変数を二つ受け取るものがあります。
これらのオブジェクトと変数を一つしか受け取らないオブジェクトは何が違うのでしょうか。

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__メソッドで取り出される要素を見てみます。

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を持ちますがここでは無視します。

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文
Pythonドキュメント 用語集 iterable
Pythonドキュメント イテレータ型
Pythonドキュメント __getitem__
イテラブル, iterable ってなに?
Pythonのイテレータとジェネレータ

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?