最近は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_instance はNone |
|
2 | ★2 | ★3 は未実行なのでまだ_unique_instance はNone |
|
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_instance はNone |
|
2 | 1 | 初回なので _unique_instance はNone |
|
3 | 2 | ロックを確保 | |
4 | 2 | ロックを確保できないので待機 | |
5 | 3 | インスタンスはまだ生成されていないので_unique_instance はNone |
|
6 | 4 | インスタンスを生成 | |
7 | 2 | ロックが確保できたので通過 | |
8 | 3 | インスタンスはthread1ですでに生成されたのでifを満たさない |
これで同時に呼び出されたとしても期待した挙動を得られることがわかりました。このクラスを継承した場合の動作など、まだ考慮事項はありますがまずはこれで一通り満足できました。