Update 2023.11.19 2017.05.05

Python デザインパターン サンプルコード Flyweight
結城 浩「Java言語で学ぶデザインパターン入門」をPython化
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」


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

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

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

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

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


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


◆◆Flyweight パターンとは◆◆

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

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

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

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

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
     BigChar               BigCharFactory            BigString
    (print_)        (getInstance, getBigChar)        (print_)


class BigChar は,大きな文字のフォントを読み込んだり出力します。
class BigCharFactory は,オブジェクトを保管・管理します。
class BigString は,指示通りに大きな文字を印刷します。

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

GoF 本には書いていませんが,ほとんど判らないくらいの小さなメモリリークがあっても,24時間終日動作しているプログラムはいつか必然的にクラッシュしてしまいます。Flyweight パターンに限らずメモリリーク対策の最重要課題はメモリの開放です。

どんなにたくさんのオブジェクトを使っていてもメモリを確実に開放していれば,ガーベッジコレクションによりメモリリークは発生しません。メモリを解放するためには独自の言語仕様があります。

比較のためにメモリリーク対策をしてあるものと無対策のものの2つのサンプルのソースコードを巻末に掲載しました。

class BigChar とメインルーチンは完全に同じものです。メモリリーク無対策の方は class BigCharFactory がなく,class BigString の dict 辞書でオブジェクトを保管します。 メモリリーク対策をしてある方は class BigCharFactory の特殊な辞書でオブジェクトを保管します。

2つのサンプルとも,メインルーチンは class BigString を呼んでるだけです。

2つのサンプルとも,class BigString は,指定された大きな文字をインスタンス化してそれを印刷するために class BigChar を呼んでいるだけです。

2つのサンプルの違いは,オブジェクトの管理です。

メモリリーク無対策の方は,メインルーチンの指定通り,片っ端からインスタンス化したオブジェトを闇雲に保管しています。一時保管するのはあとでまとめて印刷するからです。

メモリリーク対策してある方は,同じオブジェクトは初回の一回しかインスタンス化しません。つまり,シングルトンになっています。その管理を class BigCharFactory に任せています。44行目のコメントアウトを外せばオブジェクトをいくつ作っているのかを確認できます。インスタンス化は7回やっていますが保管されているオブジェクトはフォントの種類の3つです。

念が入っていて,このクラスのメソッドを呼ぶときもシングルトンになっています。この2種類のシングルトンは実装の方法がかなり違います。後者は,デザインパターンで紹介されています。前者は保管されているオブジェクトがあるか確認しているだけです。

2つのサンプルとも,オブジェクトの保管はハッシュ辞書です。メモリリーク無対策の方は,普通の dict です。 {} と書くこともあります。メモリリーク対策してある方は,weakref.WeakValueDictionary を使っています。いわゆる弱参照と言われています。

弱参照はほとんどの言語の仕様にあるそうです。いわゆる循環参照になっていてももちろん普通の参照でも確実にメモリを解放してくれるそうです。これを普通のハッシュ dict にしても見かけ上完全に同じ動作をしますがメモリの開放だけは目に見えないところで全く違うということです。ただし,このサンプル程度の規模や複雑さでは違いは判らないかもしれません。

最後に一言。「GoFの23のデザインパターン」の中で,Web にサンプルがほとんど提供されていないのがこの弱参照と tkinter を使ったものです。この2つは,C++ サンプルや Java サンプルと実装がまったく違ってしまうからかもしれません。それだけ独自の言語仕様に依存します。また,シングルトンは python ではメタクラスでもつくることができます。この実装も独自の言語仕様に依存します。オブジェクトの構造が複雑化してくると使わざるをえないかもしれません。

◆◆ソースコード◆◆

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

2つのソースファイル,10のビッグフォントがあります。。
・Flyweight.py;Flyweight パターンのPythonサンプル
・Flyweight_dict.py;Flyweight パターンのメモリリーク無対策

【Flyweight.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import weakref

class BigChar(object):
    # フォント(外部ファイル)を読み込み,それをprintする
    # コンストラクタ
    def __init__(self, charname):
        # 文字の名前(1文字)
        self.charname = charname
        buf = ""
        # ファイル読み込みの典型
        with open("big" + self.charname + ".txt") as reader:
            for text in reader:
                buf= buf + text
        reader.close()
        # 大きな文字を表現する文字列('#' '.' '\n'の列)
        self.fontdata = buf
    # 大きな文字を表示する
    def print_(self):
        print(self.fontdata)

class BigCharFactory(object):
    # すでに作ったBigCharのインスタンスを管理
    # コンストラクタ
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, "__instance__"):
            cls.__instance__ = super(BigCharFactory, cls).\
                __new__(cls, *args, **kwargs)
        return cls.__instance__         # このクラスのシングルトンを戻す(つくる)
    def __init__(self):
        self.pool = weakref.WeakValueDictionary() # インスタンスの保管場所
        #self.pool = dict()             # 普通の辞書でも動作は同じ
    # 唯一のインスタンスを得る
    def getInstance():
        return BigCharFactory().__instance__ # このクラスのシングルトンを戻す
    # BigCharフォント毎のインスタンス生成(共有)(ダブりなし)
    def getBigChar(self, charname):
        obj = self.pool.get(charname)   # 既存のオブジェクトを確認
        if not obj:                     # 既存がないときは
            obj = BigChar(charname)     # 新たにインスタンス化する
            self.pool[charname] = obj   # 保管する
        #print(len(self.pool))           # debug フォント毎のインスタンスの数
        return obj                      # フォント毎のシングルトンを戻す

class BigString(object):
    # メインルーチンの指示通りに処理する
    def __init__(self, str):
        self.bigchars = dict()          # printのためのオブジェクトの一時保管場所
        factory = BigCharFactory.getInstance() # シングルトンを作るのではなくもらう
        for i in range(len(str)):       # printのためのオブジェクトを一時保管
            obj = factory.getBigChar(str[i])
            self.bigchars[i] = obj
    def print_(self):
        for i in self.bigchars.keys():  # まとめてprintする
            self.bigchars[i].print_()

def main():
    if len(sys.argv) == 1:              # コマンドライン引数がないときは注意喚起
        print("Usage: python Flyweight.py digits")
        print("Example: python Flyweight.py 1212123")
        sys.exit()
    bs = BigString(sys.argv[1])
    bs.print_()

if __name__ == '__main__':
    main()
"""
標準出力
(省略)
外部ファイルのフォントをそのままコマンドライン引数の指定とおりにprintします

【Flyweight_dict.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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

class BigChar(object):
    # コンストラクタ
    def __init__(self, charname):
        # 文字の名前
        self.charname = charname
        buf = ""
        # ファイル読み込みの典型
        with open("big" + self.charname + ".txt") as reader:
            for text in reader:
                buf= buf + text
        reader.close()
        # 大きな文字を表現する文字列('#' '.' '\n'の列)
        self.fontdata = buf
    # 大きな文字を表示する
    def print_(self):
        print(self.fontdata)

class BigCharFactory(object):
    pass

class BigString(object):

    def __init__(self, str):
        self.bigchars = dict()
        for i in range(len(str)):
            obj = BigChar(str[i])
            self.bigchars[i] = obj

    def print_(self):
        for i in self.bigchars.keys():
            self.bigchars[i].print_()

def main():
    if len(sys.argv) == 1:
        print("Usage: python Flyweight.py digits")
        print("Example: python Flyweight.py 1212123")
        sys.exit()
    bs = BigString(sys.argv[1])
    bs.print_()

if __name__ == '__main__':
    main()
標準出力
(省略)
外部ファイルのフォントをそのままコマンドライン引数の指定とおりにprintします

以上

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