週末!プログラミング部

ソフトウェア開発ネタを中心に自分でいろいろ調べた内容を自分の勝手な解釈で思うがままに書いくためのブログ。サンプルソースコード、API、プラットフォーム、プログラミング言語、開発環境などを調査、分析して追求いく予定です。

Pythonでデザインパターン 伍ノ型「Singleton」

今回はPythonの勉強しながらデザインパターンのSingletonをやってみました。
他のパターンにつきましては、以下の目次記事をご参照ください。

Singletonとは

直訳すると「単一要素」みたいな意味になります。
wikiには「そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのこと」とあります。
Singletonパターンはwikiにも説明があるとおり、クラスのインスタンスが複数作られないようにするためのメカニズムです。
アプリケーション全体で統一しなければならない仕様を実装しなければならないときに役に立ちます。
私はデザインパターンの中でこのパターンが一番お気に入りで、オブジェクト指向言語でなにか作るときにを頻繁に利用しています。
実際の開発現場でも使っている人をチラホラ見かけたので習得しておいても損はないパターンかもしれないですね(・`ω´・)b

Singletonパターンが欲しくなるとき

それではいつものごとく、いろいろコードを書きながら考えてみます\(^ω^)/
まず以下のようなConfigクラスがあったとします。

class Config(object) :
    def __init__(self) :
        self.ParameterA = 1
        self.ParameterB = 2
        self.ParameterC = 3

このクラスを普通に使おうとするとこんな感じでしょうか?

def moduleA():
    conf = Config()
    pramA = conf.ParameterA

ひとつのモジュールで使うだけならこれでもいいのですが、 複数のモジュールでこのクラスのパラメータを共有しなければならなくなったとします。
そこでモジュール毎にインスタンスを作ってみます。
こうしてしまうとインスタンスがそれぞれ独立しているので、あるインスタンスのパラメータを書き換えても別のインスタンスには反映されません。

def moduleA():
    conf = Config()
    pramA = conf.ParameterA
    conf.ParameterA = 5

def moduleB():
    conf = Config()
    pramA = conf.ParameterA
    conf.ParameterA = 6

今度はConfigをグローバル変数にしてみます。
こうすればmoduleAとmoduleBでインスタンスを共有することができます。

conf = Config()

def moduleA():
    pramA = conf.ParameterA
    conf.ParameterA = 5

def moduleB():
    pramA = conf.ParameterA
    conf.ParameterA = 6

しかし、プログラムの規模が大きくなって複数人で開発するようになってきたりすると、 誰かがどこかでまたConfigのインスタンスを作ってしまったり、confに新しいインスタンスで上書きしてしまうかもしれません。 そうするとまた独立したインスタンスが複数できてしまうことになります。

conf = Config()
・
・
・
config = Config() # confの存在に気がつかずに新しいインスタンスを作成

def moduleA():
    pramA = conf.ParameterA
    conf.ParameterA = 5

def moduleB():
    pramA = conf.ParameterA
    conf = Config() # 誤ってインスタンスの上書き
    conf.ParameterA = 6
    
def moduleC():
    pramA = conf.ParameterA
    config.ParameterA = 7 # confではないインスタンスを使用

このようなことを回避するには、一度インスタンスを作成したら、それ以降は新しいインスタンスを作成できないようにしなければなりません(;゚Д゚)

そこでSingletonパターンの登場です。イメージとしては以下のような感じです。
ConfigにクラスメソッドgetInstance()を実装しておきます。
getInstance()は、クラス内にインスタンスがなければクラス変数に自身のインスタンスを作成してそれを返します。
クラス変数に自身のインスタンスがすでに存在すれば、それをそのまま返します。
そしてConfig()でインスタンスを作成しようとしたときエラーを出力するようにします。
このようにしておけばConfigクラス内だけに存在する単一のインスタンスを作ることができます。

#conf = Config()   # 新しいインスタンス生成はエラー

def moduleA():
    conf = Config.getInstance() # インスタンス取得
    pramA = conf.ParameterA
    conf.ParameterA = 5

def moduleB():
    #conf = Config()   # 新しいインスタンス生成はエラー
    conf = Config.getInstance() # インスタンス取得
    pramA = conf.ParameterA
    conf.ParameterA = 6

試行錯誤の時間

まずConfig()でインスタンスを作成できないようにしてみます。
Pythonではraise Exceptionで例外をスローできます。
そこで以下のようにしてコンストラクタが呼び出されたときにエラーを出力できるようにしてみました。

class Config(object) :
    def __init__(self) :
        raise Exception("Singletonクラス")

これでインスタンスを作成しようとするとエラーになります。

conf = Config() # エラーになる

つぎにインスタンスを取得するためのクラスメソッドgetInstance()を実装してみます。
この例ではクラス変数__mInstanceが空っぽだった場合、Config() で自身のインスタンスを作成するようにしています。
もうお気づきかもしれませんが、これでは困ったことがおきます。
先の実装で、Config()でインスタンスを作成しようとするとエラーを発生させるようにしてしまったからです(・´ω`・;)

class Config(object) :
    __mInstance = None

    def __init__(self) :
        raise Exception("Singletonクラス")

    @classmethod
    def getInstance(cls) :
        if cls.__mInstance == None :
            cls.__mInstance = Config()  # エラー
        return cls.__mInstance

そこで__new__というものを利用します。
参考)__new__ ってなに?
def __new__(cls)は、インスタンスオブジェクトが作成される前に呼び出さる特殊なメソッドです。
以下、動作確認用のコードを書いてみたのでこれで動きを見てみます。

# coding: utf-8

class parent(object) :
    def __new__(cls) :
        print("new")
        return super().__new__(cls)

    def __init__(self) :
        print("init")

    def __del__(self) :
        print("del")

if __name__ == '__main__':
    obj = parent()

これの実行結果は以下のようになります。
先に__new__が呼び出されて、__init__が呼び出されているのがわかります。
引数であるclsはクラスオブジェクトを示しており、super().__new__(cls)でクラスオブジェクトに対するインスタンスオブジェクトを作成することができます。
__new__(cls)のあとに__init__(self)が呼び出さるわけですが、selfはのsuper().__new__(cls)で作ったインスタンスオブジェクトだったわけですね。 (ちなみにreturn super().__new__(cls)を消せば、__init__(self)が呼び出されなくなります。当然ながら)

new
init
del

__new__を応用して以下のようにしてみました(・`ω´・)v
この例では、def __new__(cls)が呼び出されたときにraise Exceptionで例外をスローするようにしました。
そしてdef getInstance(cls)が呼び出されたとき、クラス変数__mInstanceが空っぽだった場合、 クラスオブジェクトを示すclsを使って、super().__new__(cls)でインスタンスを作成するようにしています。

class Config(object) :

    __mInstance = None

    def __new__(cls) :
        print("new")
        raise Exception("Singletonクラス")

    def __init__(self) :
        print("init")

    def __del__(self) :
        pass

    @classmethod
    def getInstance(cls) :
        if cls.__mInstance == None :
            cls.__mInstance = super().__new__(cls)
        return cls.__mInstance

これでConfig()でインスタンスを作成しようとすると以下のようなエラーを出力するようになります。

  File "Singleton.py", line 7, in __new__
    raise Exception("Singletonクラス")
Exception: Singletonクラス

インスタンスの取得(未作成の場合は作成も)するには、以下のようにしてやらなければならなくなるはずです。

conf = Config.getInstance()

サンプルプログラム

以下、サンプルプログラムです。

# coding: utf-8

class _Singleton(object) :

    __mInstance = None

    def __new__(cls) :
        print("new  parent")
        raise Exception("Singletonクラス")

    def __init__(self) :
        print("init parent")

    @classmethod
    def getInstance(cls) :
        if cls.__mInstance == None :
            cls.__mInstance = super().__new__(cls)
        return cls.__mInstance

class myClass(_Singleton) :

    def __new__(cls) :
        print("new  child")
        return super().__new__(cls)

    def __init__(self) :
        print("init child")

    def setValue(self, value) :
        self.__mValue = value
    
    def getValue(self) :
        return self.__mValue

if __name__ == '__main__':
    
    #obj = Singleton()              # エラー
    #obj = Singleton.getInstance()

    #obj1 = myClass()               # エラー

    obj1 = myClass.getInstance()
    obj1.setValue(100)
    print(obj1.getValue())

    obj2 = myClass.getInstance()
    print(obj2.getValue())

    obj2.setValue(50)
    print(obj1.getValue())

実行結果はこのようになります!
myClass.getInstance()でobj1とobj2で同じインスタンを取得しています。
その後、それぞれでgetValue/setValueを行っていますが、printすればobj1とobj2で値が共有できていることがわかります。

$ python Singleton.py
100
100
50