最近はを読みながら、デザインパターンの勉強をしています。この本は読みやすくてとても参考になるのですが、サンプルの実装は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を満たさない |
これで同時に呼び出されたとしても期待した挙動を得られることがわかりました。このクラスを継承した場合の動作など、まだ考慮事項はありますがまずはこれで一通り満足できました。