Update 2023.11.17 2017.05.05

Python デザインパターン サンプルコード Builder
Mark Summerfield『実践 Python 3』のデザインパターン
のPythonサンプルを徹底解説します

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

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

要は,複合オブジェクトから成り立つモジュールの利用者であるクライアントから見たときに非複合オブジェクトに見えるカプセル化(隠蔽)の方法である。

たくさんのオブジェクトから成り立つ一連の多様な作業を標準のアルゴリズムで実装し,個々の作業内容は別の独立したクラスに任すことができる。

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

【重要な注意】本ソースコードファイルを起動するには第2引数が必要です。ターミナルからPythonを起動するとき,普通はプロンプト「'>'」の後に次のようにタイプします。

>python formbuilder.py -P(ケースバイケースで)

ターミナルによっては'python'は'python3'になります。ソースコードファイル(スクリプトファイル)名称が第1引数です。第2引数は本プログラムに読み込むオプションです。

「PyCharm」では,次の枠に第2引数を設定します。
  メニュー→run→Edit Configurations...→左下矢印と右上矢印の枠
  ファイル名→Modify Run Configuration...→左下矢印と右上矢印の枠
  画面によっては,Interpreter options: という枠です(左下矢印と右上矢印の枠)

「Jupyter Notebook」では,自己テストの1行目に「sys.argv[1]=」を入れます。「PyCharm」では,この行はありません。「Jupyter Notebook」にこの行を入れるのは,ハードコードと言い,固有名詞や固有な数値をコマンドラインに埋め込むことは本来避けるべきですが,この場合はやむを得ないとしましょう。ちなみにこのコードでは第1引数のような扱いになっていますが,ターミナルのコマンドラインでは第2引数なので混乱しないように。

「sys.argv[1]=」の前かまたは冒頭に「import sys」を忘れないように。

.pyではターミナルから実行されたとき自己テストが実行され,他のファイルから呼ばれたときは自己テストは無視されることも忘れないように。「Jupyter Notebook」では呼ばれる側を上側に置き,下側に参照されるようにします(本記事に関係ないかも)。


本プログラムは,htmlファイルと.pyファイルを出力します。htmlファイルはブラウザに読み込み,.pyファイルはターミナルからコマンドラインで起動します。どちらも入力フォームですが,その使い方は不明です。

「PyCharm」上に「Jupyter Notebook」上に出力されるのは出力ファイルのディレクトリだけです。

wrote C:\Users\yamak\AppData\Local\Temp\login.html
wrote C:\Users\yamak\AppData\Local\Temp\login.py

「AppData」は隠しフォルダであり,

エクスプローラ(ユーザーアカウント)→表示→表示→隠しファイル

により,エクスプローラに他のフォルダのように表示されます。

上のディレクりのフォルダの中にはたくさんのファイルが有りますが,更新日時でソートすれば1番目に見つかります。


◆◆Builder パターンとは◆◆

GoFによれば,Builder パターンの目的は, 「複合オブジェクトについて,その作成過程を表現形式に依存しないものにすることにより,同じ作成過程で異なる表現形式のオブジェクトを生成できるようにする。」

GoFによれば,Builder パターンは次のような場合に適用する。
・多くの構成要素からなるオブジェクトを生成するアルゴリズムを,構成要素自体やそれらがどのように組み合わされるのかということから独立にしておきたい場合。
・オブジェクトの作成プロセスが,オブジェクトに対する多様な表現を認めるようにしておかなければならない場合。

GoFによれば,関連するパターンは次のようなものである。
Abstract Factory パターン: このパターンは,複合オブジェクトを作成するという点で Builder パターンに類似している。主な違いは,Builder パターンでは複合オブジェクトを段階的に作成していく過程に焦点をあてているのに対して,Abstract Factory パターンでは部品の集合を強調している(それが単純であっても複雑であっても)という点である。Builder パターンでは,Product オブジェクトを最終段階で返すことになるが,Abstract Factory パターンでは即座に返す。 Composite パターン: builder は,しばしば composite を作成する。

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
                   AbstractFormBuilder
    (add_title, form, add_label, add_entry, add_button)
           ↑                  ↑
       HtmlFormBuilder       TkFormBuilder
       (do.)         (do., _canonicalize)

サンプルは,GUI入力フォームをHTML コードまたは Python tkinter により作ります。そのコードは同じ手続きで作ります。

class AbstractFormBuilder は,テンプレートであり,抽象クラスです。
class HtmlFormBuilder は,HTML ファイルを出力してそれを走らせると入力フォームが現れる
class TkFormBuilder は,Python モジュールを出力してそれを走らせると入力フォームが現れる

◆◆2種類のGUI入力フォーム◆◆

HTML ファイルで左のGUIを,Python モジュールで右のGUIを出力します。



この2つのGUIは,同じ構成で出来上がっています。そこで,完全に同じ手続きで HTML コードと Python コードを出力し,それらのコードがこの2つのGUIをつくるのが,Builder パターンなのです。 Python で GUI をつくるためにこのサンプルでは tkinter を使っています。

◆◆サンプル出力の login.html

HTMLファイルのソースコードの構造を簡単に説明します。

ソースの中で山カッコ"<"と">"で囲まれたものを HTML タグと言います。

HTMLファイルのソースの骨組みは次のようになっています。

  <!doctype html>
  <html>
    <head>
      <title>Login</title>
    </head>
    <body>
      <form>ここにGUI</form>
    </body>
  </html>

1行目は,バージョン宣言であり,HTML5 を宣言しています。

インデントが0と1のタグは必ずこのように書きます。インデントが2 以降のところに固有の内容を書きます。タグのネスト(入れ子)を整理して書くとこのようになりますが,インデントや改行は義務ではありません。サンプルを見るときは注意してください。

<body>の中身がブラウザで見ることができる文章や画像になります。このサンプルでは,GUIが書かれています。

巻末のHTML コードを見ますと,<form>の中身は<table>になっています。<tr>が行であり,<td>が列なので,3行2列の GUI ができます。

タグとタグの間の文字だけがブラウザで読み取れることに注意してください。「Username:」と「Password:」だけです。これらは label として修飾されています。

あとは,文字入力と押しボタンがありますが,それらのタグの説明は省略します。HTMLコードの出力は天下り的と考えてもらってもかまいません。

◆◆サンプル出力の login.py

1つのクラスとメインルーチンしかありません。

メインルーチンは,tkinter の典型的な書き方になっています。要はクラスをインスタンス化して走らせているだけです。私は,バグっていると感じているのですが,具体的には指摘できません。

クラスはコンストラクタしかありません。ライブラリ tkinter.ttk を使っているのは,
  Label
  Entry
  Button
の3種類のパーツ(ウィジェット)だけです。これらを row と column で位置を決めて貼り付けているだけです。あとのパラメータは余白だけです。

入力フォームに入力された後,どのように使うかが不明なので,このコードを詳細に解釈してもしようがないと思います。Python コードの出力は天下り的と考えてもらってもかまいません。

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

◆◆Builder パターンの構成◆◆

メインルーチンと3つのクラスから構成されています。

クラスは2つが実装クラスであり,それらのスーパークラスが抽象クラスである。

つまり,Template Method パターンと同じ構成であるが,違いは,各メソッドがパーツ化されていて,そのパーツの組み合わせはメインルーチンが決めるということである。

◆◆抽象クラスの詳細

抽象クラスとは抽象メソッドがあるクラスのことです。

とは言うものの,Python には,言語仕様としての抽象クラスと抽象メソッドはありません。abcモジュールにAbstract Base Class (抽象基底クラス)が追加されて,それらをデコレータで呼ぶと抽象クラスと同じ働きをするのです。

abcモジュールをインポートして,AbstractFormBuilder の中でメタクラス ABCMeta を宣言します。ちなみに,abc は Abstract Base Class (抽象基底クラス)のことです。

抽象メソッドの宣言はデコレータ @abstractmethod によって行います。この抽象メソッドは pass により空にします。実装されているものは,サブクラスから呼ばれることを想定している。

このクラスのサブクラスを具象クラスとして,具象メソッドを実装します。この宣言の働きは,具象メソッドが実装されていないとエラーを出すことにある。

ちなみに,インポート宣言を
  import abc
ではなく
  from abc import ABCMeta, abstractmethod
とすれば,
  metaclass=abc.ABCMeta, @abc.abstractmethod

  metaclass=ABCMeta, @abstractmethod
と書けます。

各メソッドの働きは,パーツ(ウィジェット)をつくることを抽象化してインターフェイスとなることである。先に出力されるコードを紹介したので,これらのメソッドが実装されたときの働きは概略判るでしょう。

実際のメソッドは次の5つである。

  def add_title
  def form
  def add_label
  def add_entry
  def add_button

◆◆class HtmlFormBuilder が login.html を出力する

このクラスで前項で紹介した抽象メソッドを具象メソッドにします。出力されるコードは既知なので,具象メソッドの働きは想像がつくと思います。

「def form」により,HTML ファイルの骨格をすべてつくってしまいます。テーブルの行<tr>までつくってしまいます。テーブルの列<td>(つまり1区画)は,他のパーツ(ウィジェット)をつくるメソッド「def add_label」「def add_entry」「def add_button」により出力されます。

「def add_title」だけはスーパークラスを呼んでいます。タイトルが出力コードのどこに入るのかは,実際の出力コードで確認してください。

◆◆class TkFormBuilder が login.py を出力する

パーツ(ウィジェット)を作るコード以外は,決まりきったコードを出力するだけです。126行目から150行目のトリプルクォートで囲まれた変数 TEMPLATE の中身です。

改行はエスケープシーケンスを使わなくても見ためのまま出力されます。インデントは半角スペースで埋まっているのでそのまま出力されます。

この変数は,「def form」で使われます。

パーツ(ウィジェット)を作るコードは,メソッド「def add_label」「def add_entry」「def add_button」により出力されます。

「def add_title」だけはスーパークラスを呼んでいます。タイトルが出力コードのどこに入るのかは,実際の出力コードで確認してください。

「def _canonicalize」は,パーツ(ウィジェット)の名前を変えています。
  self.loginButton = ttk.Button(self, text="Login")
の例では,「login」「Login」を使い分けています。あと,先頭が数字のときはアンダースコアを付けるようです。

◆◆メインルーチン

コマンドラインの第1引数にこのモジュールが指定されると,必ず,201行目の決まり文句が起動されます。次の行でグローバル関数を呼んでいるので,インデントが0の関数が呼ばれます。

最初の関数では,ファイル出力がされます。ディレクトリが深いので注意してください。

2番目の関数では,パーツ(ウィジェット)を作るコードを出力します。パーツ(ウィジェット)の名前と位置を指定します。

メインルーチンでは,出力される HTML のコードや tkinter のコードを意識させることはありません。抽象クラスも独自のコードを意識させることはありませんが,インターフェイスであることは意識させています。

◆◆ソースコード◆◆

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

ソースファイルは1つです。サンプル出力も添付しました。
・formbuilder.py;Builder パターンのPythonサンプル
・login.html;サンプル出力の html ファイル
・login.py;サンプル出力の Python モジュール

【formbuilder.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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/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 os
import re
import sys
import tempfile
if sys.version_info[:2] < (3, 2):
    from xml.sax.saxutils import escape
else:
    from html import escape


def main():
    if len(sys.argv) > 1 and sys.argv[1] == "-P": # For regression testing
        print(create_login_form(HtmlFormBuilder()))
        print(create_login_form(TkFormBuilder()))
        return

    htmlFilename = os.path.join(tempfile.gettempdir(), "login.html")
    htmlForm = create_login_form(HtmlFormBuilder())
    with open(htmlFilename, "w", encoding="utf-8") as file:
        file.write(htmlForm)
    print("wrote", htmlFilename)

    tkFilename = os.path.join(tempfile.gettempdir(), "login.py")
    tkForm = create_login_form(TkFormBuilder())
    with open(tkFilename, "w", encoding="utf-8") as file:
        file.write(tkForm)
    print("wrote", tkFilename)


def create_login_form(builder):
    builder.add_title("Login")
    builder.add_label("Username", 0, 0, target="username")
    builder.add_entry("username", 0, 1)
    builder.add_label("Password", 1, 0, target="password")
    builder.add_entry("password", 1, 1, kind="password")
    builder.add_button("Login", 2, 0)
    builder.add_button("Cancel", 2, 1)
    return builder.form()


class AbstractFormBuilder(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def add_title(self, title):
        self.title = title


    @abc.abstractmethod
    def form(self):
        pass


    @abc.abstractmethod
    def add_label(self, text, row, column, **kwargs):
        pass


    @abc.abstractmethod
    def add_entry(self, variable, row, column, **kwargs):
        pass


    @abc.abstractmethod
    def add_button(self, text, row, column, **kwargs):
        pass


class HtmlFormBuilder(AbstractFormBuilder):

    def __init__(self):
        self.title = "HtmlFormBuilder"
        self.items = {}


    def add_title(self, title):
        super().add_title(escape(title))


    def add_label(self, text, row, column, **kwargs):
        self.items[(row, column)] = ('<td><label for="{}">{}:</label></td>'
                .format(kwargs["target"], escape(text)))


    def add_entry(self, variable, row, column, **kwargs):
        html = """<td><input name="{}" type="{}" /></td>""".format(
                variable, kwargs.get("kind", "text"))
        self.items[(row, column)] = html


    def add_button(self, text, row, column, **kwargs):
        html = """<td><input type="submit" value="{}" /></td>""".format(
                escape(text))
        self.items[(row, column)] = html


    def form(self):
        html = ["<!doctype html>\n<html><head><title>{}</title></head>"
                "<body>".format(self.title), '<form><table border="0">']
        thisRow = None
        for key, value in sorted(self.items.items()):
            row, column = key
            if thisRow is None:
                html.append("  <tr>")
            elif thisRow != row:
                html.append("  </tr>\n  <tr>")
            thisRow = row
            html.append("    " + value)
        html.append("  </tr>\n</table></form></body></html>")
        return "\n".join(html)


class TkFormBuilder(AbstractFormBuilder):

    TEMPLATE = """#!/usr/bin/env python3
import tkinter as tk
import tkinter.ttk as ttk

class {name}Form(tk.Toplevel):

    def __init__(self, master):
        super().__init__(master)
        self.withdraw()     # hide until ready to show
        self.title("{title}")
        {statements}
        self.bind("<Escape>", lambda *args: self.destroy())
        self.deiconify()    # show when widgets are created and laid out
        if self.winfo_viewable():
            self.transient(master)
        self.wait_visibility()
        self.grab_set()
        self.wait_window(self)

if __name__ == "__main__":
    application = tk.Tk()
    window = {name}Form(application)
    application.protocol("WM_DELETE_WINDOW", application.quit)
    application.mainloop()
"""

    def __init__(self):
        self.title = "TkFormBuilder"
        self.statements = []


    def add_title(self, title):
        super().add_title(title)


    def add_label(self, text, row, column, **kwargs):
        name = self._canonicalize(text)
        create = """self.{}Label = ttk.Label(self, text="{}:")""".format(
                name, text)
        layout = """self.{}Label.grid(row={}, column={}, sticky=tk.W, \
padx="0.75m", pady="0.75m")""".format(name, row, column)
        self.statements.extend((create, layout))


    def add_entry(self, variable, row, column, **kwargs):
        name = self._canonicalize(variable)
        extra = "" if kwargs.get("kind") != "password" else ', show="*"'
        create = "self.{}Entry = ttk.Entry(self{})".format(name, extra)
        layout = """self.{}Entry.grid(row={}, column={}, sticky=(\
tk.W, tk.E), padx="0.75m", pady="0.75m")""".format(name, row, column)
        self.statements.extend((create, layout))


    def add_button(self, text, row, column, **kwargs):
        name = self._canonicalize(text)
        create = ("""self.{}Button = ttk.Button(self, text="{}")"""
                .format(name, text))
        layout = """self.{}Button.grid(row={}, column={}, padx="0.75m", \
pady="0.75m")""".format(name, row, column)
        self.statements.extend((create, layout))


    def form(self):
        return TkFormBuilder.TEMPLATE.format(title=self.title,
                name=self._canonicalize(self.title, False),
                statements="\n        ".join(self.statements))


    def _canonicalize(self, text, startLower=True):
        text = re.sub(r"\W+", "", text)
        if text[0].isdigit():
            return "_" + text
        return text if not startLower else text[0].lower() + text[1:]


if __name__ == "__main__":
    main()

【login.html】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!doctype html>
<html><head><title>Login</title></head><body>
<form><table border="0">
  <tr>
    <td><label for="username">Username:</label></td>
    <td><input name="username" type="text" /></td>
  </tr>
  <tr>
    <td><label for="password">Password:</label></td>
    <td><input name="password" type="password" /></td>
  </tr>
  <tr>
    <td><input type="submit" value="Login" /></td>
    <td><input type="submit" value="Cancel" /></td>
  </tr>
</table></form></body></html>

【login.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
#!/usr/bin/env python3
import tkinter as tk
import tkinter.ttk as ttk

class LoginForm(tk.Toplevel):

    def __init__(self, master):
        super().__init__(master)
        self.withdraw()     # hide until ready to show
        self.title("Login")
        self.usernameLabel = ttk.Label(self, text="Username:")
        self.usernameLabel.grid(row=0, column=0, sticky=tk.W, padx="0.75m", pady="0.75m")
        self.usernameEntry = ttk.Entry(self)
        self.usernameEntry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx="0.75m", pady="0.75m")
        self.passwordLabel = ttk.Label(self, text="Password:")
        self.passwordLabel.grid(row=1, column=0, sticky=tk.W, padx="0.75m", pady="0.75m")
        self.passwordEntry = ttk.Entry(self, show="*")
        self.passwordEntry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx="0.75m", pady="0.75m")
        self.loginButton = ttk.Button(self, text="Login")
        self.loginButton.grid(row=2, column=0, padx="0.75m", pady="0.75m")
        self.cancelButton = ttk.Button(self, text="Cancel")
        self.cancelButton.grid(row=2, column=1, padx="0.75m", pady="0.75m")
        self.bind("<Escape>", lambda *args: self.destroy())
        self.deiconify()    # show when widgets are created and laid out
        if self.winfo_viewable():
            self.transient(master)
        self.wait_visibility()
        self.grab_set()
        self.wait_window(self)

if __name__ == "__main__":
    application = tk.Tk()
    window = LoginForm(application)
    application.protocol("WM_DELETE_WINDOW", application.quit)
    application.mainloop()

以上

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