LibreOffice5(51)Javaの例:ConfigExamplesをPythonにする その4

browseDataExample()のbrowseElementRecursively()をVisitorパターンで書き直します。Visitorパターンには再帰版、ジェネレーター(あるいはコルーチン)版があります。

前の関連記事:LibreOffice5(50)Javaの例:ConfigExamplesをPythonにする その3


VisitパターンはCompositeパターンを操作する


Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本はJavaの本なのですが、一番最後にVisitorパターンの短い解説があります。

この解説を読むとVisitorパターンはコンポジット構造に作用すると書いてあります。

この「コンポジット」というのが何なのか調べてみると同じ本の第9章にCompositeパターンの解説がありました。

なんと、これまで「多分木」と私が思っていたものはCompositeパターンのことだと知りました。

道理でxmlなど「多分木」の構造のデータは多いはずなのに「多分木」で検索してもあまり例がでてこないはずでした。

再帰を使うVistorパターン


Python Cookbook08.21.implementing_the_visitor_patternにPythonのVisitorパターンの例があります。

この例では二分木をまず作っています。

二分木はつまり子が2個以下に制限されているコンポジットパターンです。

なので同じコンポジットパターンである多分木にそのまま転用できます。

転用するのはNodeVistorクラスとEvaluatorクラスです。

NodeVistorクラスのvisit()メソッドで、ノードに適用するEvaluatorクラスのメソッドを振り分けます。

Evaluatorクラスでノードの種類に応じたメソッドを用意しておきます。

ということでまず扱うコンポジットパターンのノードの種類を考えます。

ノードの種類というのは、操作の種類です。

今回の例では葉か節(葉でないノード)か判別すればよいだけです。

PyUNOオブジェクトであれば節、そうでなければ葉、と判別して、それぞれvisit_PyUNO()メソッド、visit_Vlaues()メソッドに誘導すればよいわけです。

節からは次の節に移動するのでvisit_PyUNO()メソッドからノードを引数にしてまたvisit()メソッドを呼び出しています。
def browseDataExample(cp):
    try:
        print("\n--- starting example: browse filter configuration ------------------")
        printRegisteredFilters(cp)
    except:
        traceback.print_exc()
def printRegisteredFilters(cp):
    path = "/org.openoffice.TypeDetection.Filter/Filters"
    ca = createConfigurationView(path, cp)
    e = Evaluator()
    e.visit(ca)
    ca.dispose()
class NodeVisitor:
    def visit(self, node):
        name = "PyUNO" if type(node).__name__=="pyuno" else "Values"
        self.methname = 'visit_{}'.format(name)
        meth = getattr(self, self.methname, None)
        if meth is None:
            meth = self.generic_visit
        return meth(node)
    def generic_visit(self, node):
        raise RuntimeError('No {} method'.format(self.methname))
class Evaluator(NodeVisitor):
    def visit_Values(self, node):
        if isinstance(node, tuple):
            print("\tValue: {0} = {{{1}}}".format(self.path, ", ".join(node)))
        else:
            print("\tValue: {} = {}".format(self.path, node)) 
    def visit_PyUNO(self, node):
        if hasattr(node, "getTemplateName") and node.getTemplateName().endswith("Filter"):
            print("Filter {} ({})".format(node.getName(), node.getHierarchicalName()))
        childnames = node.getElementNames()
        for childname in childnames:
            self.path = node.composeHierarchicalName(childname)
            self.visit(node.getByName(childname))   
10行目でビジターをインスタンス化して、11行目で根を引数でvisit()メソッドを呼び出しています。

35行目で再帰的にvisit()メソッドを呼び出しています。

ConfigExamples/configexamples.py at e51937cef77dad94830451c8a11ec82096e19d81 · p--q/ConfigExamples

Visitパターンを使うとノードの振り分けとノードごとの操作がはっきり分かれるのでわかりやすいですね。

でもノードだけではすべての情報を得ることができずEvaluatorクラスでインスタンス変数(self.path)を使わないといけませんでした。
def browseDataExample(cp):
    try:
        print("\n--- starting example: browse filter configuration ------------------")
        printRegisteredFilters(cp)
    except:
        traceback.print_exc()
def printRegisteredFilters(cp):
    path = "/org.openoffice.TypeDetection.Filter/Filters"
    ca = createConfigurationView(path, cp)
    e = Evaluator()
    output = e.visit(ca)
    print("\n".join(output))
    ca.dispose()
class NodeVisitor:
    def visit(self, node):
        name = "PyUNO" if type(node).__name__=="pyuno" else "Values"
        self.methname = 'visit_{}'.format(name)
        meth = getattr(self, self.methname, None)
        if meth is None:
            meth = self.generic_visit
        return meth(node)
    def generic_visit(self, node):
        raise RuntimeError('No {} method'.format(self.methname))
class Evaluator(NodeVisitor):
    def visit_Values(self, node):
        output = []
        if isinstance(node, tuple):
            output.append("\tValue: {0} = {{{1}}}".format(self.path, ", ".join(node)))
        else:
            output.append("\tValue: {} = {}".format(self.path, node)) 
        return output
    def visit_PyUNO(self, node):
        output = []
        if hasattr(node, "getTemplateName") and node.getTemplateName().endswith("Filter"):
            output.append("Filter {} ({})".format(node.getName(), node.getHierarchicalName()))
        childnames = node.getElementNames()
        for childname in childnames:
            self.path = node.composeHierarchicalName(childname)
            output.extend(self.visit(node.getByName(childname)))
        return output   
Python Cookbookの例と同様にしてEvaluatorクラスのメソッドに戻り値を付けました。

ConfigExamples/configexamples.py at ec3b2f394ab4880fe26c29cf1a8c54bd52f0b019 · p--q/ConfigExamples

再帰をジェネレーターで置換したVisitorパターン


まずこれまでと同様に再帰をwhile文に置換する手順を考えます。

上のコードの15行目のNodeVisitorクラスのvisit()メソッドが、そのサブクラスであるEvaluatorクラスのvisit_PyUNO()メソッドの中で呼び出されています(39行目)。

visit()メソッドはメソッドを返しているので、返すメソッドがvisit_PYUNO()メソッドである場合にvisit()メソッドの再帰になります。

visit_PyUNO()メソッドのなかでvisit()メソッドを呼んだ後にfor文が回るので、visit()メソッドの後に使っているのはnode(ConfigurationAccessサービスのインスタンス)とchildname(サブノードの名前)です。

これはVisitorパターンを使わない再帰のときと同じですね。

このまま同じ思考回路で再帰を除去すると前回と同じコードになってしまうのでPython Cookbook08.22.implementing_the_visitor_pattern_without_recursionの再帰を使わないVisitorパターンの例を参考にします。

まずnode.pyのNodeVisitorクラスを使ってみましたがこれはうまくいきませんでした。

というのもnode.pyではstackのリストの要素を、ジェネレーターか、Nodeクラスのインスタンス、それ以外か、で場合分けして処理を分けています。

この例では自分でコンポジットパターンを作っているのでNodeクラスで場合分けできているのですが、ConfigurationAccessサービスのコンポジット構造はConfigurationAccessサービスのインスタンスのgetByName()メソッドから得られるオブジェクトがConfigurationAccessサービスのインスタンスだけではなく、文字列かそのタプルであるときがあるのでやっかいです。

というのもstackにメソッドの結果を入れて出力しようと思ったときに、これらと区別する方法がないからです。

出力したい結果の方をリストにでも入れようかと思いましたが、そういうときのためにノードをクラスのインスタンスのプロパティにする方法もちゃんと例がありました。

example3.pyがその例で、既存のノードをVisitクラスのインスタンスのnodeプロパティに入れています。

ConfigExamples/configexamples.py at 5d0c75067d85c1490e82ae9fc18206763792eca9 · p--q/ConfigExamples
def browseDataExample(cp):
    try:
        print("\n--- starting example: browse filter configuration ------------------")
        printRegisteredFilters(cp)
    except:
        traceback.print_exc()
def printRegisteredFilters(cp):
    path = "/org.openoffice.TypeDetection.Filter/Filters"
    ca = createConfigurationView(path, cp)
    e = Evaluator()
    output = e.visit(ca)
    print("\n".join(output))
    ca.dispose()
class Visit:
    def __init__(self, node):
        self.node = node   
class NodeVisitor:
    def visit(self, node):
        stack = [Visit(node)]
        last_result = []
        while stack:
            try:
                last = stack[-1]
                if isinstance(last, types.GeneratorType):
                    stack.append(next(last))
                elif isinstance(last, Visit):
                    stack.append(self._visit(stack.pop().node))
                else:
                    last_result.append(stack.pop())
            except StopIteration:
                stack.pop()
        return last_result
    def _visit(self, node):
        name = "PyUNO" if type(node).__name__=="pyuno" else "Values"
        self.methname = 'visit_{}'.format(name)
        meth = getattr(self, self.methname, None)
        if meth is None:
            meth = self.generic_visit
        return meth(node)
    def generic_visit(self, node):
        raise RuntimeError('No {} method'.format(self.methname))
class Evaluator(NodeVisitor):
    def visit_Values(self, node):
        if isinstance(node, tuple):
            yield "\tValue: {0} = {{{1}}}".format(self.path, ", ".join(node))
        else:
            yield "\tValue: {} = {}".format(self.path, node)
    def visit_PyUNO(self, node):
        if hasattr(node, "getTemplateName") and node.getTemplateName().endswith("Filter"):
            yield "Filter {} ({})".format(node.getName(), node.getHierarchicalName())
        childnames = node.getElementNames()
        for childname in childnames:
            self.path = node.composeHierarchicalName(childname)
            yield Visit(node.getByName(childname))
これでうまくいきました。

ノードに作用させるEvaluatorクラスのメソッドはすべてジェネレーターにしています。

21行目のwhile文のなかでstackの要素を、ジェネレーターか、Visitクラスのインスタンスか、それ以外、今回はgetByName()メソッドで返ってくるタプルか文字列か、で場合分けしてそれぞれ処理しています。

stackに積むのはnode.getByName(childname)で得られるノードなので、node(ConfigurationAccessサービスのインスタンス)とchildname(サブノードの名前)をスタックに積むのと同じことです。

23行目でまずstackの最後の要素を取得していますが、取得するだけでまだ削除していません。

というのも要素がジェネレーターのときはさらに次の要素を追加しているだけだからです。

ジェネレーター自体は使い終わってから31行目で削除しています。

example3.pyではジェネレーターに対してsend()メソッドでyieldに値を戻してコルーチンのようにしていますが、今回はyieldに値を戻す必要がなかったので関数next()でジェネレーターを回しています。

Visitorパターンはわかりやすくてとても気に入りました。

これでbrowseDataExample()の翻訳は完了です。

次の関連記事:LibreOffice5(52)Javaの例:ConfigExamplesをPythonにする その5

PR

0 件のコメント:

コメントを投稿