検索ガイド -Search Guide-

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

Python Cookbook [Implementing the Iterator Protocol : イテレータープロトコルの実装] 投稿一覧へ戻る

Published 2020年5月9日9:29 by mootaro23

SUPPORT UKRAINE

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

Problem:

イテレーションをサポートするカスタムオブジェクトを作成したいが、イテレータプロトコルの実装では苦労したくない。

Solution:

イテレーションをサポートするオブジェクトを作成する一番簡単な手段は generator 関数を利用することでしょう。


" Delegating Iteration " の項で取り上げたクラスを再利用してみます。
今回は Node クラスとしてツリー構造を模倣し、ルート階層から子階層へとノードを縦断していくようなイテレータを実装します。


class Node:
def __init__(self, value):
self._value = value
self._children = []

def __repr__(self):
return 'Node({!r})'.format(self._value)

def add_child(self, node):
self._children.append(node)

def __iter__(self):
return iter(self._children)

def depth_first(self):
yield self
for c in self:
yield from c.depth_first()

# 使用例:
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
child1.add_child(Node(3))
child1.add_child(Node(4))
child2.add_child(Node(5))

for ch in root.depth_first():
print(ch)

# Node(0)
# Node(1)
# Node(3)
# Node(4)
# Node(2)
# Node(5)




Node クラスの depth_first() でやっていることは単純です。


最初に自分自身を yield して、続けて自分の子 (child) を yield して...
このとき yield from ステートメントを使用しているので、実行はその child へ委ねられています。


もっともっと噛み砕いてみます。
「使用例」の中の for ループを追いかけてみましょう。


最初の for ループで root.depth_first() が実行されると、当たり前ですが root オブジェクトの depth_first() が実行されます。


ここで yield self をしていますから、自分自身を yield して動作は停止、呼び出し元へ自分自身を返します。


よって for ループの ch 変数には root オブジェクトが入ってきます。


ここで print(ch) をしていますから、root オブジェクトの __repr__() が実行されて root オブジェクトが保持している値 0 が出力されます。


ここで次の for ループが実行されると、depth_first() 内の yield 文の次から実行が再開されます。


ここにまた for 文がありますね。


for c in self: では for ステートメントによって self(=root) のイテレータ (root.__iter__()) が消費され、その結果として iter(self._children)、すなわちイテレーションをサポートするオブジェクト、ここではリストオブジェクトが返されてきます。


この self(=root)._children の実体は [child1, child2] ですから、for 文の c 変数には child1 オブジェクトが入ってきます。


ここで yield from c.depth_first() が実行されると、root オブジェクトでの実行は一時停止され、c (=child1) へと実行が委譲され、child1 の depth_first() が実行され....


そしてそれぞれの階層で StopIteration 例外が発生すると呼び出し元の階層へ実行が戻り、停止していた場所から再開されます。


Python におけるイテレータプロトコルでは、__next__() を実装したイテレータオブジェクトを返す __iter__() メソッドの実装、イテレーションの終了を StopIteration 例外を発生させて通知すること、が共に求められます。


これらの機能を全て自分で実装しようとすると結構ワチャワチャです。


以下のコードは、楽をしたくない人用自分でイテレーションプロトコル実装バージョンです。


class Node:
def __init__(self, value):
self._value = value
self._children = []

def __repr__(self):
return 'Node({!r})'.format(self._value)

def add_child(self, other_node):
self._children.append(other_node)

def __iter__(self):
return iter(self._children)

def depth_first(self):
return DepthFirstIterator(self)

class DepthFirstIterator(object):
def __init__(self, start_node):
self._node = start_node
self._children_iter = None
self._child_iter = None

def __iter__(self):
return self

def __next__(self):
# イテレーションプロセスの開始時には自分自身を返す
if self._children_iter is None:
self._children_iter = iter(self._node)
return self._node

# 子ノードを実行中であれば、そのノード内の次の値を返す
elif self._child_iter:
try:
nextchild = next(self._child_iter)
return nextchild
except StopIteration:
self._child_iter = None
return next(self)

# 次の子ノードへ進み、そのノードにおけるイテレーションを開始する
else:
self._child_iter = next(self._children_iter).depth_first()
return next(self)



DepthFirstIterator クラスは、苦労したくないバージョンの generator 関数と同じ働きをしています。


イテレータは、現在イテレーションプロセスのどの段階にいるのか、という情報を抑えておく必要があるのでこれだけ複雑な実装になります。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
Python Cookbook [Creating New Iteration Patterns with Generators : ジェネレーターを利用した独自イテレーションパターンの実装]
【Python 雑談・雑学 + coding challenge】iterator protocol の実装 --- __iter__ 特殊関数は何を返すべき? イテレータオブジェクト ( iterator object ) なら何でも、そう、generator expression でもOKです!
Python Cookbook [Manually Consuming an Iterator : 手作業によるイテレーション操作]
Python Cookbook [Delegating Iteration : 独自クラスへのイテレーション機能の実装]
【 Effective Python, 2nd Edition 】引数として受け取った値を関数内で複数回「消費」する場合には要注意! イテレータ ( iterator ) とコンテナ ( container ) の違いをちゃんと認識しよう!
【 Effective Python, 2nd Edition 】throw() メソッドを利用したジェネレータ ( generator ) 内部での状態遷移はなるだけ避けましょう。ネストが深くなってコードの読解性が落ちちゃいますよ!
【 Effective Python, 2nd Edition 】ジェネレータ ( generator ) に値を注入したいなら、yield from 式と send() 関数の併用よりも、注入する値を提供するイテレータ ( iterator ) を渡しましょう、の巻