【Python 雑談・雑学 + coding challenge】iterator protocol の実装 --- __iter__ 特殊関数は何を返すべき? イテレータオブジェクト ( iterator object ) なら何でも、そう、generator expression でもOKです! 投稿一覧へ戻る

Tags: Python , generator , iterable , iterator , __iter__ , miscellaneous , challenge , __next__ , stopiteration

Published 2020年8月13日17:06 by T.Tsuyoshi

さて、今回もちょっとしたコーディングチャレンジ ( coding challenge ) から。


問題 ( 制限時間: 25 分 ):


2 つの引数 (シーケンスと数値) を取るイテラブルクラス ( iterable class ) を定義します。


このクラスのオブジェクトは、「数値」の回数だけ「シーケンス」の要素を順番に返します。


もし「数値」が「シーケンス」の長さよりも大きい場合は、「シーケンス」の先頭に戻って要素を返し続けます。


つまり、


c = Circle('abc', 8)


print(list(c))

# ['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b']



というわけですから、Circle クラスのオブジェクト c は、イテラブルオブジェクト ( iterable object ) である、ということです。


豆ヒント:

何かがイテラブルであるためには、__iter__ 特殊関数が定義されていなければなりません。

__iter__ 特殊関数からの戻り値は __next__ 特殊関数が定義されているクラスのオブジェクト ( イテレータオブジェクト; iterator object ) です。

それは、イテラブルオブジェクト自身 ( self ) でも、__next__ 特殊関数を実装したヘルパークラスオブジェクトで構いません。


では、実装してみてください。


class Circle:
def __init__(self, data, max_times):
self.data = data
self.max_times = max_times

def __iter__(self):
pass


c = Circle('abc', 5)


print(list(c))

# ['a', 'b', 'c', 'a', 'b']



いかがでしたか?


Circle クラスに __next__ 特殊関数も実装して、__iter__ 特殊関数からは return self と自分自身を返す方法が一般的かもしれませんが、今回は拡張性、メンテナンス性を考慮して、イテレータクラスをヘルパークラスとして別に設けたいと思います。


class CircleIterator:
pass



ですから、Circle クラスの __iter__ 特殊関数からはこのヘルパー関数のオブジェクトを返すことになります。


class Circle:
def __init__(self, data, max_times):
self.data = data
self.max_times = max_times

def __iter__(self):
return CircleIterator(self.data, self.max_times)



さて、イテレータ本体となる CircleIterator クラスは次のように実装してみました。


class CircleIterator:
def __init__(self, data, max_times):
if iter(data): # 1:
self.data = data
self.max_times = max_times
self.index = 0 # 2:

def __next__(self): # 3:
if self.index >= self.max_times: # 4:
raise StopIteration

value = self.data[self.index % len(self.data)] # 5:
self.index += 1
return value



1: 渡されてきたデータがイテラブルかを確認しています。もしイテラブルでない場合は iter() が TypeError 例外を投げるのでプログラムはここで終了します。

2: 返した文字数を記憶しておくための変数です。

3: __next__ 特殊関数を実装します。このクラスがイテレータクラスである証です。

4: 要素を指定された回数返したら、処理の終了を知らせるために StopIteration 例外を投げます。

5: 限られた要素数のものをグルグルと回りながら好きなだけ取り出すために modulus ( % ) オペレータを良く利用しますよね。ここでもその方法を使って、もしシーケンスの要素数よりも取り出し回数が多い場合に対処しています。


最終的には次のような実装になりました。


class CircleIterator:
def __init__(self, data, max_times):
if iter(data):
self.data = data
self.max_times = max_times
self.index = 0

def __next__(self):
if self.index >= self.max_times:
raise StopIteration

value = self.data[self.index % len(self.data)]
self.index += 1
return value


class Circle:
def __init__(self, data, max_times):
self.data = data
self.max_times = max_times

def __iter__(self):
return CircleIterator(self.data, self.max_times)


c = Circle('abc', 7)


print(list(c))

# ['a', 'b', 'c', 'a', 'b', 'c', 'a']



さて、メでタしメでタし! なんですけど、__iter__ 特殊関数で返すイテレータオブジェクトはジェネレータ式 ( generator expression ) でも構わないことをご存知ですか?


ジェネレータ式も 1 度に値を 1 つずつ返すイテレータですから、全然問題ないんです。__next__ 特殊関数の処理はジェネレータ式にお任せです。


そこで、Circle クラスを実装し直してみると次のようになります。ヘルパー関数は必要なくなります。


class Circle:
def __init__(self, data, max_times):
if iter(data):
self.data = data
self.max_times = max_times

def __iter__(self):
n = len(self.data)
return (self.data[i % n] for i in range(self.max_times)) # 1:



1: 指定された回数だけ range() を利用してループします。その都度、先ほど説明した modulus ( % ) オペレータを利用して、シーケンス内の該当する要素を取り出して返します。


とっても Python 的な素敵なコードだと思いませんか?


簡潔ですし、かと言って、変に複雑なわけでもありません。


今回の例のように、イテレータプロトコル ( iterator protocol ) を実装するための 3 つの柱、__iter__、__next__、StopIteration において、__next__ と StopIteration 例外はイテレータの仕事です。


イテラブルは __iter__ を実装し、処理を任せるイテレータオブジェクトを返します。


そして、そのイテレータオブジェクトは、自分自身でも、ヘルパークラスでも、そして、ジェネレータ式、ジェネレータ関数でも構わないんです!

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)