検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>
python_coding_challenge

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

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

さて、今回もちょっとしたコーディングチャレンジ ( 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__ を実装し、処理を任せるイテレータオブジェクトを返します。


そして、そのイテレータオブジェクトは、自分自身でも、ヘルパークラスでも、そして、ジェネレータ式、ジェネレータ関数でも構わないんです!
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
Python Cookbook [Implementing the Iterator Protocol : イテレータープロトコルの実装]
【 Effective Python, 2nd Edition 】ジェネレータ ( generator ) に値を注入したいなら、yield from 式と send() 関数の併用よりも、注入する値を提供するイテレータ ( iterator ) を渡しましょう、の巻
【 Effective Python, 2nd Edition 】引数として受け取った値を関数内で複数回「消費」する場合には要注意! イテレータ ( iterator ) とコンテナ ( container ) の違いをちゃんと認識しよう!
Python Cookbook [Manually Consuming an Iterator : 手作業によるイテレーション操作]
【Python 雑談・雑学 + coding challenge】comprehension は確かに Pythonic ですけど、map 組み込み関数と使い分けることも必要ですね!
【 Effective Python, 2nd Edition 】入力元のデータサイズが大きい場合は、リスト内包表記 ( list comprehension ) ではなくジェネレータ式 ( generator expression ) の利用を検討しよう!
【Python 雑談・雑学 + coding challenge】シーケンス ( sequence ) における インデックス ( index ) を使った要素 1 つの取り出しと、スライス ( slice ) を利用した場合の取り出しの違いをちゃんと理解していますか?