[AST]
(http://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8) を調べていて [Visitor パターン]
(http://ja.wikipedia.org/wiki/Visitor_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3) が出てきました。Visitor パターン使うと何が嬉しかったんだっけ?と基本的なことが分かっていなかったので復習しました
wikipedia には Java のコード例が紹介されているので Python3 で書き直してみました。当初は、日本語の方のコードを例を見ながら移植していて、なんか中途半端なサンプルだなと思っていたら英語の Visitor pattern の方はより簡潔なコード例に修正されていました。このページは英語の方を見た方が良さそうです。
Python にはインターフェースがないので abc モジュールの @abstractmethod
デコレータを使っています。余談ですが、abc モジュールはクラスのインスタンスにしか適用されないけど、 zope.interface はクラス、オブジェクト、モジュールなどにも適用できるのが凄いよといったことが以下に書かれています。
閑話休題。コードをみながらパターンをみていきます。
# -*- 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)
+
変更点を簡単に説明すると、インターフェースっぽいものにリフレクションを使ってデフォルト実装を定義します。
class ICarElementVisitor(metaclass=ABCMeta):
"""
Interface like in Python
"""
@abstractmethod
def visit(self, obj):
getattr(self, 'visit_' + obj.__class__.__name__)(obj)
そうすると、visitor を受け取るクラスでは visitor.visit(self)
を呼ぶように統一できて少しすっきりします。
class Engine(ICarElement):
def accept(self, visitor):
visitor.visit(self)
Visitor のサブクラスは、PrintVisitor
のように visit()
の実装を直接定義しても良いし、DoVisitor
のように抽象クラス (インターフェース) のデフォルト実装を使うのも良いでしょう。
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パターンとダックタイピングの比較]
(http://d.hatena.ne.jp/podhmo/20101127/1290845198) によると、エキスパート Python プログラミング に載っていたのかな?もう何年も前のことで、そのとき読書会に参加してそこで話題になった記憶はあるけれど、コードは全く覚えていませんでした。