Python デザインパターン サンプルコード Iterator
Mark Summerfield『実践 Python 3』デザインパターンのサンプルコード
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」
(2023-11-17)Python3.11で動作確認済み
【重要な注意】本ソースコードファイルを起動するには第2引数が必要です。ターミナルからPythonを起動するとき,普通はプロンプト「'>'」の後に次のようにタイプします。
>python Bag1.py -v(小文字)
ターミナルによっては'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」では呼ばれる側を上側に置き,下側に参照されるようにします(本記事に関係ないかも)。
【doctestについて】本プログラムは,Iterator パターンの実例ですが,「doctest」の実例にもなっています。本プログラムでは,「doctest」を実行し,エラーがなければ何も出力しません。
第2引数「-P」を付ければ,「doctest」の内容を出力します。その内容が何をしているか見たい場合は,「doctest」の一部を末尾の2行に書いてありますので,そのコメントアウトを外し,上の2行をコメントアウトして,実行すれば判ります。
「doctest」では,Pythonのプロンプト「>>>」の後にコマンドを書き,その下に出力(解)書きます。その解が正しければプログラムは何も出力しません。この「doctest」はIterator パターンのマニュアルになっているわけです。
解が文字出力の場合,行末の半角スペースが見えませんので,それを出力しているのに解にスペースが付いてないとエラーになるので注意が必要です。
「doctest」のコーディングでは,本プログラムの自己テストの例のように,自己テストに1つ1つコマンドを書き,その出力を「doctest」のプロンプト「>>>」の下にコピペしていきます。つまり,「doctest」の1つ1つを検証してそれをソースコードにしていきます。
Bag (__init__, clear, add, __delitem__, count, __len__, __iter__, __contains__)サンプルは,いわゆるバッグクラスまたはマルチセットと言われるもので,アイテムの重複を許すコレクションを扱います。
◆◆Iterator パターンの実装◆◆
サンプルは,次の3つのモジュールがあります。
bag1.py
bag2.py
bag3.py
違いは,
def __iter__
の実装だけです。
◆◆サンプルコードはドキュメントテストdoctestだけ実施します◆◆
このままではサンプルコードは何もしません。実はdoctestを実施し,合格も出力しています。が,サンプルコードが何をするコードなのか,どのように使うのかはさっぱり判りません。
その答えはdoctestの中にすべて書いてあります。ので,コードを読むしかないのです。
ちなみに,doctestを実施した結果を表示したいときは,コマンドラインのオプションつまりコマンド引数を「-v」とします。結果が標準出力に表示されます。デバッガでは実行の前にパラメータに入力しておきます。
◆◆Pythonを会話で使う◆◆
doctestの書き方はiPythonを会話で使うときと同じです。Pythonが動作するコンソール,例えば,Anaconda Promptの入力待ちプロンプトはインストールに使うときがあります。例えば,
(base) C:\Users\yamak>conda install -c conda-forge lightgbm
このプロンプトに次のようにタイプするとiPythonが起動します。
(base) C:\Users\yamak>python
すると,iPythonの入力待ちのプロンプトが出るので,例えば,
>>>1+2
3
というように会話でPythonをrunさせることができます。
◆◆doctestはトリプルクォーテーション(トリプルクォート「"""」)に書いてある◆◆
トリプルクォートに囲まれたものはdocstringといい,コメント扱いである。外から呼べるのが特徴である(他記事参照)。
docstringの中に,トリプル山カッコ閉じのプロンプトを書いたものがdoctestである。
例えば,Bag1.pyの16行目に,
15 16 17 18 19 20
def __init__(self, items=None): """ >>> bag = Bag(list("ABCDEB")) >>> bag.count("A"), bag.count("B"), bag.count("Z") (1, 2, 0) """
本当の使い方はここに書いてあります。
128 129 130 131 132
def __iter__(self): # This needlessly creates a list of items! """ >>> bag = Bag(list("DABCDEBCCDD")) >>> for key in sorted(bag.items()): print(key, end=" ") A B B C C C D D D D E 以下省略
上のことを本当に動作させて確認するには,自己テストに同じことを書きます。結果の出力は上のとおりになります。
179 180 181 182 183
if __name__ == "__main__": # import doctest # doctest.testmod() bag = Bag(list("DABCDEBCCDD")) for key in sorted(bag.items()): print(key, end=" ")
doctestに書いてあることはすべて自己テストに書けばその動作を確認することができるのです。
doctest の結果は,標準出力へ出力される。それの一部を次に示す。メソッドのabc順に動作していて,次のものは,158行目からの docstring である。
「>>> 」を「Trying:」として実行して,print関数などは「Expecting:」として結果を出力している(スクリプトではprint関数は不要であるが)。
Trying: bag = Bag(list("DABCDEBCCDD")) # __contains__ Expecting nothing ok Trying: "D" in bag Expecting: True ok Trying: del bag["D"] Expecting nothing ok Trying: "D" in bag Expecting: True ok
上のように動作するコードを書くのは実は簡単ではありません。デザインパターンには特殊メソッドが付きものです。個人が独力で論理的にいくら考えても思いつくのが困難であるコードです。これを解決するのがデザインパターンなのです。
◆◆Iteratorは公開されていないコレクション型データにアクセスする◆◆
コレクション型データというのは,例えば,都市名のような固有名詞の集まりであり,重複もあるし,ソートもされていない。
サンプルコードのデータは文字列のように見えるがそうではない。クラスの引数には,文字列で渡されているが,コンストラクタ__init__で1文字ずつにアンパックされて,ハッシュ(辞書)に格納されている。つまり,1つの文字が要素になった集合(set)である。
ハッシュの名称はダブルアンダースコア「__」でマングリングされて非公開属性となっている。
応用でデータをつくるときはサンプルコードを真似ることはできないことに注意してください。
◆◆サンプルコードには5つの特殊メソッドがある◆◆
特殊メソッドは明示的に呼ばれることはまずありません。どのようなときに特殊メソッドが呼ばれるが判ればコードの動作が判ります。
bag = Bag(list("ABCDEB")) # __init__ が呼ばれる del bag["B"] # __delitem__ が呼ばれる print(len(bag)) # __len__ が呼ばれる for key in sorted(bag.items()) # __iter__ が呼ばれる for key in sorted(bag) # __iter__ が呼ばれる print("D" in bag) # __contains__ が呼ばれる
サンプルコードのメソッドのやっていることは,
・データの内容をクリアする
・データ数を数える
・要素ごとの数を数える
・要素を加える
・ある要素が存在するか確認する
・存在が確認された要素を削除する
・データの中からある条件の順番で要素を取り出す(これが主目的)
主目的のデータの要素を取り出すのは,for in 文を使います。具体的なコードはdoctestに書いてあります。特殊メソッド__iter__は,明示的に呼ばれなくても,for in 文で自動的に呼ばれているのである。
他の特殊メソッドも明示的に呼ばれているわけではありません。呼ばれたあとの動作はdoctestを取り除けばすごく簡単であるのが判るでしょう。
◆◆3つのサンプルコードの違いは__iter__の使い方◆◆
イテレータを実現する特殊メソッド__iter__の実装は次の通りである。詳細は省きます。
【bag1.py】 def __iter__(self): for item, count in self.__bag.items(): for _ in range(count): items.append(item) return iter(items) 【bag2.py】 def __iter__(self): for item, count in self.__bag.items(): for _ in range(count): yield item 【bag3.py】 def __iter__(self): return (item for item, count in self.__bag.items() for _ in range(count))
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
#!/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. class Bag: def __init__(self, items=None): """ >>> bag = Bag(list("ABCDEB")) >>> bag.count("A"), bag.count("B"), bag.count("Z") (1, 2, 0) """ self.__bag = {} if items is not None: for item in items: self.add(item) def clear(self): """ >>> bag = Bag(list("ABCDEB")) >>> bag.count("A"), bag.count("B"), bag.count("Z") (1, 2, 0) >>> len(bag) # "Z" was added but count of 0 doesn't count in len 6 >>> bag.clear() >>> len(bag) 0 """ self.__bag.clear() def add(self, item): """ >>> bag = Bag(list("ABCDEB")) >>> bag.add("B") >>> bag.add("X") >>> bag.count("A"), bag.count("B"), bag.count("Z") (1, 3, 0) """ self.__bag[item] = self.__bag.get(item, 0) + 1 def __delitem__(self, item): """ >>> bag = Bag(list("ABCDEB")) >>> print(len(bag)) 6 >>> del bag["B"] >>> del bag["Z"] Traceback (most recent call last): ... KeyError: 'Z' >>> bag.count("A"), bag.count("B"), bag.count("Z") (1, 1, 0) >>> del bag["B"] >>> bag.count("A"), bag.count("B"), bag.count("Z") (1, 0, 0) >>> del bag["C"], bag["D"], bag["E"] >>> print(len(bag)) 1 >>> del bag["A"] >>> print(len(bag)) 0 >>> del bag["A"] Traceback (most recent call last): ... KeyError: 'A' """ if self.__bag.get(item) is not None: self.__bag[item] -= 1 if self.__bag[item] <= 0: del self.__bag[item] else: raise KeyError(str(item)) def count(self, item): """ >>> bag = Bag(list("ABCDEB")) >>> bag.count("B"), bag.count("X") (2, 0) """ return self.__bag.get(item, 0) def __len__(self): """ >>> bag = Bag(list("ABCDEB")) >>> len(bag) 6 >>> bag.add("B") >>> len(bag) 7 >>> bag.add("X") >>> len(bag) 8 >>> bag.add("B") >>> len(bag) 9 >>> del bag["Z"] Traceback (most recent call last): ... KeyError: 'Z' >>> len(bag) 9 >>> del bag["A"] >>> len(bag) 8 >>> for _ in range(4): del bag["B"] >>> len(bag) 4 >>> bag.clear() >>> len(bag) 0 """ return sum(count for count in self.__bag.values()) def __iter__(self): # This needlessly creates a list of items! """ >>> bag = Bag(list("DABCDEBCCDD")) >>> for key in sorted(bag.items()): print(key, end=" ") A B B C C C D D D D E >>> for key in sorted(bag): print(key, end=" ") A B B C C C D D D D E >>> for _ in range(4): del bag["D"] >>> for key in sorted(bag.items()): print(key, end=" ") A B B C C C E >>> for key in sorted(bag): print(key, end=" ") A B B C C C E >>> del bag["A"] >>> for key in sorted(bag.items()): print(key, end=" ") B B C C C E >>> for key in sorted(bag): print(key, end=" ") B B C C C E """ items = [] for item, count in self.__bag.items(): for _ in range(count): items.append(item) return iter(items) items = __iter__ def __contains__(self, item): """ >>> bag = Bag(list("DABCDEBCCDD")) >>> "D" in bag True >>> del bag["D"] >>> "D" in bag True >>> del bag["D"] >>> "D" in bag True >>> del bag["D"] >>> "D" in bag True >>> del bag["D"] >>> "D" in bag False >>> "X" in bag False """ return item in self.__bag if __name__ == "__main__": import doctest doctest.testmod() # bag = Bag(list("DABCDEBCCDD")) # for key in sorted(bag.items()): print(key, end=" ")
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
def __iter__(self): """ >>> bag = Bag(list("DABCDEBCCDD")) >>> for key in sorted(bag.items()): print(key, end=" ") A B B C C C D D D D E >>> for key in sorted(bag): print(key, end=" ") A B B C C C D D D D E >>> for _ in range(4): del bag["D"] >>> for key in sorted(bag.items()): print(key, end=" ") A B B C C C E >>> for key in sorted(bag): print(key, end=" ") A B B C C C E >>> del bag["A"] >>> for key in sorted(bag.items()): print(key, end=" ") B B C C C E >>> for key in sorted(bag): print(key, end=" ") B B C C C E """ for item, count in self.__bag.items(): for _ in range(count): yield item
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
def __iter__(self): """ >>> bag = Bag(list("DABCDEBCCDD")) >>> for key in sorted(bag.items()): print(key, end=" ") A B B C C C D D D D E >>> for key in sorted(bag): print(key, end=" ") A B B C C C D D D D E >>> for _ in range(4): del bag["D"] >>> for key in sorted(bag.items()): print(key, end=" ") A B B C C C E >>> for key in sorted(bag): print(key, end=" ") A B B C C C E >>> del bag["A"] >>> for key in sorted(bag.items()): print(key, end=" ") B B C C C E >>> for key in sorted(bag): print(key, end=" ") B B C C C E """ return (item for item, count in self.__bag.items() for _ in range(count))