Python デザインパターン サンプルコード Command
Mark Summerfield『実践 Python 3』デザインパターンのサンプルコード
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」
著作権の問題があるので,本に書いてないことだけを解説します。つまり,視点を変えて解説します。
◆◆Command パターンの使われる場面◆◆
要は,いわゆる undo(取り消し)や redo(再実行)をする方法が Command パターンである。ポイントは,undo(取り消し)や redo(再実行)の対象をオブジェクトにすることである。
個人が独力で論理的にいくら考えても思いつかないコードです。詳しく説明していますので少々長いです。
(2023-11-17)Python3.11で動作確認済み
◆◆Command パターンとは◆◆
GoFによれば,Command パターンの目的は, 「要求をオブジェクトとしてカプセル化することによって,異なる要求や,要求からなるキューやログにより,クライアントをパラメータ化する。また,取り消し可能なオペレーションをサポートする。」Grid (cell, rows, columns, as_html) ↑ UndoableGrid (create_cell_command, create_rectangle_macro)class Grid は,htmlのテーブルを書くためのクラスです
Command Macro (__call__) (add, __call__, undo)
◆◆Command パターンの実装◆◆
サンプルは,html で作ったテーブルのセルに,文字を書き込んだり,色を付けたりします。
◆◆htmlファイルとhtmlコード◆◆
ずっと下の色の付いた図は,サンプル出力のhtmlコードをブラウザで見たものです。表示のために2段に分けましたが出力は1段です。途中経過が判るようになっていて,undoの過程も判ります。テーブルのタイトルはメインルーチンで書いたものです。
あなたは今,サンプル出力をブラウザで見ていて,図が見ることができていますが,サンプル出力はhtmlコードであり,ただの文字列です。
あなたのパソコンでサンプルコードをrunさせて得た出力のhtmlコードをブラウザで見るためにはhtmlファイルを作る必要があります。
まず,新しくテキストファイル(拡張子.txt)を作ります。開いてから最初に文字コードをUTF-8Nにします。これはメモ帳(NotePad)では難しいかもしれません。私はTeraPadやAtomを使っています。拡張子を.htmlに書き換えてください。これを最初にやってもよいです。
htmlファイルをテキストエディタで開いて,次のhtmlタグを書き込んでください。エディタによっては,htmlファイルを自動で作ってくれるのもあります。出力を貼り付けるのは手作業です。
ブラウザで文字が読み取れるのは,bodyエリアに書かれたコードのうちhtmlタグ(<>)の外側に書かれた文字です。htmlタグの内側に書かれたものはプログラムです。
さて,htmlファイルの原則どおりの作り方は上に書いたとおりですが,サンプルコードの出力に限っては,それをテキストファイルに貼り付けて,拡張子を.htmlに書き直して,ダブルクリックで(ブラウザを)開いてやれば下の図が表示されます。
理由は,英数字だけのファイルはshit_jisとutf-8は同じものであるからと,utf-8NはBOMなしを意味するのだがブラウザがBOMの有無を無視しているからだと思われる。
いずれにしてもhtmlは原則どおりに作られていないとWebサーバーにはuploadできないので正しく作ることをお薦めします。
◆◆htmlのテーブル◆◆
htmlのテーブルの次のように単純な構造をしています。タグのうちでスラッシュの付いたものは「閉じ」です。テーブルは,「テーブル全体」「行」「列(セル)」を示す3種類のタグのペアで構成されている。
これは,3行3列のテーブルであり,1行目はタイトル行になる。操作できるセルは2行3列となる。
<table border="1"> <tr> <td colspan="3">Title</td> </tr> <tr> <td>あ</td> <td>い</td> <td>う</td> </tr> <tr> <td>え</td> <td>お</td> <td>か</td> </tr> </table>
上のhtmlコードをドラッグしてhtmlファイルに貼り付ければ下の図と似た図がブラウザで見ることができる。
混乱してほしくないのは,上のhtmlコードはあなたがブラウザで見ているからそのように見えるだけで,このhtmlファイル(ソースファイル)のhtmlコードは上のようにはなっていないことに注意してください。
ただし,ドラッグしてコピペすれば,ブラウザで見たままの文字列が得られます。そのままhtmlファイルにできるのです。
◆◆テーブルのセルの操作はハードコード◆◆
さて,次図を出力する一連の作業は,本来手作業であるべきなのですが,プログラムを簡単にするためメインルーチンに書かれたハードコードになっています。どういう作業であるかは,ハードコードとテーブルのタイトルを見てください。
実際の応用には工夫が必要になりますが,ハードコードであっても,Command パターンの本質には変わりないと思います。
|
|
|
|
|
|
|
|
15行目からのメインルーチンで,17行目でインスタンス化した後,一連のセルの操作が19行目から39行目まで続きます。
インスタンスgridが呼ばれているものは,セルの操作の内容であり,コマンドとマクロがあります。
()が付いているのは,少し前の行のセルの操作の内容を実行するコマンドであり,.do()と.undo()があります。どちらも付いていないものは,.do()と同等です。
◆◆undoの内容◆◆
undoの最もポピュラーなものは「Ctrl+z」でしょう。操作の順序を覚えていて,順序良く遡って取り消していくものです。
サンプルは,操作の場所を覚えていて,場所を指示することにより取り消していくものです。応用に際しては場所の指示の方法は工夫が必要でしょう。
◆◆セルの操作の内容◆◆
セルの操作の内容はクラスUndoableGridのインスタンスgridを付けたコマンドとマクロの2種類あります。例えば,次の左辺はクラスCommandとMacroのインスタンスです。
redLeft = grid.create_cell_command(2, 1, "red")
rectangleLeft = grid.create_rectangle_macro(1, 1, 2, 2, "lightblue")
右辺の引数はセルの位置と色です。セルの位置は,x y 座標であり,0から始まります。マトリックスは日米を問わず行が先,列が後ですがそれとは逆です。
マクロは複数セルを左上位置と右下位置により指定します。
前者の右辺のメソッドの戻りが次である。
Command.Command(do, undo, "Cell")
ここでは,引数の定義とクラスCommandのインスタンスを作って,メソッド__call__が使えるようになっただけだある。
後者の右辺のメソッドの戻りが次である。
Command.Macro("Rectangle")
ここでは,引数の定義とクラスMacroのインスタンスを作って,メソッド__call__が使えるようになっただけだある。
◆◆クロージャdoとundo◆◆
メソッドgrid.create_cell_command内にある関数undoとdoはクロージャと呼ばれ,上位関数の引数や変数を覚えている。
例えば,上位関数が
redRight = grid.create_cell_command(5, 0, "red")
で呼ばれたとき上位関数の引数を覚えているので,
redRight.do()
の関数do()は覚えている座標,色を参照できる。
注意してほしいのは,一般的なクロージャの使い方と違うことです。上のように関数を呼ぶと左辺は普通関数オブジェクトです。この関数オブジェクトを引数を付けて呼ぶのが一般的です。このときクロージャを明示的に呼ぶことはありません。
サンプルの場合は,左辺はインスタンスオブジェクトです。上のメソッドは戻り値でクラスを呼び,インスタンス化のコンストラクタの引数でクロージャじしんを定義しています。このとき上の式のメソッドの引数はどこにも記憶されていません。直下のクロージャが覚えているのです。
クロージャを付けないでインスタンスだけで呼んだときは特殊メソッド__call__が呼ばれ,ここでクロージャdoを参照しています。
いずれにしても,セルの操作の内容がインスタンスに残っていることがdoやundoを実現することできるのです。ただし,操作の内容ごとにインスタンスが生成されていることに注意が必要です。
◆◆特殊メソッド__call__◆◆
特殊メソッド__call__は,__init__,__str__または__repr__の次によく使われる特殊メソッドらしい。デザインパターンでは必須であるみたいです。
インスタンスに()を付けて関数のように呼ぶと,特殊メソッド__call__が呼ばれる。ここに書かれたコードは一見関数のように動作する。
例えば,メインルーチンには,セルの操作の内容を定義したあと,
redLeft()
redRight.do()
前者は,特殊メソッド__call__に飛び,さらにコンストラクタで定義された関数doに飛ぶ。後者は,直接関数doに飛ぶ。このdoやundoはクロージャなのでセルの座標と色を覚えている。これらの動作はセルの色付け,文字埋めである。
クラスCommandのメソッド__call__のself.do()はインスタンス化のときに定義されていて,メソッドcreate_cell_commandの戻り値でインスタンス化されたクラスCommandのコンストラクタ引数のメソッドdoである。これはメソッドcreate_cell_command内の関数であり,引数である座標,色を参照できる。
クラスMacroのメソッド__call__はコマンド列を順番に実行している。つまり,メソッドcreate_cell_commandを必要な数だけ呼んでいる。
メソッドdoのメソッドcellはモジュールgrid.pyの52行目のメソッドcellであり,セル位置を指定して色付けをしている。
メソッドcellは引数に color が無いと getter になり引数に color が有ると setter になる。
◆◆undo◆◆
redLeft()
は,クラスCommandのメソッド__call__に飛んだあと,メソッドcreate_cell_commandの関数doに飛ぶ。
redRight.do()
redRight.undo()
は,メソッドcreate_cell_commandの関数doとundoに直接飛ぶ。
一方,
rectangleLeft()
は,クラスMacroのメソッド__call__に飛んだあと,さらに必要な数だけメソッド create_cell_commandの関数doに飛ぶ。
rectangleRight.do()
は,do = __call__ と定義されているので,上と同じである。
rectangleLeft.undo()
は,クラスMacroのメソッドundoに飛んだあと,さらに必要な数だけメソッド create_cell_commandの関数undoに飛ぶ。
◆◆htmlテーブルを作る◆◆
出力のテーブルのhtmlコードを作っているのが,メソッドas_htmlである。この中で,行と列の長さを戻しているのが,メソッドrowsとcolumnsである。デコレータ@propertyが付いているのでgetter である。
getterはメソッドとして呼ばれているのだが,()が付いていなくて引数がなく数学的な関数ではないので,その戻り値が変数のように扱われていると思ってもらって差し支えない。
クラスGridの中のただ1つの属性__cellsはマングリングされていて外から呼ばれることはありません。
__cellsは2重リストですが,マトリックスと思ってもらって差支えありません。テーブルのある位置のセルの色をメモリます。
テーブルのhtmlコードを作っていくコードの詳細は省きます。上に書いたテーブルの構造とhtmlタグの記法を併せてコードを読めば難しくありません。
◆◆ソースコード◆◆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
#!/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 Command def main(): html = [] grid = UndoableGrid(8, 3) # (1) Empty html.append(grid.as_html("(1) Empty")) redLeft = grid.create_cell_command(2, 1, "red") redRight = grid.create_cell_command(5, 0, "red") redLeft() # (2) Do Red Cells redRight.do() # OR: redRight() html.append(grid.as_html("(2) Do Red Cells")) greenLeft = grid.create_cell_command(2, 1, "lightgreen") greenLeft() # (3) Do Green Cell html.append(grid.as_html("(3) Do Green Cell")) rectangleLeft = grid.create_rectangle_macro(1, 1, 2, 2, "lightblue") rectangleRight = grid.create_rectangle_macro(5, 0, 6, 1, "lightblue") rectangleLeft() # (4) Do Blue Squares rectangleRight.do() # OR: rectangleRight() html.append(grid.as_html("(4) Do Blue Squares")) rectangleLeft.undo() # (5) Undo Left Blue Square html.append(grid.as_html("(5) Undo Left Blue Square")) greenLeft.undo() # (6) Undo Left Green Cell html.append(grid.as_html("(6) Undo Left Green Cell")) rectangleRight.undo() # (7) Undo Right Blue Square html.append(grid.as_html("(7) Undo Right Blue Square")) redLeft.undo() # (8) Undo Red Cells redRight.undo() html.append(grid.as_html("(8) Undo Red Cells")) print('<table border="0"><tr><td>{}</td></tr></table>'.format( "</td><td>".join(html))) class Grid: def __init__(self, width, height): self.__cells = [["white" for _ in range(height)] for _ in range(width)] def cell(self, x, y, color=None): if color is None: return self.__cells[x][y] self.__cells[x][y] = color @property def rows(self): return len(self.__cells[0]) @property def columns(self): return len(self.__cells) def as_html(self, description=None): table = ['<table border="1" style="font-family: fixed">'] if description is not None: table.append('<tr><td colspan="{}">{}</td></tr>'.format( self.columns, description)) for y in range(self.rows): table.append("<tr>") for x in range(self.columns): color = self.__cells[x][y] name = color if not color.startswith("light") else color[5:] char = (name[0].upper() if color != "white" else '<font color="white">X</font>') table.append('<td style="background-color: {}">{}</td>' .format(color if color != "red" else "pink", char)) table.append("</tr>") table.append("</table>") return "\n".join(table) class UndoableGrid(Grid): def create_cell_command(self, x, y, color): def undo(): self.cell(x, y, undo.color) def do(): undo.color = self.cell(x, y) # Subtle! self.cell(x, y, color) return Command.Command(do, undo, "Cell") def create_rectangle_macro(self, x0, y0, x1, y1, color): macro = Command.Macro("Rectangle") for x in range(x0, x1 + 1): for y in range(y0, y1 + 1): macro.add(self.create_cell_command(x, y, color)) return macro if __name__ == "__main__": main()
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
#!/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 sys if sys.version_info[:2] < (3, 3): import collections def callable(function): return isinstance(function, collections.Callable) class Command: def __init__(self, do, undo, description=""): assert callable(do) and callable(undo) self.do = do self.undo = undo self.description = description def __call__(self): self.do() class Macro: def __init__(self, description=""): self.description = description self.__commands = [] def add(self, command): if not isinstance(command, Command): raise TypeError("Expected object of type Command, got {}". format(type(command).__name__)) self.__commands.append(command) def __call__(self): for command in self.__commands: command() do = __call__ def undo(self): for command in reversed(self.__commands): command.undo()