Update 2023.11.16 2017.05.05

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

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

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

Pythonの「print」が「sys.stdout.write」のラッパーであることはあまりにも有名な話である。そういえば,丸カッコの中身の書式が同じだったような。「print」は改行や半角スペースを出力するので「sys.stdout.write」より少し余分なことをしていることになる。

このようにラッパーは,ラッピング対象の名前を変え,実行の内容は変えないかまたは少し変えたものである。

似ているが違うもの,例えば,平社員と管理職の給与計算,エコノミークラスとファーストクラスのサービスなどオプションの違いを見かけ上同じしてしまう。これは充電器のACアダプタのようにつながらないものをつなげてしまう接続装置なのである。つまり,一致してないインターフェイスを一致させてしまうのがアダプタである。

他から与えられたクラスが修正を禁止されている場面がよくあるが(それが普通である),元をそのままにして高品質化,高機能化できるのがAdapter パターンである。

ただ,似ているパターンも多いので,次項の関連するパターンを参照してほしい。

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

◆◆Adapter パターンとは◆◆

GoFによれば,Adapter パターンの目的は, 「あるクラスのインタフェースを,クライアントが求める他のインタフェースへ変換する。Adapter パターンは,インタフェースに互換性のないクラス同士を組み合わせることができるようにする。」

GoFによれば,Adapter パターンの別名は,Wrapper パターンだそうだ。

GoFによれば,Adapter パターンは,次のような状況で使うことができる。
・既存のクラスを利用したいが,そのインタフェースが必要なインタフェースと一致していない場合。
・まったく無関係で予想もつかないようなクラス(必ずしも互換性のあるインタフェースを持つとは限らない)とも協調していける,再利用可能なクラスを作成したい場合。
・(オブジェクトに適用する Adapter パターンのみ)既存のサブクラスを複数利用したいが,それらすべてのサブクラスをさらにサブクラス化することで,そのインタフェースを適合させることが現実的でない場合。オブジェクトに適用する Adapter パターンでは,その親クラスのインタフェースを適合させればよい。

GoFによれば,関連するパターンは次のようなものである。
Bridge パターン: このパターンは,オブジェクトを基にした adapter によく似た構造をしているが,目的が異なっている。Bridge パターンの目的は,インタフェースと実装を分離して,それらを容易にかつ独立して変更できるようにすることである。Adapter パターンの目的は,“既存の”オブジェクトのインタフェースを変換することである。
Decorator パターン: このパターンでは,インタフェースを変更することなく,オブジェクトに対して機能の追加を行う。アプリケーションにとっては,adapter よりも decorator の方が透過性が高い。その結果 Decorator パターンは,純粋な adapter では不可能な,再帰的なオブジェクト構造をサポートする。
Proxy パターン: このパターンでは,他のオブジェクトに対する代表あるいは代理を定義するが,その際にインタフェースを変更することはない。

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
ソースコードは2種類あるが,原著にならって,「render1.py」を解説します。
            Renderer                      HtmlWriter
    (__subclasshook__)     (header, title, start_body, end_body, footer)
                                  ↑
         TextRenderer             HtmlRenderer
    (header, paragraph, footer)  (header, paragraph, footer)
       ↑                ↑
               Page
          (add_paragraph, render)
継承関係はありません。↑は参照関係です。

class Page は,メインルーチンから呼ばれ,レンダラーを呼ぶクラスです。
class TextRenderer は,プレーンテキストで簡単な描画を実行するクラスです。
class HtmlRenderer は,class HtmlWriter のアダプタクラスです。
class HtmlWriter は,Webページで簡単な描画を実行するクラスです。
           旧いクラスを想定していて,ラッピング対象です。

◆◆各クラスの役割◆◆

このサンプルの働きは,2種類の簡単なページを描画するものです。1つは,プレーンテキストで描画され,もう1つは,Webページに描画されます。

実際の出力が次です。プレーンテキスト描画,Webページ描画,Webページの失敗の3つが出力されています。プレーンテキスト描画とWebページ描画は同じものが出力されていることが判ります。

【サンプル出力】

      Plain Text
      ==========
This is a very short
plain-text paragraph
that demonstrates the
simple TextRenderer
class.

This is another short
paragraph just so that
we can see two
paragraphs in action.

<!doctype html>
<html>
<head><title>HTML</title></head>
<body>
<p>This is a very short HTML paragraph that demonstrates
the simple HtmlRenderer class.</p>
<p>This is another short paragraph just so that we can
see two paragraphs in action.</p>
</body>
</html>
Expected object of type Renderer, got HtmlWriter

HTML コード部分をブラウザで見ると
HTML

This is a very short HTML paragraph that demonstrates the simple HtmlRenderer class.

This is another short paragraph just so that we can see two paragraphs in action.

inserted by FC2 system
【注意】上のソースは,HTML で書かれているわけであり,このWebページ全体から見れば二重構造(ネスト)になっているのでbugるかもしれません。少なくとも HTML5 チェッカーからは警告を受けます。

プレーンテキストで描画したのは,class TextRenderer です。Webページで描画したのは,class HtmlWriter です。後者は旧いクラスを想定していて,修正することが許可されていません。この2つのクラスのメソッドは統一されていません。

そこで,アダプタclass HtmlRenderer を用意して,class HtmlWriter を委譲を使ってラッピングします。class TextRenderer と アダプタ class HtmlRenderer は,メソッドが統一されているのです。

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

◆◆class Renderer

1つの import と1つのクラスが,Pythonのバージョンによって選択されています。

17行目と19行目の import文は,バージョン3.2以前・以後で選択します。
  from xml.sax.saxutils import escape
  from html import escape
バージョン3.6における動作は,前者は,"<", ">", "&"を変換します。後者は,それに加えて,",も変換します。バージョン3.6で後者を使う意義がよく判りません。

class Renderer が,23行目と35行目にありますが,バージョン3.6では前者を選択します。
class Renderer には,特殊メソッド__subclasshook__があるだけです。これは,84行目の isinstance が実行されたときに自動的に呼ばれます。

特殊メソッドが再実装されたときに,いつ呼ばれるかは,注意が必要です。

この特殊メソッドは,バージョン3.3からあるそうですが,サンプルとほぼ同じ再実装が,Python ドキュメントに例がありますので,そちらも見てください。

https://docs.python.jp/3/library/abc.html
『Python ≫ 3.6.3 ドキュメント ≫ Python 標準ライブラリ ≫ 29. Python ランタイムサービス ≫ 』

この class Renderer の特殊メソッドが呼ばれているのは,class Page のコンストラクタの isinstance からです(84行目)。class Page をインスタンス化しているのは,メインルーチンの58行目,68行目,74行目です。

特殊メソッドでしていることは,インスタンス化の第2引数のクラスの中に,30行目で定義した3つのメソッド
  methods = ("header", "paragraph", "footer")
が,揃っているかを確認しています。

◆◆メインルーチンとclass Page

コマンドラインの第1引数でこのモジュールが起動されると,必ず,179行目が走ります。グローバル関数を呼んでいるので,インデントが0の def main が呼ばれます。

メインルーチンでは,最初に,描画したい文章を指定します。

次に,class Page をインスタンス化(58行目,68行目,74行目)して,そのインスタンスでメソッドを呼ぶだけです。インスタンス化の第2引数で,
  class TextRenderer
  class HtmlRenderer
  class HtmlWriter
を指定します。前項で説明したメソッドの確認が行われます。3番目は,メソッドが揃っていないのでエラーになります。

class Page では,96行目のメソッドが,
  class TextRenderer
  class HtmlRenderer
の3つのメソッドを呼んでいますが,class Page をインスタンス化したときに上のクラスのインスタンスを引数で渡されていますので,そのインスタンスを使ってメソッドを呼んでいます。

つまり,呼ぶクラスをまったく意識しないで,メソッドを呼んでいます。

◆◆class TextRenderer, class HtmlWriter, class HtmlRenderer

class TextRenderer には,class HtmlRenderer と統一された3つのメソッドがプレーンテキストを描画するように実装されています。

一方,実際にWebページを描画するのは,class HtmlWriter なので,統一された3つのメソッドを持つ class HtmlRenderer は,class HtmlWriter に委譲します。具体的には,class HtmlRenderer の3つのメソッドから class HtmlWriter の適切なメソッドを呼ぶことになります。

つまり,class HtmlWriter がラッピング対象であり,class HtmlRenderer は,アダプタまたはラッパーと呼ばれます。

◆◆ソースコード◆◆

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

ソースファイルは3つです。サンプル出力は本文にあります。
・render1.py;Adapter パターンのPythonサンプル(その1)
・render2.py;Adapter パターンのPythonサンプル(その2)
・Qtrac.py;render2.pyがインポートする

【render1.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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/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 abc
import collections
import sys
import textwrap
if sys.version_info[:2] < (3, 2):
    from xml.sax.saxutils import escape
else:
    from html import escape

# Thanks to Nick Coghlan for these!
if sys.version_info[:2] >= (3, 3):
    class Renderer(metaclass=abc.ABCMeta):

        @classmethod
        def __subclasshook__(Class, Subclass):
            if Class is Renderer:
                attributes = collections.ChainMap(*(Superclass.__dict__
                        for Superclass in Subclass.__mro__))
                methods = ("header", "paragraph", "footer")
                if all(method in attributes for method in methods):
                    return True
            return NotImplemented
else:
    class Renderer(metaclass=abc.ABCMeta):

        @classmethod
        def __subclasshook__(Class, Subclass):
            if Class is Renderer:
                needed = {"header", "paragraph", "footer"}
                for Superclass in Subclass.__mro__:
                    for meth in needed.copy():
                        if meth in Superclass.__dict__:
                            needed.discard(meth)
                    if not needed:
                        return True
            return NotImplemented


MESSAGE = """This is a very short {} paragraph that demonstrates
the simple {} class."""

def main():
    paragraph1 = MESSAGE.format("plain-text", "TextRenderer")
    paragraph2 = """This is another short paragraph just so that we can
see two paragraphs in action."""
    title = "Plain Text"
    textPage = Page(title, TextRenderer(22))
    textPage.add_paragraph(paragraph1)
    textPage.add_paragraph(paragraph2)
    textPage.render()

    print()

    paragraph1 = MESSAGE.format("HTML", "HtmlRenderer")
    title = "HTML"
    file = sys.stdout
    htmlPage = Page(title, HtmlRenderer(HtmlWriter(file)))
    htmlPage.add_paragraph(paragraph1)
    htmlPage.add_paragraph(paragraph2)
    htmlPage.render()

    try:
        page = Page(title, HtmlWriter())
        page.render()
        print("ERROR! rendering with an invalid renderer")
    except TypeError as err:
        print(err)


class Page:

    def __init__(self, title, renderer):
        if not isinstance(renderer, Renderer):
            raise TypeError("Expected object of type Renderer, got {}".
                    format(type(renderer).__name__))
        self.title = title
        self.renderer = renderer
        self.paragraphs = []


    def add_paragraph(self, paragraph):
        self.paragraphs.append(paragraph)


    def render(self):
        self.renderer.header(self.title)
        for paragraph in self.paragraphs:
            self.renderer.paragraph(paragraph)
        self.renderer.footer()


class TextRenderer:

    def __init__(self, width=80, file=sys.stdout):
        self.width = width
        self.file = file
        self.previous = False


    def header(self, title):
        self.file.write("{0:^{2}}\n{1:^{2}}\n".format(title,
                "=" * len(title), self.width))


    def paragraph(self, text):
        if self.previous:
            self.file.write("\n")
        self.file.write(textwrap.fill(text, self.width))
        self.file.write("\n")
        self.previous = True


    def footer(self):
        pass


class HtmlWriter:

    def __init__(self, file=sys.stdout):
        self.file = file


    def header(self):
        self.file.write("<!doctype html>\n<html>\n")


    def title(self, title):
        self.file.write("<head><title>{}</title></head>\n".format(
                escape(title)))

    def start_body(self):
        self.file.write("<body>\n")


    def body(self, text):
        self.file.write("<p>{}</p>\n".format(escape(text)))


    def end_body(self):
        self.file.write("</body>\n")


    def footer(self):
        self.file.write("</html>\n")


class HtmlRenderer:

    def __init__(self, htmlWriter):
        self.htmlWriter = htmlWriter


    def header(self, title):
        self.htmlWriter.header()
        self.htmlWriter.title(title)
        self.htmlWriter.start_body()


    def paragraph(self, text):
        self.htmlWriter.body(text)


    def footer(self):
        self.htmlWriter.end_body()
        self.htmlWriter.footer()


if __name__ == "__main__":
    main()

【render2.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
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/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 abc
import sys
import textwrap
if sys.version_info[:2] < (3, 2):
    from xml.sax.saxutils import escape
else:
    from html import escape
import Qtrac

@Qtrac.has_methods("header", "paragraph", "footer")
class Renderer(metaclass=abc.ABCMeta): pass

MESSAGE = """This is a very short {} paragraph that demonstrates
the simple {} class."""

def main():
    paragraph1 = MESSAGE.format("plain-text", "TextRenderer")
    paragraph2 = """This is another short paragraph just so that we can
see two paragraphs in action."""
    title = "Plain Text"
    textPage = Page(title, TextRenderer(22))
    textPage.add_paragraph(paragraph1)
    textPage.add_paragraph(paragraph2)
    textPage.render()

    print()

    paragraph1 = MESSAGE.format("HTML", "HtmlRenderer")
    title = "HTML"
    file = sys.stdout
    htmlPage = Page(title, HtmlRenderer(HtmlWriter(file)))
    htmlPage.add_paragraph(paragraph1)
    htmlPage.add_paragraph(paragraph2)
    htmlPage.render()

    try:
        page = Page(title, HtmlWriter())
        page.render()
        print("ERROR! rendering with an invalid renderer")
    except TypeError as err:
        print(err)


class Page:

    def __init__(self, title, renderer):
        if not isinstance(renderer, Renderer):
            raise TypeError("Expected object of type Renderer, got {}".
                    format(type(renderer).__name__))
        self.title = title
        self.renderer = renderer
        self.paragraphs = []


    def add_paragraph(self, paragraph):
        self.paragraphs.append(paragraph)


    def render(self):
        self.renderer.header(self.title)
        for paragraph in self.paragraphs:
            self.renderer.paragraph(paragraph)
        self.renderer.footer()


class TextRenderer:

    def __init__(self, width=80, file=sys.stdout):
        self.width = width
        self.file = file
        self.previous = False


    def header(self, title):
        self.file.write("{0:^{2}}\n{1:^{2}}\n".format(title,
                "=" * len(title), self.width))


    def paragraph(self, text):
        if self.previous:
            self.file.write("\n")
        self.file.write(textwrap.fill(text, self.width))
        self.file.write("\n")
        self.previous = True


    def footer(self):
        pass


class HtmlWriter:

    def __init__(self, file=sys.stdout):
        self.file = file


    def header(self):
        self.file.write("<!doctype html>\n<html>\n")


    def title(self, title):
        self.file.write("<head><title>{}</title></head>\n".format(
                escape(title)))

    def start_body(self):
        self.file.write("<body>\n")


    def body(self, text):
        self.file.write("<p>{}</p>\n".format(escape(text)))


    def end_body(self):
        self.file.write("</body>\n")


    def footer(self):
        self.file.write("</html>\n")


class HtmlRenderer:

    def __init__(self, htmlWriter):
        self.htmlWriter = htmlWriter


    def header(self, title):
        self.htmlWriter.header()
        self.htmlWriter.title(title)
        self.htmlWriter.start_body()


    def paragraph(self, text):
        self.htmlWriter.body(text)


    def footer(self):
        self.htmlWriter.end_body()
        self.htmlWriter.footer()


if __name__ == "__main__":
    main()

【Qtrac.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
#!/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 abc
import collections
import errno
import functools
import os
import sys


def coroutine(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        generator = function(*args, **kwargs)
        next(generator)
        return generator
    return wrapper


if sys.version_info[:2] < (3, 3):
    def remove_if_exists(filename):
        try:
            os.remove(filename)
        except OSError as err:
            if err.errno != errno.ENOENT:
                raise
else:
    def remove_if_exists(filename):
        try:
            os.remove(filename)
        except FileNotFoundError:
            pass # All other exceptions are passed to the caller


# Thanks to Nick Coghlan for these!
if sys.version_info[:2] >= (3, 3):
    def has_methods(*methods):
        def decorator(Base):
            def __subclasshook__(Class, Subclass):
                if Class is Base:
                    attributes = collections.ChainMap(*(Superclass.__dict__
                            for Superclass in Subclass.__mro__))
                    if all(method in attributes for method in methods):
                        return True
                return NotImplemented
            Base.__subclasshook__ = classmethod(__subclasshook__)
            return Base
        return decorator
else:
    def has_methods(*methods):
        def decorator(Base):
            def __subclasshook__(Class, Subclass):
                if Class is Base:
                    needed = set(methods)
                    for Superclass in Subclass.__mro__:
                        for meth in needed.copy():
                            if meth in Superclass.__dict__:
                                needed.discard(meth)
                        if not needed:
                            return True
                return NotImplemented
            Base.__subclasshook__ = classmethod(__subclasshook__)
            return Base
        return decorator


# Thanks to Nick Coghlan for this!
class Requirer(metaclass=abc.ABCMeta):

    # Since we have rules for adding new expected attributes, we *do*
    # perform the check for subclasses
    @classmethod
    def __subclasshook__(Class, Subclass):
        methods = set()
        for Superclass in Subclass.__mro__:
            if hasattr(Superclass, "required_methods"):
                methods |= set(Superclass.required_methods)
        attributes = collections.ChainMap(*(Superclass.__dict__
                for Superclass in Class.__mro__))
        if all(method in attributes for method in methods):
            return True
        return NotImplemented


def report(message="", error=False):
    if len(message) >= 70 and not error:
        message = message[:67] + "..."
    sys.stdout.write("\r{:70}{}".format(message, "\n" if error else ""))
    sys.stdout.flush()

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