PythonでSingleton(シングルトン)使いたくなったんで、
色々調べてみたんですが、見てく中でちょっと、ん?って感じた。
PythonにはC#とかで言う、privateがないので、いろいろ工夫が必要。ってのは分かる。
例えば、
# まずとりえず class SingletonClass: __instance = None def __init__(self): self.val = "value" @classmethod def get_model(cls): if cls.__instance is None: cls.__instance = SingletonClass() return cls.__instance if __name__ == "__main__": x = SingletonClass.get_model() y = SingletonClass.get_model() print(f"x = {id(x)} , y = {id(y)}") # x = 140550875600936 , y = 140550875600936
get_modelを使ってる間は大丈夫。ただ、SingletonClass()の普通のコンストラクタに
制限かけてるわけじゃないので、1つである事を保証はしてない。
なので、まず__new__を使ってみる。
#__new__使う場合 class SingletonClass: __instance = None def __init__(self): print("__init__") self.val = "value" def __new__(cls): print("__new__") if cls.__instance is None: cls.__instance = super().__new__(cls) return cls.__instance if __name__ == "__main__": x = SingletonClass() print(x.__dict__) y = SingletonClass() print(x.__dict__) print(f"x = {id(x)} , y = {id(y)}") ↓実行結果 __new__ __init__ {'val': 'value'} __new__ __init__ {'val': 'value'} x = 140367603169544 , y = 140367603169544
公式のドキュメント的には
__init__は、インスタンスが (__new__() によって) 生成された後、それが呼び出し元に返される前に呼び出されます。引数はクラスのコンストラクタ式に渡したものです。
__new__() が cls のインスタンスを返した場合、 __init__(self[, ...]) のようにしてインスタンスの __init__() が呼び出されます。このとき、 self は新たに生成されたインスタンスで、残りの引数は __new__() に渡された引数と同じになります。
__new__() が cls のインスタンスを返さない場合、インスタンスの __init__() メソッドは呼び出されません。
__new__() の主な目的は、変更不能な型 (int, str, tuple など) のサブクラスでインスタンス生成をカスタマイズすることにあります。また、クラス生成をカスタマイズするために、カスタムのメタクラスでよくオーバーライドされます。
って事らしいので、__new__でオブジェクトの実態が作られてるから、__new__の中で既にインスタンスがあるかどうか見る感じ。
で、この実装だと、
x = SingletonClass() y = SingletonClass() x.val = "x-value" print(f"y.val = {y.val}") x.val = "y-value" print(f"x.val = {x.val}") ↓結果 y.val = x-value x.val = y-value
みたいに書けるので、シングルトンを意識しないで使えて、値も共有出来て便利ー。みたいな感じ?
正直、「え?それってどうなん?」って思た。
つか、これってシングルトンっていうか、
ただのグローバル変数 だよね?
確かに、オブジェクトのIDは一致してるんだけど、上で行くと、2回目の__new__の中で
__init__呼び出されてるから、self.valの値変わっちゃうよね?↓の感じ。
x = SingletonClass() x.val = "x-value" print(f"x.val = {x.val}") y = SingletonClass() print(f"x.val = {x.val}") ↓結果 x.val = x-value __new__ __init__ x.val = value
んーと、これだいぶヤベー気がするのですが、こういうモンなの?
あと、シングルトン意識しなくていいとか、どうなんだろ。
シングルトンは意識して使って欲しいと自分は思う。
つか、シングルトンって何のために使うんだろ。
グローバル変数の変わり?それは違うよね?
オブジェクト指向自体、グローバル変数との闘いの果てに出来たものだと思うので、
そういう使い方は違うように思う。
シングルトンって、不変な何かのマスタとか設定とか、オブジェクト指向だと、
どうしてもインスタンスを毎回作る感じになってしまうので、
そういう不要なインスタンスを作らないでパフォーマンスとかメモリ効率を
上げるためのものだと思ってます。
ただ、Pythonだとやっぱりどうしてもオブジェクト内の値すらも、自由に書き換え出来ちゃうから、
こういうの強制させるものは作るの無理なんかな。。。
一人で作ってる時は正直どーでもいいんだけど、10人とか開発者がいると、
色々意識して欲しいので、とりあえずこんな感じにしてみた。
class SingletonClass: def __new__(cls): raise NotImplementedError("コンストラクタ直呼びは禁止。SingletonFactory.get_model()を使用。") @classmethod def __private_init__(cls, self): print("__init__") self.val = "value" return self @classmethod def __private_new__(cls): print("__new__") return cls.__private_init__(super().__new__(cls)) def inst_method(self): return True class SingletonFactory: __instance = None @classmethod def get_model(cls): if cls.__instance is None: cls.__instance = SingletonClass.__private_new__() return cls.__instance
完全に自前でコンストラクタ用意した感じ。
いちを、普通に__new__と__init__で作ったオブジェクトとdir()の結果は
一致してたから大丈夫だと思うんだけど、ちょっと心配。
ただ、まー不変な設定とかマスタをキャッシュ的に使いたいだけなのでこれでもいいか。
で、これだとスレッドセーフじゃないので、ロックをつける。
import threading class SingletonFactory: __instance = None _lock = threading.Lock() @classmethod def get_model(cls): with cls._lock: if cls.__instance is None: cls.__instance = SingletonClass.__private_new__() return cls.__instance
あと、起動中は不変なデータなハズなんだけど、もしかしたら、稼働中に
値の更新をしたくなるかもしれないので、いちを更新用のメソッドをつけておく。
最終系。。。
import threading class SingletonClass: def __new__(cls): raise NotImplementedError("コンストラクタ直呼びは禁止。SingletonFactory.get_model()を使用。") @classmethod def __private_init__(cls, self): print("__init__") self.val = "value" return self @classmethod def __private_new__(cls): print("__new__") return cls.__private_init__(super().__new__(cls)) def inst_method(self): return True class SingletonFactory: __instance = None _lock = threading.Lock() @classmethod def get_model(cls): with cls._lock: if cls.__instance is None: cls.__instance = SingletonClass.__private_new__() return cls.__instance @classmethod def recreate(cls): with cls._lock: cls.__instance = SingletonClass.__private_new__() return cls.__instance
もちろん、Pythonのコードなので、正直、取得した後に値とかオブジェクトの内容を変更したりは
余裕で出来る。ただ、意識はしてくれるんじゃないだろうかと期待。
StackOverFlowだと、デコレータ使ったりして定義してるっぽいけど、どうなんでしょ。
サクッと作るならこの辺りかなーとか思いました。
ついでに、Djangoのソース読んでるとロックのところって下で書いてる。
_lock = threading.RLock()
なんで、RLock()使ってるんだろ?
理由が思いつかない。知ってる人誰か教えてください。