cover_python_cookbook

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

Tags: Python , Cookbook , generator , iterable , iteration , iterator , yield

Published 2020年5月9日9:29 by T.Tsuyoshi

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 関数と同じ働きをしています。


イテレータは、現在イテレーションプロセスのどの段階にいるのか、という情報を抑えておく必要があるのでこれだけ複雑な実装になります。

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

0 comments

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

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