Update 2023.11.17 2017.05.05

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

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

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

いわゆるコンテナオブジェクトから要素を順番に取り出すのがIterator パターンです。オブジェクト指向の考えにより,コンテナの構造をクライアントから隠蔽することも目的の一つです。

(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つを検証してそれをソースコードにしていきます。


◆◆Iterator パターンとは◆◆

GoFによれば,Iterator パターンの目的は, 「集約オブジェクトが基にある内部表現を公開せずに,その要素に順にアクセスする方法を提供する。」

GoFによれば,Iterator パターンの別名は,Cursor パターンである。

GoFによれば,次のような場合に Iterator パターンを使うとよい。
・コンテナオブジェクトの内部表現を公開せずに,その中にあるオブジェクトにアクセスしたい場合。
・コンテナオオブジェクトに対して,複数の走査をサポートしたい場合。
・異なるコンテナオ構造の走査に対して,単一のインタフェースを提供したい(すなわち,ポリモルフィックな iteration をサポートしたい)場合。

GoFによれば,Iterator パターンの関連するパターンは次のようなものである。
Composite パターン:iterator は,しばしば Composite のような再帰的な構造に対して適用される。
Factory Method パターン:ポリモルフィックな iterator では,Iterator のサブクラスの中から適切なクラスをインスタンス化するために,factory method を使う。
Memento パターン:しばしば Iterator パターンとともに使われる。iterator は,iteration の状態を把握するために memento を使うことができる。この場合,iterator は memento を内部に保持する。

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。

         Bag
(__init__, clear, add, __delitem__, count, __len__, __iter__, __contains__)

サンプルは,いわゆるバッグクラスまたはマルチセットと言われるもので,アイテムの重複を許すコレクションを扱います。
ほぼ同じものが,標準モジュールの collections.Counter クラスにあるそうです。

◆◆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))

Python では,言語仕様として,イテレータが組み込まれているので,複雑なことをしないですむが,イテレータらしいクラスが,同じサイトの別記事に掲載されているので,参考にしてください(C/C++やJavaらしいコード)。
https://yamakatsusan.web.fc2.com/hp_software/pythonpattern11.html
『Iterator パターン 結城 浩「Java言語で学ぶデザインパターン入門」をPython化』

◆◆ソースコード◆◆

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

ソースファイルは3つです。doctestの結果(3つのソースの結果は同じ)も添付します。
・Bag1.py;Iterator パターンのPythonサンプル(その1)
・Bag2.py;Iterator パターンのPythonサンプル(その2)
・Bag3.py;Iterator パターンのPythonサンプル(その3)
・doctest.txt;上の3つのモジュールのdoctestの標準出力(掲載はない)

【bag1.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
#!/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=" ")

【bag2.py】
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

【bag3.py】
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))

以上

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