[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

Pythonでシングルトン(Singleton)を実装してみる

最近はHead First デザインパターンを読みながら、デザインパターンの勉強をしています。この本は読みやすくてとても参考になるのですが、サンプルの実装はJavaになっておりそのままPythonに移植することはできません。

第5章がシングルトンパターンですが、そもそもPythonでどのようにシングルトンを実装すべきかがわからなかったので確認した結果を残しておきます。

Javaでの実装

書籍に載っているJavaの実装は2重チェックロッキングを用いた以下のようなものです。

public class Singleton {
    // 唯一のインスタンスを保持する変数
    private volatile static Singleton uniqueInstance;

    // コンストラクタがPrivateなので外部からは呼び出せない
    private Singleton(){}
    
    // 外部からインスタンスを取得するためのメソッド
    public static Singleton getInstance() {
        // 初回呼び出しではインスタンスが未生成
        if (uniqueInstance == null){ // ★1
            // インスタンス生成を同時に複数のスレッドが行わないようにsynchronizedする
            synchronized(Singleton.class){  // ★2
                // ★1,★2は厳密には複数スレッドが到達する可能性がある
                // 必ず1スレッドのみがインスタンスを生成できるようにする
                if (uniqueInstance == null){
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

Pythonでの実装

上述のクラスをPythonのクラスに実装していきます。まずは、同期処理等を考慮せず元になるクラスを考えます。

class Singleton:

    _unique_instance = None

    @classmethod
    def get_instance(cls):
        if not cls._unique_instance:
            cls._unique_instance = cls()

        return cls._unique_instance

クラスメソッドとしてget_instanceを設けておき、その内部ではクラス変数_unique_instanceをチェックし、まだ生成されていない場合はインスタンスを生成してから戻します。

>>> from lib.singleton import Singleton
>>> Singleton.get_instance()
<lib.singleton.Singleton object at 0x101a18a90>
>>> Singleton.get_instance()
<lib.singleton.Singleton object at 0x101a18a90>
>>> Singleton.get_instance()
<lib.singleton.Singleton object at 0x101a18a90>

本結果のように何度呼び出しても同じインスタンス(address:0x101a18a9)が戻っていることがわかります。

コンストラクタのプライベート化

しかし、先の実装では、以下のように通常の方法でインスタンスを生成できてしまいます。

>>> Singleton()
<lib.singleton.Singleton object at 0x101a18a58>
>>> Singleton()
<lib.singleton.Singleton object at 0x101a18b70>

これを避けるにはコンストラクタをPrivateにできればよいのですが、PythonにはJavaと違ってそのような方法はありません。Pythonのコンストラクタ(のようなもの)は__init__という内部メソッドです。そこで以下のように__init__を呼び出せないようにすることが考えられます。

class Singleton:
    _unique_instance = None
    def __init__(self):
        raise NotImplementedError('not allowed')

    @classmethod
    def get_instance(cls):
        if not cls._unique_instance:
            cls._unique_instance = cls()  # ★1
        return cls._unique_instance

これで確かに通常の方法ではインスタンスが作成できなくなります。

>>> Singleton()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/denzow/PycharmProjects/practice-design-pattern/05_singleton/chocolate_factory/lib/singleton.py", line 9, in __init__
    raise NotImplementedError('not allowed')
NotImplementedError: not allowed

しかし、get_instance経由でも同じように呼びさせなくなってしまいます。★1の箇所の呼び出して結局__init__が呼び出されてしまうからです。

>>> Singleton.get_instance()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/denzow/PycharmProjects/practice-design-pattern/05_singleton/chocolate_factory/lib/singleton.py", line 14, in get_instance
    cls._unique_instance = cls()
  File "/Users/denzow/PycharmProjects/practice-design-pattern/05_singleton/chocolate_factory/lib/singleton.py", line 9, in __init__
    raise NotImplementedError('not allowed')
NotImplementedError: not allowed

Pythonのクラスは初期化する際に、まず__new__が呼ばれ、その後に__init__が呼ばれています。 そこで、__init__ではなく__new__を変更します。

class Singleton:

    _unique_instance = None

    def __new__(cls):
        raise NotImplementedError('Cannot initialize via Constructor')

    @classmethod
    def __internal_new__(cls):
        return super().__new__(cls)

    @classmethod
    def get_instance(cls):
        if not cls._unique_instance:
            cls._unique_instance = cls.__internal_new__()  # 変更

        return cls._unique_instance

__new__を呼び出すことはできず、これまでの__new__と同様の処理を行う__internal_new__を定義し、get_instance内部ではそちらを呼び出すようにします。

>>> from lib.singleton import Singleton
>>> Singleton()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/denzow/PycharmProjects/practice-design-pattern/05_singleton/chocolate_factory/lib/singleton.py", line 9, in __new__
    raise NotImplementedError('Cannot initialize via Constructor')
NotImplementedError: Cannot initialize via Constructor  # コンストラクタでは呼び出せない
>>> Singleton.get_instance()
<lib.singleton.Singleton object at 0x104db3518>
>>> Singleton.get_instance()
<lib.singleton.Singleton object at 0x104db3518>  # 同じインスタンスが戻っている

意図した挙動になっています。

マルチスレッドへの考慮

基本的な実装ができましたが、まだマルチスレッドで呼び出された場合を考慮できていません。

    @classmethod
    def get_instance(cls):
        if not cls._unique_instance:  # ★2
            cls._unique_instance = cls.__internal_new__()  # ★3

        return cls._unique_instance

複数スレッドから呼び出すときに、★2★3が必ずアトミックに実行される保証はありません。例えば以下のようなケースです。

time thread1 thread2 memo
1 ★2 初回なので _unique_instanceNone
2 ★2 ★3は未実行なのでまだ_unique_instanceNone
3 ★3 _unique_instanceにインスタンスがセットされる
4 ★3 ifはすでに抜けているので_unique_instanceにインスタンスが再度セットされる

これは期待した動作ではありません。Javaでいうところのsynchronized的なのが必要になります。Pythonでの実装を考えるならばthreading.Lockを使用することになるでしょう。

from threading import Lock


class Singleton:

    _unique_instance = None
    _lock = Lock()  # クラスロック

    def __new__(cls):
        raise NotImplementedError('Cannot initialize via Constructor')

    @classmethod
    def __internal_new__(cls):
        return super().__new__(cls)

    @classmethod
    def get_instance(cls):
        if not cls._unique_instance:
            with cls._lock:
                if not cls._unique_instance:
                    cls._unique_instance = cls.__internal_new__()
        return cls._unique_instance

これで、get_instanceを呼び出したタイミングでインスタンス生成が必要になったとしてもwith cls._lockでシリアライズしているので、インスタンスの初期化は必ず1回だけ実施されるようになります。ロックを取得してから再度if not cls._unique_instanceをしているのは、以下のような呼び出しを考慮してのものです。

1:        if not cls._unique_instance:
2:            with cls._lock:
3:                if not cls._unique_instance:
4:                    cls._unique_instance = cls.__internal_new__()
time thread1 thread2 memo
1 1 初回なので _unique_instanceNone
2 1 初回なので _unique_instanceNone
3 2 ロックを確保
4 2 ロックを確保できないので待機
5 3 インスタンスはまだ生成されていないので_unique_instanceNone
6 4 インスタンスを生成
7 2 ロックが確保できたので通過
8 3 インスタンスはthread1ですでに生成されたのでifを満たさない

これで同時に呼び出されたとしても期待した挙動を得られることがわかりました。このクラスを継承した場合の動作など、まだ考慮事項はありますがまずはこれで一通り満足できました。