Update 2023.11.19 2017.05.05

Python デザインパターン サンプルコード Visitor
結城 浩「Java言語で学ぶデザインパターン入門」をPython化
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

◆◆Visitor パターンの使われる場面◆◆

GoF が言っていることは何かさっぱり判らないのが普通でしょう。結城氏もそうだったのは本人が言っています。でもこの Visitor パターンを学べばすべて得心がいくのではないでしょうか。

GoFのC++サンプルではある種のコンパイラを対象としている。ちょっと考えれば判るようにソースコードの翻訳は,型チェック,コードの最適化,制御フロー分析,事前に値を割り当てられている変数のチェック,など多岐にわたる。これらを片っ端から片付けてもよいのだが見通しの悪いメンテナンスがしにくいプログラムになってしまう。

Visitor パターンの第1の目的はデータ構造側と処理側を分離することにある。コンパイラもこのような構成にすればあとあとデータ構造を変更しないで処理を追加したりするとき干渉する範囲を最小限にしておくことができる。これが GoF の言っているすべてである。

Java サンプルは Composite パターンと同じものであり,ディレクトリエントリ(フォルダとファイルのツリー)を対象とする。Composite パターンと戦術は似ているが戦略はまったく異なる。コンパイラも必ず再帰呼び出しは使われるので,このサンプルもあながち的外れというわけではない。


(2023-11-19)Python3.11で動作確認済み


◆◆Visitor パターンとは◆◆

GoFによれば,Visitor パターンの目的は, 「あるオブジェクト構造上の要素で実行されるオペレーションを表現する。Visitor パターンにより,オペレーションを加えるオブジェクトのクラスに変更を加えずに,新しいオペレーションを定義することができるようになる。」

GoFによれば,Visitor パターンは次のような場合に使用する。
・オブジェクト構造にインタフェースが異なる多くのクラスのオブジェクトが存在し,これらのオブジェクトに対して,各クラスで別々に定義されているオペレーションを実行したい場合。
・関連のない異なるオペレーションをオブジェクト構造の中のオブジェクトに対して実行する必要があり,さらに,これらのオペレーションをクラスに持たせることで,クラスを“汚くする”ことを避けたい場合。visitor を利用すれば関連するオペレーションを1つのクラスの中に定義するので,それらをまとめておくことができるようになる。オブジェクト構造が多くのアプリケーションで共有されるときには,オペレーションをそれらのアプリケーションで共通に使うことができるように Visitor パターンを使用する。
・オブジェクト構造を定義するクラスはほとんど変わらないが,その構造に新しいオペレーションを定義することがしばしば起こる場合。オブジェクト構造のクラスを変更する場合には,すべての visitor のインタフェースを再定義する必要があり,潜在的にコストは高くつく。もしオブジェクト構造のクラスを変更することがしばしばあるならば,それらのクラスにオペレーションを定義しておく方がよいだろう。

GoFによれば,Visitor パターンの関連するパターンは次のようなものである。
Composite パターン:Visitor パターンは,Composite パターンで定義されるオブジェクト構造上にオペレーションを適用するために使うことができる。
Interpreter パターン:Visitor パターンを言語の解釈のために適用してもよい。

◆◆サンプルのデータ構造◆◆

Java サンプルはディレクトリエントリを対象としている。Windows で言えば,フォルダ(ディレクトリ)とファイルである。一般的なツリー構造と違う特徴は,フォルダは中身がなくてもフォルダであり将来に中身が入る可能性があり,ファイルは中身を入れることができない。

【サンプルの入出力のツリーを Windows Explorer 風に表現したもの(カッコ内はサイズ)】
root
  = bin
      = vi(10000)
      = latex(20000)
  = tmp
  = usr
      = yuki
          = diary.html(100)
          = Composite.java(200)
      = hanako
          = memo.tex(300)
      = tomura
          = game.doc(400)
          = junk.mail(500)
このようなデータ構造に対して,Composite パターンでも Visitor パターンでも対応できることになる。逆に Visitor パターンの対象はこのような構造になっていなければならないということではない。

◆◆Visitor パターンのサンプルのクラス構成◆◆

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
                       Element                   Visitor
                       (accept)             (visit, visitFile, visitDirectory)
                           ↑                      ↑
                         Entry                  ListVisitor
                (getName, getSize, add)    (visitFile, visitDirectory)

                      ↑           ↑
                   File        Directory
 (getName, getSize, accept)   (getName, getSize, add, accept)
class Element は,Java ではインターフェイスです。Visitor を受け入れるインターフェイスであり処理される要素です。
class Entry は,抽象メソッドでありテンプレートです。
class File は,ファイルを取り扱います
class Directory は,フォルダ(ディレクトリ)を取り扱います。また,ファイルとフォルダ(ディレクトリ)をツリーに登録することができます。
上の2つはデータ構造側です。
class Visitor は,抽象メソッドでありテンプレートです。
class ListVisitor は,処理側つまり訪問者です。

◆◆データ構造の詳細◆◆

class Directory のメソッド add はメインルーチンだけから呼ばれます。ファイルとフォルダ(ディレクトリ)をツリーに登録することができます。

ツリーの各ノード(ファイルとフォルダ(ディレクトリ))の登録はすべてクライアント;メインルーチンから行います。この登録の書式がデータ構造を特徴づけています。応用するときはこの登録を自動化する必要があるかと考えます。

登録の方法は次のようにします。
    usrdir = Directory("usr")   # "usr"をインスタンス化してインスタンス「usrdir」を得る
    rootdir.add(usrdir)         # 「usrdir」をインスタンス「rootdir」のリストに加える

    yuki = Directory("yuki")    # "yuki"をインスタンス化してインスタンス「yuki」を得る
    usrdir.add(yuki)            # 「yuki」をインスタンス「usrdir」のリストに加える

    yuki.add(File("diary.html", 100))  # "diary.html"をインスタンス化して
                                       # インスタンス「yuki」のリストに加える
このようにしてすべてのノード(ファイルとフォルダ(ディレクトリ))はインスタンス化されます。そのうち各ディレクトリの各インスタンスはそれぞれリストを持っています(リストはコンストラクタで作られる)。各ディレクトリの中身(子ノード)はそのリストに入ることになります。上の例はリストが3つあり最後のファイルも含めて4階層になっていることが判ると思います。

ツリー構造のアルゴリズムをまったく意識しなくても上のソースコードのように決まった書式で登録ができます。登録される子ノードのインスタンスはメソッド add の引数となり,メソッド add の前に親ノードのインスタンスを付けるという簡単な書式です。

これで出来上がったリストは各インスタンスに所属しているだけであり,ネスト(入れ子)構造になっているわけではありません。リストの親子関係はそれの所属するインスタンスに記録されているわけです。

ファイルとフォルダ(ディレクトリ)の棲み分けはインスタンス化するときだけクラスを選ぶだけです。あとはすべて区別していません。そのことは同名メソッドで抽象メソッドをオーバーライドすることで実現しています。

◆◆Visitor パターンの実装◆◆

この実装の第1の特徴は if 文がないということです(Java サンプルも同じ)。最後にある if は,自分がトップレベルにあるか判断するものです。オブジェクト指向を徹底すると if 文を減らせます。そうすると,アルゴリズムが簡単になりミスもなくなります。Visitor パターンのオブジェクトの使い方は独力で思いつくようなものではないです。そのあたりを詳細に説明します。

class Entry は,抽象メソッドであり詳細は省略します。
def __str__はインスタンスを print すると呼ばれます。これがないままインスタンスを print するとメモリアドレスが表示されるだけです。デザインパターンではインスタンスを自在に使いこなすことが多いのでたいへん便利な特殊関数です。

class File,class Directory のメソッド accept は処理の受け入れの了承です。処理を申し込むのは class ListVisitor からです(これが訪問者です)。最初の1回だけはメインルーチンから呼ばれます。つまり,class ListVisitor は二重にインスタンス化されていますが,動作を解析するのに無視してかまいません。

データ構造側の呼ばれ方を次に示しますが,entry はツリーの親ノードのインスタンスです。これにより,2つのクラスの accept が呼び分けられます。メインルーチンからは大元のインスタンス rootdir を使います。引数は自分じしんのインスタンスです(戻るのに使われる)。

【class File,class Directory の呼ばれ方】

        it = iter(directory.dir)
        for entry in it:
            entry.accept(self)
        self.currentdir = savedir
(メインルーチンからは最初の1回だけ)
    rootdir.accept(ListVisitor())

処理の受け入れの了承はどのようにするのでしょうか。メソッド accept を次に示します。

【class File,class Directory のメソッド accept】

    def accept(self, v):
        v.visit(self)

2つのクラスとも同じであり,呼ばれた引数をメソッド visit の前に付け自分じしんのインスタンスを引数にしています。結局,元に戻り,class ListVisitor のメソッド visit を呼ぶことになるのです。 実は Java ではメソッド visit がオーバロード(シグニチャが違う同名メソッド)なのですが,Python では親クラスで引数のインスタンスを確認して呼び分けています。

換言すれば,Visitor がデータ構造側に処理の了承を得るために accept を送り付け,データ構造側は引数のインスタンスを利用して送り元の処理メソッドを場合分けして起動しています。

データ構造側から起動された処理メソッド次に示します。データ構造のインスタンスがツリー構造になっていますのでそのインスタンスを利用して再帰呼び出しを行います。処理を変更したい場合はここだけ修正すればよいことになります。

【処理メソッド visitFile と visitDirectory(Java では visit)】
71
72
73
74
75
76
77
78
79
80
    def visitFile(self, file):      # ファイルを訪問したときに呼ばれる
        sys.stdout.write("{0}/{1}\n".format(self.currentdir, file))
    def visitDirectory(self, directory): # ディレクトリを訪問したときに呼ばれる
        sys.stdout.write("{0}/{1}\n".format(self.currentdir, directory))
        savedir = self.currentdir
        self.currentdir = "{0}/{1}".format(self.currentdir, directory.getName())
        it = iter(directory.dir)
        for entry in it:
            entry.accept(self)
        self.currentdir = savedir

親子関係があるインスタンスを利用することにより,親子関係の再帰定義を意識することなく,また,ファイルとフォルダ(ディレクトリ)を意識することなく,単純な呼び出しで複雑なアルゴリズム?を実現しています(Python の手法を使うとこれが1行で書けるそうです)。

【注意】このような構成はアルゴリズムと言わないかもしれません。やはりデザインパターンと言うのがいいかもしれません。
この Web サイトでは Python 独自の格好いい書き方は控えています。C++ でも Java でも Python で書いても似たようになることを狙っています。アルゴリズムやデザインパターンを学ぶにはその方が良いと考えます。

◆◆ソースコード◆◆

このWebページの左上隅からダウンロードできます。

ソースファイルは1つです。
・Visitor;Visitor パターンのPythonサンプル

【Visitor】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from abc import ABCMeta, abstractmethod

class Visitor(metaclass=ABCMeta):

    def visit(self, obj):           # Javaのオーバーロードの代わり
        if isinstance(obj, File):
            self.visitFile(obj)
        elif isinstance(obj, Directory):
            self.visitDirectory(obj)
    @abstractmethod
    def visitFile(self, file):
        pass
    @abstractmethod
    def visitDirectory(self, directory):
        pass

class Element(metaclass=ABCMeta):

    @abstractmethod
    def accept(self, v):
        pass

class Entry(Element, metaclass=ABCMeta):

    @abstractmethod
    def getName(self):              # 名前を得る
        pass
    @abstractmethod
    def getSize(self):              # サイズを得る
        pass
    def add(self, entry):           # エントリを追加する
        raise FileTreatmentException
    def __str__(self):              # インスタンスをprintすると呼ばれます
        return "{} ({})".format(self.getName(), self.getSize())

class File(Entry):
    def __init__(self, name, size):
        self.name = name
        self.size = size
    def getName(self):
        return self.name
    def getSize(self):
        return self.size
    def accept(self, v):
        v.visit(self)

class Directory(Entry):
    def __init__(self, name):
        self.name = name            # ディレクトリの名前
        self.dir = []               # ディレクトリエントリの集合
    def getName(self):              # 名前を得る
        return self.name
    def getSize(self):              # サイズを得る
        self.size = 0
        it = iter(self.dir)
        for entry in it:
            self.size += entry.getSize()
        return self.size
    def add(self, entry):
        self.dir.append(entry)
        return self
    def accept(self, v):            # 訪問者の受け入れ
        v.visit(self)

class ListVisitor(Visitor):
    def __init__(self):
        self.currentdir = ""        # 現在注目しているディレクトリ名
    def visitFile(self, file):      # ファイルを訪問したときに呼ばれる
        sys.stdout.write("{}/{}\n".format(self.currentdir, file))
    def visitDirectory(self, directory): # ディレクトリを訪問したときに呼ばれる
        sys.stdout.write("{}/{}\n".format(self.currentdir, directory))
        savedir = self.currentdir
        self.currentdir = "{}/{}".format(self.currentdir, directory.getName())
        it = iter(directory.dir)
        for entry in it:
            entry.accept(self)
        self.currentdir = savedir

class FileTreatmentException(Exception):
    pass

def main():
    sys.stdout.write("Making root entries...\n")
    rootdir = Directory("root")
    bindir = Directory("bin")
    tmpdir = Directory("tmp")
    usrdir = Directory("usr")
    rootdir.add(bindir)
    rootdir.add(tmpdir)
    rootdir.add(usrdir)
    bindir.add(File("vi", 10000))
    bindir.add(File("latex", 20000))
    rootdir.accept(ListVisitor())

    sys.stdout.write("Making user entries\n")
    yuki = Directory("yuki")
    hanako = Directory("hanako")
    tomura = Directory("tomura")
    usrdir.add(yuki)
    usrdir.add(hanako)
    usrdir.add(tomura)
    yuki.add(File("diary.html", 100))
    yuki.add(File("Composite.java", 200))
    hanako.add(File("memo.txt", 300))
    tomura.add(File("game.doc", 400))
    tomura.add(File("junk.mail", 500))
    rootdir.accept(ListVisitor())

if __name__ == '__main__':
    main()
"""
Making root entries...
/root (30000)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (0)
Making user entries
/root (31500)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (1500)
/root/usr/yuki (300)
/root/usr/yuki/diary.html (100)
/root/usr/yuki/Composite.java (200)
/root/usr/hanako (300)
/root/usr/hanako/memo.txt (300)
/root/usr/tomura (900)
/root/usr/tomura/game.doc (400)
/root/usr/tomura/junk.mail (500)
"""

以上

トップページに戻る
inserted by FC2 system