昨日の@nyaxさんの記事は、GPT-3についてでした!
MYJLab Advent Calendar 2021の4日目はPythonのfor文についてです。
はじめに
Pythonのfor文は、range、リスト、辞書、文字列など様々なオブジェクトを回すことができます。
では、for文で回すことのできるオブジェクトとはどんなオブジェクトなのでしょうか。
今までなんとなーく雰囲気で使っていたfor文について整理してみました。
PythonのバージョンはPython3.8.2です。
for文で回せるのはイテラブルなオブジェクト
いきなり結論を書いてしまいますが、for文で回せるのは「イテラブルなオブジェクト」です。
「イテラブルなオブジェクト」とは次のいずれかに該当するオブジェクトです。
- __iter__ メソッドを定義したオブジェクト
- __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のイテレータとジェネレータ