pythonによるデザインパターン[Flyweight]

# Intro

この文章は結城 浩さんの「Java言語で学ぶデザインパターン入門」を、pythonで実装してみたサンプルコードです。

筆者の環境は以下の通りです。Python 3.6.3

まだ修行中の身なので間違いがあると思いますがご了承ください。 今回は「Flyweight」です。ボクシングで最も体重が軽いフライ級と同じ意味ですね🥊🥊🥊

# Summarise

本書のp308によると、 Flyweightとは

「インスタンスをできるだけ共有させて、無駄にnewしない」

とあります。インスタンスを共有させることで、メモリの使用量が減らせ、高速化が狙えます。

# Practice

## Specification

大きな文字を「#」を用いて作成したtxtファイルが用意されています。例えば数字の1なら以下が記された、big1.txtが用意されています。

......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................

これと同様のものを、0~9の数字と — (ハイフン)について作成してあります。ファイル名は全て big + (数字もしくは-) + (.txt) となっています。

このファイル群を読み込み、コマンドライン引数で指定された数字を標準出力に表示してください。

例えばpython3 flyweight.py 12 と実行すると

python3 flyweight.py 12    ......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................
....######......
..##......##....
..........##....
......####......
....##..........
..##............
..##########....
................

と表示します。

但し条件として、ファイルの読み込みはたくさんメモリを消費するとして、一度だけにしてください。例えば python3 flyweight.py 121212 とした場合、big1.txt と big2.txt は一度しか読み込んではいけません。

## Explanation

まずは大きな文字、1文字を表すクラス class BigChar を用意します。

ここの処理は簡単で、インスタンスを生成するときに __charname Fieldには、その1文字自身(例えば1なら1を代入)し、 __fontdata Fieldにtxtファイルから読み込んだデータ(string型)を代入します。

また def pprint ではその文字を表示します。

次に、1文字を表すクラス class BigChar の作成・管理をするクラス class BigCharFactory を作成します。

def __init__def get_instance で複雑なことをやっているように見えますが、ここら辺はただのSingletonです。Singletonについては以下で詳しく説明しているので、見てみてください。

重要なのは、dict型の__pool Fieldと def get_big_char です。__pool Field はdict型で、keyを数字自体のstring、valueを大きい文字を表すstringとして格納します。例えば数字の1,2なら以下のイメージです。

__pool = {'1': '''
......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................
''',
'2': '''
....######......
..##......##....
..........##....
......####......
....##..........
..##............
..##########....
................
'''
}

def get_big_char では、この __pool から目的の大きい文字を取り出します。もし __pool の中に目的の大きい文字が存在していたら取り出し、存在していなかったら大きい文字を生成して __pool の中に格納します。必要になったら生成する。すでに存在しているなら取り出す、と言った感じですね。

なぜこんなことをしているのでじょう?dict型の変数の中になんども使うインスタンス(今回はstring)を格納することで、そのインスタンスを再利用することができ、インスタンスを生成するだけのメモリ消費を省けるからです。今回はdict型を使いましたが、再利用するだけなので、場合によってはList型でも構わないと思います。

最後に大きな文字の文字列全体を表すクラス class BigString を作成します。

生成するときに BigCharFactory.get_instance() を呼び出し、class BigCharFactory のインスタンスを取得します。引数に与えられた文字列 string から、1文字ずつ factory.get_big_char(s) で大きな文字を作成していき bigchars Fieldに格納していきます。

def pprint ではbigchars Fieldを一つづつ取り出して、大きな文字1文字ずつ標準出力しています。

また、大きな文字の文字列全体を表すクラス class BigString は、1文字を表すクラス class BigChar を直接操作していない点に注意してください。全ての文字生成の操作は、class BigCharFactory を介して行なっていることに気をつけてください。ここでclass BigCharFactory を用いることで、class BigChar を生成する必要がなくなります(省メモリ)。

最後にmain関数ですが、main関数は文字の文字列全体を表すクラス class BigString のみを操作し、その他のクラスは一切に気にかけません。綺麗な疎結合が実現できていますね。

def main(args):
bs = BigString(args[0])
bs.pprint()

## Sample Code

python3 flyweight.py 12   ......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................
....######......
..##......##....
..........##....
......####......
....##..........
..##............
..##########....
................

# Conclusion

気をつけないといけないのはインスタンスを共有している点です。私はあまり詳しくないのですが、マルチスレッドの時とかに気をつけなといけないのかな…と思います。

あとは共通資源として何を・どこに持たせるかを考えることも重要です。今回は __pool Fieldに文字の情報だけを持たせましたが、文字の色を管理する場合は、 class BigChar , class BigString どちらに情報を持たせればいいでしょうか?それは場合によって変わり、1文字ごとに色を持たせたい場合はclass BigChar に格納しておいた方がよく、文字列自体や文字の順番によって色を持たせたい場合はclass BigString に持たせた方がいいです。このように場合によって、共通資源として何を・どこに持たせるかをしっかりと意識しましょう(p319)。

最後に気をつけないといけないのはgarbage collectionについてです。pythonではC++などと異なり、自動的ににメモリ・ヒープなどの確保・解放を行います。このような使われていないメモリを解放し、メモリの空き領域を増やす機能をgarbase collectionと呼びます。

今回dict型にどんどんインスタンスを格納しますが、長時間動かすプログラムの場合、dict型の中にインスタンスがだんだんたまっていきます。もし使わないインスタンスがあったとしても、garbase collectionの対象にならず、最終的にメモリリークする可能性があります。それを防ぐために使わないインスタンスについては、参照を外す必要があります。と本書(p320)にあったのですが、pythonのGCについて詳しくないので、よくわからないです…

今回はインスタンスを共有することで、リソースの量を減らせるデザインパターンを学びました。すでにどのインスタンスを使うかが決まっている時や、複数個のインスタンスからどれか1つのインスタンスを選択するようなプログラムで、このFlyweightを使っていきたいですね。

# ref

--

--