Python でシングルトンパターンを実装する

生成されるクラスインスタンスを常に一つにするシングルトンパターンを Python で実装します。

Python は Java みたいに “private static” みたいな修飾ができないし、どこで初期化すればいいのかもちょっと考える必要があります。


まずダメなパターンから。

class Sandwich:
    singleton = Sandwich()

# これは NameError: name 'Sandwich' is not defined で失敗する。

Python ではこのスコープでインスタンスを生成しようとすると「クラスが定義されていない」として失敗します。

では __init__() メソッドはでしょうか。このアイデアもダメです。なぜなら __init__() メソッドの引数に渡される self はインスタンスですよね。 __init__() メソッドがコールされた時点で、既にインスタンスが作られてしまっています。複数回呼ばれたら複数のインスタンスが生成されることになって、シングルトンになりません。

そこで __new__() メソッドです。 __init__() メソッドより前に呼ばれ、インスタンスを生成して返すのが __new__() メソッドです。単にインスタンスを生成して返すだけの場合はこんな感じです。

class Sandwich:
    def __new__(cls, *args, **kwargs):
        # 親クラスの __new__() にインスタンスを作ってもらい、そのまま返す
        return super(Sandwich, cls).__new__(cls)

つまり __new__() メソッドが返すインスタンスを一つだけにすれば、シングルトンパターンを実装できます。それがこちら。

# Singleton

import threading

class Sandwich:
    __singleton = None
    __new_lock = threading.Lock()
    # 変数名に "__" (アンダースコアを2つ) をつけるとプライベート変数になる

    def __new__(cls, *args, **kwargs):
        cls.__new_lock.acquire() # ロック取得
        if cls.__singleton == None:
            cls.__singleton = super(Sandwich, cls).__new__(cls)
        cls.__new_lock.release() # ロック開放
        return cls.__singleton

クラス変数にはじめて生成したインスタンスを代入しておいて、それを使いまわします。マルチスレッドでインスタンスが複数作られないように「既存インスタンスのチェック」と「インスタンスの生成」に再入されないように排他制御しています。ただ、2回目以降は再入されてても問題ないので、無駄な排他制御になってしまいます。以下のように、例えばインポート時に必ず1回だけインスタンスを作っておいて、あとはロックフリーでもいいでしょう。

# Singleton'

class Sandwich:
    __singleton = None

    def __new__(cls, *args, **kwargs):
        if cls.__singleton == None:
            cls.__singleton = super(Sandwich, cls).__new__(cls)
        return cls.__singleton

Sandwich() # クラス定義の直下に書いて、インスタンスを作るために必ず呼ぶ

最後に、本当にシングルトンになっているか確認するコードを貼っておきます。

>>> class Sandwich:
...     __singleton = None
...     __spam = None
...     def __new__(cls, *args, **kwargs):
...         if cls.__singleton == None:
...             cls.__singleton = super(Sandwich, cls).__new__(cls)
...         return cls.__singleton
...     def set_spam(self, spam=None):
...         self.__spam = spam
...     def get_spam(self):
...         return self.__spam
... 
>>> Sandwich()
<__main__.Sandwich object at 0x76ad5630>
>>> sw1 = Sandwich()
>>> sw2 = Sandwich()
>>> sw1
<__main__.Sandwich object at 0x76ad5630>
>>> sw2
<__main__.Sandwich object at 0x76ad5630>
>>> sw2.set_spam('spam spam spam')
>>> sw1.get_spam()
'spam spam spam'

うえの sw1 と sw2 は同じものだということがわかります。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*