Update 2023.11.17 2017.05.05

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

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

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

以下は,同じサイトの別記事について説明されている。
https://yamakatsusan.web.fc2.com/hp_software/pythonpattern21.html
『Flyweight パターン 結城 浩「Java言語で学ぶデザインパターン入門」をPython化』

Flyweight とはボクシングの最軽量級のことです。この名称のせいかもしれませんが多くの人がこの Flyweight パターンを必要と思わないらしいです。

しかし,Flyweight パターンの目的はオブジェクトを軽くすること,つまり,メモリリーク対策なのです。実は「GoFの23のデザインパターン」の中で最重要なのです。Webのサンプルを動作させているぶんには必要ないかもしれませんが,販売するためのソフトウェアには必ず必要なものです。メモリリーク対策をしてなければ欠陥品と言われてもしかたありません。

C/C++ ではガーベッジコレクションを意識的にやらないと時間がたてばそのうちにクラッシュします。Java ではガーベッジコレクションは自動で働くので意識はしなくてもよいかもしれません。Python は C/C++ と Java の中間です。

Flyweight パターンがお薦めしているのは,積極的に del や gc を使えということではありません。インスタンス化されるオブジェクトを最小限にすることとガーベッジコレクションの対象になりやすいオブジェクトを使えということです。

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

コードを追加「time.clock = time.time」

旧いモジュールがなくなったので新しい互換モジュールを変換して,旧いプログラムをそのまま使えるようにした。


◆◆Flyweight パターンとは◆◆

GoFによれば,Flyweight パターンの目的は, 「多数の細かいオブジェクトを効率よくサポートするために共有を利用する。」

GoFによれば,Flyweight パターンの有効性は,それがどこで,どのように利用されるかに大きく依存する。以下のすべてがあてはまるときに Flyweight パターンを適用するとよい。

・アプリケーションが非常に多くのオブジェクトを利用する。
・大量のオブジェクトのために,メモリ消費コストが高くつく。
・オブジェクトの状態を構成するほとんどの情報を extrinsic にできる。
・extrinsic 状態が取り除かれれば,オブジェクトのグループの多くを比較的少数の共有オブジェクトに置き換えることができる。
・アプリケーションがオブジェクトの同一性に依存しない。flyweight オブジェクトは共有されている可能性があるため,概念的には異なるオブジェクトなのだが,同一性テストの結果は真になってしまうことがあるだろう。

GoFによれば,Flyweight パターンの関連するパターンは次のようなものである。
Composite パターン:Flyweight パターンは,しばしば Composite パターンと組み合わされて,共有 Leaf ノードを持つ有向非循環グラフとして論理的な階層構造を実装するために使われる。
State パターン,Strategy パターン:これらのパターンを flyweight として実装するのは,しばしば最良の実装となる。

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
【pointstore1.py】
            Point
    (__slots__, __repr__)
【pointstore2.py】
            Point
    (__slots__, __key, __getattr__, __setattr__, __repr__)

class Point は,大量のオブジェクトを生成するクラスです。

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

【pointstore1.py】

Pythonでは,インスタンス属性は,dict に保存されます。そして,dict にアクセスすれば,インスタンス属性は自由に読み書きができます(できてもしない方が良いです)。

このインスタンス属性の dict を生成しないようにするのが,特殊メソッド
  __slots__
です。その使い方は,37行目に書かれています。コンストラクタの引数と対応していることに注意してください。

このインスタンス属性の dict を生成しないだけでも,速くなるし,メモリ消費量も少なくなるそうです。サンプルは,オブジェクトを100万個つくっています。このくらいだと差が出るということです。

46行目の特殊メソッド
  __repr__
は,オブジェクトを print しようとしたときに呼ばれます。これを定義しないとアドレスが print されるだけです。

その書式は,コンストラクタで定義されたインスタンス変数4つを普通の整数で書き出すだけです。

メインルーチンは,インスタンスを100万個生成してリストに保存するだけです。1つだけインスタンス属性を設定しようとしています。また,コマンドラインの第2引数を「-P」とすれば debug モードになります。

このサンプルじしんは応用できるものではありませんが,特殊メソッド
  __slots__
は,使えます。

【pointstore2.py】

このサンプルでは,インスタンス属性を外部ファイルのデータペースに保存しています。このデータベースは,永続的に残ります。

データベースは,43行目の
  shelve.open
でつくり,Tempフォルダにできます。

原著では,100万個のオブジェクトを生成するのに約1分ということであるが,筆者が確認したところ,100個のオブジェクトの生成でそのくらいかかった。23行目のオブジェクトの size を書き直してから debug することをお薦めする。

そのデータベースを見てみると,本来は,テキストではなく,読めないはずだが?,

'1F5A886E150:x', (0, 5)
'1F5A886E150:y', (512, 5)
'1F5A886E150:z', (1024, 5)
'1F5A886E150:color', (1536, 4)

最初はインスタンスの id (16進数)とインスタンス変数が「:」を挟んで文字列として書かれています。次は,たぶん,値ですが,タプルになっている意味が判りません。ファイルは,もう1つあり,こちらの方はテキストではなく,もちろん内容も読めません。

52行目の
  def __key
は,インスタンス属性のキーを定義しています。

特殊メソッド
  __getattr__
  __setattr__
は,インスタンス属性の getter, setter であり,データベースのインスタンス属性を読み取ろうとしたとき,設定しようしたときに呼ばれます。

特殊メソッド
  __repr__
は,オブジェクトを print しようとしたときに呼ばれます。これを定義しないとアドレスが print されるだけです。

最後の
  atexit.register(__dbm.close)
は,特殊なデータベースをクローズするときの特別なコマンドです。

このサンプルでは,100万個のオブジェクトであるインスタンス属性の使い道がピンと来ませんでした。

◆◆ソースコード◆◆

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

ソースファイルは3つです。
・pointstore1.py;Flyweight パターンのPythonサンプル(その1)
・pointstore2.py;Flyweight パターンのPythonサンプル(その2)
・Qtrac.py;pointstore2.py がインポートする

【pointstore1.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
#!/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
import time


def main():
    regression = False
    size = int(1e6)
    if len(sys.argv) > 1 and sys.argv[1] == "-P":
        regression = True
        size = 20
    start = time.clock()
    points = []
    for i in range(size):
        points.append(Point(i, i ** 2, i // 2))
    end = time.clock() - start
    assert points[size - 1].x == size - 1
    assert points[size - 1].color is None
    print(len(points))
    if not regression: # wait until we can see how much memory is used
        print("took {} secs to create {:,} points".format(end, size))
        input("press Enter to finish")


class Point:

    __slots__ = ("x", "y", "z", "color")

    def __init__(self, x=0, y=0, z=0, color=None):
        self.x = x
        self.y = y
        self.z = z
        self.color = color


    def __repr__(self):
        return "Point({0.x!r}, {0.y!r}, {0.z!r}, {0.color!r})".format(self)


if __name__ == "__main__":
    main()

【pointstore2.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
#!/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 atexit
import os
import shelve
import sys
import tempfile
import time
import Qtrac


def main():
    regression = False
    size = int(1e6)
    if len(sys.argv) > 1 and sys.argv[1] == "-P":
        regression = True
        size = 20
    Qtrac.remove_if_exists(os.path.join(tempfile.gettempdir(), "point.db"))
    start = time.clock()
    points = []
    for i in range(size):
        points.append(Point(i, i ** 2, i // 2))
    end = time.clock() - start
    assert points[size - 1].x == size - 1
    print(len(points))
    if not regression: # wait until we can see how much memory is used
        print("took {} secs to create {:,} points".format(end, size))
        input("press Enter to finish")


class Point:

    __slots__ = ()
    __dbm = shelve.open(os.path.join(tempfile.gettempdir(), "point.db"))

    def __init__(self, x=0, y=0, z=0, color=None):
        self.x = x
        self.y = y
        self.z = z
        self.color = color


    def __key(self, name):
        return  "{:X}:{}".format(id(self), name)


    def __getattr__(self, name):
        return Point.__dbm[self.__key(name)]


    def __setattr__(self, name, value):
        Point.__dbm[self.__key(name)] = value


    def __repr__(self):
        return "Point({0.x!r}, {0.y!r}, {0.z!r}, {0.color!r})".format(self)


    atexit.register(__dbm.close)


if __name__ == "__main__":
    main()

【Qtrac.py】
29
30
31
32
33
34
35
36
37
38
39
40
41
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

以上

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