Python で Visitor パターン

  • 25
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

AST を調べていて Visitor パターン が出てきました。Visitor パターン使うと何が嬉しかったんだっけ?と基本的なことが分かっていなかったので復習しました :sweat:

wikipedia には Java のコード例が紹介されているので Python3 で書き直してみました。当初は、日本語の方のコードを例を見ながら移植していて、なんか中途半端なサンプルだなと思っていたら英語の Visitor pattern の方はより簡潔なコード例に修正されていました。このページは英語の方を見た方が良さそうです。

Python にはインターフェースがないので abc モジュールの @abstractmethod デコレータを使っています。余談ですが、abc モジュールはクラスのインスタンスにしか適用されないけど、 zope.interface はクラス、オブジェクト、モジュールなどにも適用できるのが凄いよといったことが以下に書かれています。

閑話休題。コードをみながらパターンをみていきます。

3.4
# -*- coding: utf-8 -*-
from abc import ABCMeta, abstractmethod

class ICarElementVisitor(metaclass=ABCMeta):
    """
    Interface like in Python
    """
    @abstractmethod
    def visit_Wheel(self, wheel): pass

    @abstractmethod
    def visit_Engine(self, engine): pass

    @abstractmethod
    def visit_Body(self, body): pass

    @abstractmethod
    def visit_Car(self, car): pass

class ICarElement(metaclass=ABCMeta):
    """
    Interface like in Python
    """
    @abstractmethod
    def accept(self, visitor): pass

class Wheel(ICarElement):
    def __init__(self, name):
        self.name = name

    def accept(self, visitor):
        visitor.visit_Wheel(self)

class Engine(ICarElement):
    def accept(self, visitor):
        visitor.visit_Engine(self)

class Body(ICarElement):
    def accept(self, visitor):
        visitor.visit_Body(self)

class Car(ICarElement):

    def __init__(self):
        self.elements = [
            Wheel('front left'), Wheel('front right'),
            Wheel('back left'), Wheel('back right'),
            Body(), Engine(),
        ]

    def accept(self, visitor):
        for elem in self.elements:
            elem.accept(visitor)
        visitor.visit_Car(self)

class PrintVisitor(ICarElementVisitor):
    def visit_Wheel(self, wheel):
        print('Visiting {} wheel'.format(wheel.name))

    def visit_Engine(self, engine):
        print('Visiting engine')

    def visit_Body(self, body):
        print('Visiting body')

    def visit_Car(self, car):
        print('Visiting car')

class DoVisitor(ICarElementVisitor):
    def visit_Wheel(self, wheel):
        print('Kicking my {} wheel'.format(wheel.name))

    def visit_Engine(self, engine):
        print('Starting my engine')

    def visit_Body(self, body):
        print('Moving my body')

    def visit_Car(self, car):
        print('Starting my car')

def main():
    """
    >>> main()
    Visiting front left wheel
    Visiting front right wheel
    Visiting back left wheel
    Visiting back right wheel
    Visiting body
    Visiting engine
    Visiting car
    --------------------------------
    Kicking my front left wheel
    Kicking my front right wheel
    Kicking my back left wheel
    Kicking my back right wheel
    Moving my body
    Starting my engine
    Starting my car
    """
    car = Car()
    car.accept(PrintVisitor())
    print('-' * 32)
    car.accept(DoVisitor())

Java のコードをそのまま移植するとこんな感じですが、Python なのでもうちょっと緩くしてみましょう。

--- visitor.py  2015-02-17 18:43:53.000000000 +0900
+++ visitor-generic.py  2015-02-17 18:46:24.000000000 +0900
@@ -6,16 +6,8 @@
     Interface like in Python
     """
     @abstractmethod
-    def visit_Wheel(self, wheel): pass
-
-    @abstractmethod
-    def visit_Engine(self, engine): pass
-
-    @abstractmethod
-    def visit_Body(self, body): pass
-
-    @abstractmethod
-    def visit_Car(self, car): pass
+    def visit(self, obj):
+        getattr(self, 'visit_' + obj.__class__.__name__)(obj)

 class ICarElement(metaclass=ABCMeta):
     """
@@ -29,15 +21,15 @@
         self.name = name

     def accept(self, visitor):
-        visitor.visit_Wheel(self)
+        visitor.visit(self)

 class Engine(ICarElement):
     def accept(self, visitor):
-        visitor.visit_Engine(self)
+        visitor.visit(self)

 class Body(ICarElement):
     def accept(self, visitor):
-        visitor.visit_Body(self)
+        visitor.visit(self)

 class Car(ICarElement):

@@ -51,9 +43,12 @@
     def accept(self, visitor):
         for elem in self.elements:
             elem.accept(visitor)
-        visitor.visit_Car(self)
+        visitor.visit(self)

 class PrintVisitor(ICarElementVisitor):
+    def visit(self, obj):
+        getattr(self, 'visit_' + obj.__class__.__name__)(obj)
+
     def visit_Wheel(self, wheel):
         print('Visiting {} wheel'.format(wheel.name))

@@ -67,6 +62,9 @@
         print('Visiting car')

 class DoVisitor(ICarElementVisitor):
+    def visit(self, obj):
+        super().visit(obj)
+

変更点を簡単に説明すると、インターフェースっぽいものにリフレクションを使ってデフォルト実装を定義します。

3.4
class ICarElementVisitor(metaclass=ABCMeta):
    """
    Interface like in Python
    """
    @abstractmethod
    def visit(self, obj):
        getattr(self, 'visit_' + obj.__class__.__name__)(obj)

そうすると、visitor を受け取るクラスでは visitor.visit(self) を呼ぶように統一できて少しすっきりします。

3.4
class Engine(ICarElement):
    def accept(self, visitor):
        visitor.visit(self)

Visitor のサブクラスは、PrintVisitor のように visit() の実装を直接定義しても良いし、DoVisitor のように抽象クラス (インターフェース) のデフォルト実装を使うのも良いでしょう。

3.4
class PrintVisitor(ICarElementVisitor):
    def visit(self, obj):
        getattr(self, 'visit_' + obj.__class__.__name__)(obj)
    ...

class DoVisitor(ICarElementVisitor):
    def visit(self, obj):
        super().visit(obj)
    ...

Visitor パターンは、accept() メソッドに Visitor オブジェクトを渡し、その内部で visitor.visit(self) メソッドを呼び出す (Double Dispatch) といったパターンです。この例では car.accept(visitor) の内部でそれぞれの要素オブジェクトの elem.accept(visitor) メソッドも呼び出しています。

利点の1つとして、複数の要素オブジェクトを横断 (traverse) して操作を行うことにより、新しい操作を追加するときに既存のオブジェクトのコードやデータ構造を変更しなくて済むという Separation of concerns (関心の分離) につながります。

例えば、ブレーキという操作を追加することを考えてみると、

+class Brake(ICarElement):
+    def accept(self, visitor):
+        visitor.visit(self)
+
 class Car(ICarElement):

     def __init__(self):
         self.elements = [
             Wheel('front left'), Wheel('front right'),
             Wheel('back left'), Wheel('back right'),
-            Body(), Engine(),
+            Body(), Engine(), Brake(),
         ]
@@ -55,6 +59,9 @@
     def visit_Engine(self, engine):
         print('Visiting engine')

+    def visit_Brake(self, engine):
+        print('Visiting brake')
+        

これだけの修正で済むということでしょう。

さらに visitor オブジェクトは状態をもつこともできるので polymorphic メソッドよりも強力であると説明されています。

この例では特に状態を管理していませんが、例えば、呼び出された順番を記録したり、ある操作が行われたらフラグを立てるといったことも容易だと言えます。

と、書いてみてから調べ直してみたら、visitorパターンとダックタイピングの比較 によると、エキスパート Python プログラミング に載っていたのかな?もう何年も前のことで、そのとき読書会に参加してそこで話題になった記憶はあるけれど、コードは全く覚えていませんでした。