Update 2023.11.17 2017.05.05

Python デザインパターン サンプルコード Proxy
Mark Summerfield『実践 Python 3』デザインパターンのサンプルコード
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

著作権の問題があるので,本に書いてないことだけを解説します。つまり,視点を変えて解説します。

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

以下は,同じサイトの別記事について説明されている。
https://yamakatsusan.web.fc2.com/hp_software/pythonpattern13.html
『Proxy パターン 結城 浩「Java言語で学ぶデザインパターン入門」をPython化』

Proxy パターンの使われる場面を説明するのはかなり難しいです。

サンプルを見れば判るのだが,抽象クラスが1つと具象クラスが2つある。これは Decorator パターンと酷似している。違うのは,Decorator パターンはクラスを明示的に呼び分けるが,Proxy パターンは明示的に呼び分けることはしない。

Decorator パターンではクラスを使う人がクラスの内容をよく知った上で使うが,Proxy パターンではクラスの内容を知らないで使う。つまり,使う人にはメリットが良く判らないわけです。

例えば,「間に入ったプロキシサーバがキャッシュを返すためネットにつながっていて実体を見ていると錯覚する」とか「実体を処理をするメインなオブジェクトと呼び出し元の間にこっそり入って知らんぷりしていて盗聴もするオブジェクト」とか,このようなことを hook フック(横取り)と言うことがあります。

GoFのC++のサンプルでは,文書エディタの中でグラフィックオブジェクトの表示に時間がかかるのでとりあえずダミーを見せておき,本当に使うときになったら実体を見せるというものでした。これも hook と言えるでしょう。

クラスを作る人が何かしらこっそり hook したいものがあるときProxy パターンを仕組む!?と言ってよいでしょう。

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


◆◆Proxy パターンとは◆◆

GoFによれば,Proxy パターンの目的は, 「あるオブジェクトへのアクセスを制御するために,そのオブジェクトの代理,または入れ物を提供する。」

GoFによれば,オブジェクトへの単なるポインタよりも多機能で精巧な参照が必要なときには,いつでも Proxy パターンが適用可能である。ここでは,Proxy パターンが適用可能な一般的な状況をいくつかあげる。
1.remote proxy は,別のアドレス空間にあるオブジェクトのローカルな代理を提供する。NEXTSTEP[Add94]はこの目的のために NXProxy クラスを用いている。Coplien[Cop92]は,この種の proxy を“Ambassador(大使)”と呼んでいる。
2.virtual proxy は,コストの高いオブジェクトを要求があり次第生成する。「動機」の節で説明した ImageProxy オブジェクトは,virtual proxy の一例である。
3.protection proxy は,実オブジェクトへのアクセスを制御する。オブジェクトごとに異なるアクセス権が必要なときには,この protection proxy は有用である。たとえば,Choices オペレーティングシステム[CIRM93]の KernelProxy オブジェクトは,オペレーティングシステムのオブジェクトへのアクセスを保護する。
4.smart reference は,通常のポインタに代わるものである。オブジェクトがアクセスされるときに,その smart reference はさらにアクションを実行する。この proxy の典型的な使い方を以下にあげる。
・参照されなくなった実オブジェクトを自動的に解放できるように,実オブジェクトへの参照個数を数えておく(smart pointer とも呼ばれる[Ede92])。
・永続オブジェクトが初めて参照されたときに,それをメモリ上にロードする。
・他のオブジェクトが実オブジェクトを変更することのないように,実オブジェクトがアクセスされる前に,それがロックされていることを確認する。

GoFによれば,Proxy パターンの関連するパターンは次のようなものである。
Adapter パターン:adapter は,オブジェクトに異なるインタフェースを提供する。それとは対照的に,proxy は RealSubject オブジェクトと同じインタフェースを提供する。しかし,アクセス保護のために使われる proxy は,RealSubject オブジェクトならば実行するであろうオペレーションの実行を拒否するかもしれない。したがって,実際上は,proxy のインタフェースは Subject クラスのインタフェースの一部かもしれない。
Decorator パターン:decorator は proxy と似た形態で実装されるが,両者の目的は異なる。decorator はあるオブジェクトに1つ,または複数の責任を追加する。一方 proxy は,あるオブジェクトへのアクセスを制御する。proxy が decorator のように実装される程度は,proxy の種類により異なる。protection proxy はd ecorator とまったく同じように実装されるかもしれない。一方,remote proxy は RealSubject オブジェクトへの直接参照を持たず,“ホストIDとそのホスト上でのローカルアドレス”などの間接参照だけを持つであろう。virtual proxy は,生成時にはファイル名などの間接参照のみを持つが,最後には直接参照を手に入れて使うようになる。

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
        Imageライブラリ
        (内容省略)

             ↑(参照)
        ImageProxy
        (内容省略)
             ↑(参照)
        def draw_and_save_image
        (内容省略)
Imageライブラリ は,XPM画像を作るためのライブラリです。
class ImageProxy は,上のライブラリを使いながら実体を作らず,proxyをつくります。
def draw_and_save_image は,XPM画像を作る命令文の集まりです。

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

サンプルは,XPM画像を作るのですが,1つは,即実体をつくり,もう1つは,proxyを作ります。

proxyとは,画像作成のようにコストのかかる作業を途中何度やり直してもコストがかからないように最後まで実体をつくらないで,画像作成のコマンドを記録していくものです。

なぜそのようなものを作るのか,その理由は,巻頭に書かれています。

XPM画像を作成するためのライブラリを原著者がオリジナルとして作ったものが,
  Imageライブラリ
です。この使い方についての詳細は省略しますが,
  def draw_and_save_image
のコマンドの使い方を見ていただければ概略は判ると思います。

XPM画像は,BMPとほぼ同じであり,1画素がRGB濃度と透明度で成り立っています。それらがテキストで表現されているのが一番の特徴です。

サンプル出力の画像は次のようなものです(GIMP, XnViewなどで見ることができる)。
【image.xpm】【proxy.xpm】


この画像は,34行目のグローバル関数 def draw_and_save_image で書かれています。

image.xpm を出力するのは,
  Image.Image
です。proxy.xpm を出力するのは,
  class ImageProxy
です。もちろん,このクラスは Image.Image を参照しています。

共通のグローバル関数 def draw_and_save_image で,このように使い分けることができるのは,引数に上のクラスのインスタンスを渡されていて,画像を作るコマンドでそのインスタンスを使うからです。

メインルーチンの26行目で,image.xpm を出力するためのインスタンスを作っています。30行目で,proxy.xpm を出力ためのインスタンスを作っています。さらに,Image.Image を class ImageProxy のコンストラクタに渡しています。

このように,インスタンスを渡すことで,場合分けができるようになり,if 文が無くなるのが,オブジェクト指向のメリットです。

class ImageProxy では,52行目または55行目で,画像を作るコマンドを記録するリストを用意していて,最後の save まで画像の実体をつくらないで,コストの発生を抑えています。

役に立つ場面が判りにくいパターンですが,やっていることは単純です。画像をつくるメソッドについては詳細を省略します。

◆◆ソースコード◆◆

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

2つのソースファイル,2つのパッケージ,2つのサンプル出力があります
・imageproxy1.py;Proxy パターンのPythonサンプル(その1)
・imageproxy2.py;Proxy パターンのPythonサンプル(その2)
・Imageパッケージ;上のソースがインポートするライブラリ(4つのソースファイル)
・cyImageパッケージ;上のソースがインポートするライブラリ(7つのソースファイル)
・image.xpm;サンプル出力の画像(GIMP, XnViewなどで見ることができる)
・proxy.xpm;サンプル出力の画像(GIMP, XnViewなどで見ることができる)

ライブラリのソースコードの掲載は省略します。

【imageproxy1.py】
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
#!/usr/bin/env python3
# Copyright c 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

import os
import tempfile
try:
    import cyImage as Image
except ImportError:
    import Image


YELLOW, CYAN, BLUE, RED, BLACK = (Image.color_for_name(color)
    for color in ("yellow", "cyan", "blue", "red", "black"))


def main():
    filename = os.path.join(tempfile.gettempdir(), "image.xpm")
    image = Image.Image(300, 60)
    draw_and_save_image(image, filename)

    filename = os.path.join(tempfile.gettempdir(), "proxy.xpm")
    image = ImageProxy(Image.Image, 300, 60)
    draw_and_save_image(image, filename)


def draw_and_save_image(image, filename):
    image.rectangle(0, 0, 299, 59, fill=YELLOW)
    image.ellipse(0, 0, 299, 59, fill=CYAN)
    image.ellipse(60, 20, 120, 40, BLUE, RED)
    image.ellipse(180, 20, 240, 40, BLUE, RED)
    image.rectangle(180, 32, 240, 41, fill=CYAN)
    image.line(181, 32, 239, 32, BLUE)
    image.line(140, 50, 160, 50, BLACK)
    image.save(filename)
    print("saved", filename)


class ImageProxy:

    def __init__(self, ImageClass, width=None, height=None, filename=None):
        assert (width is not None and height is not None) or \
                filename is not None
        self.Image = ImageClass
        self.commands = []
        if filename is not None:
            self.load(filename)
        else:
            self.commands = [(self.Image, width, height)]


    def load(self, filename):
        self.commands = [(self.Image, None, None, filename)]


    def save(self, filename=None):
        command = self.commands.pop(0)
        function, *args = command
        image = function(*args)
        for command in self.commands:
            function, *args = command
            function(image, *args)
        image.save(filename)
        return image


    def set_pixel(self, x, y, color):
        self.commands.append((self.Image.set_pixel, x, y, color))


    def line(self, x0, y0, x1, y1, color):
        self.commands.append((self.Image.line, x0, y0, x1, y1, color))


    def rectangle(self, x0, y0, x1, y1, outline=None, fill=None):
        self.commands.append((self.Image.rectangle, x0, y0, x1, y1,
                outline, fill))


    def ellipse(self, x0, y0, x1, y1, outline=None, fill=None):
        self.commands.append((self.Image.ellipse, x0, y0, x1, y1,
                outline, fill))

    # Incomplete API. Unsupported are:
    # pixel(), subsample(), scale(), and size()

if __name__ == "__main__":
    main()

【imageproxy2.py】
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python3
# Copyright c 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

import os
import tempfile
try:
    import cyImage as Image
except ImportError:
    import Image


YELLOW, CYAN, BLUE, RED, BLACK = (Image.color_for_name(color)
    for color in ("yellow", "cyan", "blue", "red", "black"))


def main():
    filename = os.path.join(tempfile.gettempdir(), "image.xpm")
    image = Image.Image(300, 60)
    draw_and_save_image(image, filename)

    filename = os.path.join(tempfile.gettempdir(), "proxy.xpm")
    image = ImageProxy(Image.Image, 300, 60)
    draw_and_save_image(image, filename)


def draw_and_save_image(image, filename):
    image.rectangle(0, 0, 299, 59, fill=YELLOW)
    image.ellipse(0, 0, 299, 59, fill=CYAN)
    image.ellipse(60, 20, 120, 40, BLUE, RED)
    image.ellipse(180, 20, 240, 40, BLUE, RED)
    image.rectangle(180, 32, 240, 40, fill=CYAN)
    assert image.size == (300, 60) # force the image to be created
    assert image.width == 300 and image.height == 60
    image.line(181, 32, 239, 32, BLUE)
    image.line(140, 50, 160, 50, BLACK)
    image.save(filename)
    print("saved", filename)


class ImageProxy:

    def __init__(self, ImageClass, width=None, height=None, filename=None):
        assert (width is not None and height is not None) or \
                filename is not None
        self.Image = ImageClass
        self.__image = None
        self.commands = []
        if filename is not None:
            self.load(filename)
        else:
            self.commands = [(self.Image, width, height)]


    @property
    def image(self):
        if self.__image is None:
            command = self.commands.pop(0)
            function, *args = command
            self.__image = function(*args)
            for command in self.commands:
                function, *args = command
                function(self.image, *args)
        return self.__image


    def load(self, filename):
        self.__image = None
        self.commands = [(self.Image, None, None, filename)]


    def save(self, filename=None):
        self.image.save(filename)


    def set_pixel(self, x, y, color):
        if self.image is None:
            self.commands.append((self.Image.set_pixel, x, y, color))
        else:
            self.image.set_pixel(x, y, color)


    def line(self, x0, y0, x1, y1, color):
        if self.image is None:
            self.commands.append((self.Image.line, x0, y0, x1, y1, color))
        else:
            self.image.line(x0, y0, x1, y1, color)


    def rectangle(self, x0, y0, x1, y1, outline=None, fill=None):
        if self.image is None:
            self.commands.append((self.Image.rectangle, x0, y0, x1, y1,
                    outline, fill))
        else:
            self.image.rectangle(x0, y0, x1, y1, outline, fill)


    def ellipse(self, x0, y0, x1, y1, outline=None, fill=None):
        if self.image is None:
            self.commands.append((self.Image.ellipse, x0, y0, x1, y1,
                    outline, fill))
        else:
            self.image.ellipse(x0, y0, x1, y1, outline, fill)


    def pixel(self, x, y):
        return self.image.pixel(x, y)


    def subsample(self, stride):
        return self.image.subsample(stride)


    def scale(self, ratio):
        return self.image.scale(ratio)


    @property
    def size(self):
        return self.image.size


    @property
    def width(self):
        return self.image.width


    @property
    def height(self):
        return self.image.height


if __name__ == "__main__":
    main()

以上

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