intellista

engineer's notes about application development, data analysis, and so on

Pythonでシングルトン (Singleton) を実装する方法と注意点!

こんにちは!

今回はPythonでシングルトン(Singleton) を実装する方法と注意点をご紹介します。

仕事で必要になったのですが、インターネットで調査するといろいろな方法が紹介されています。
Javaのような方法からPythonらしい方法、独自で作りこむ方法など・・・
私はシンプルなのが好みなので、その中でも一番シンプルな方法をご紹介します。

実装する方法

Pythonでシングルトン (Singleton) を実装する方法をご説明します。

基本形

Pythonでのシングルトン(Singleton) の基本形は下記です。

class Singleton():
    def __new__(cls, *args, **kargs):
        if not hasattr(cls, "_instance"):
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

メソッド__new__を実装します。
その中で、自身のインスタンスを保持する内部変数がなければインスタンスを格納します。
2回目以降は、すでにインスタンスが格納されているため、再格納はされません。
そして最後に、最初に格納した自身のインスタンスを返します。
つまり、何回クラスをnewしても、最初に格納し自身のインスタンスを常に返します。

ご参考

実はif not hasattr(cls, "_instance"):if cls._instance == None:と書く方法もあります。
ただ、こうすると、下記のようにクラス変数_instance = Noneが必要になってしまい、少しコードが冗長になります。
_instance = Noneがないと、初回のif cls._instance == None:cls._instanceが存在しないという実行時エラーとなります。)

class Singleton():
    _instance = None  # ← 冗長
    def __new__(cls, *args, **kargs):
        if cls._instance == None:  # ← ここが基本形と異なる
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

少し突っ込んだ注意点

PythonでのSingletonについての、少し突っ込んだ注意点をご説明します。

*args, **kargs はどこいった?

もう一度、基本形を見てみます。

class Singleton():
    def __new__(cls, *args, **kargs):  # ← *args, **kargs を受け取る
        if not hasattr(cls, "_instance"):
            cls._instance = super(Singleton, cls).__new__(cls)  # ← *args, **kargs を渡さない
        return cls._instance

__new__(*args, **kargs):の引数で*args, **kargsを受け取っています。
しかし、super(Singleton, cls).__new__(cls)では*args, **kargsを渡していません。
super(Singleton, cls).__new__(cls, *args, **kargs)のように渡すと、エラーになります。

*args, **kargsはクラスのコンストラクタ式の引数なので、このクラスに渡るはずです。
ですが、その実装が見当たりません。
いったいどこに渡されるのでしょうか?

実は*args, **kargsは、このクラスの__init__()メソッドに渡されます。
なので、自分でdef __init__(self, 引数)と書くことでコンストラクタ式の引数を受け取れたわけです。
実際、次のように書くと、コンストラクタ式の第1引数をprintできます。

class Singleton():
    def __new__(cls, *args, **kargs):  # ← *args, **kargs を受け取る
        if not hasattr(cls, "_instance"):
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

    def __init__(self, *args, **karg):  # ← *args, **kargs を受け取る
        print(args[0])

__init__()についての私の誤解

結論を先に申します。

__init__()の直下に初回のみ実行することを期待する処理を書いてはいけません。
シングルトンであっても、__init__()はクラスを生成するたびに毎回実行されるからです。

呼び出し元がクラスのコンストラクタ式を呼び出してからクラスのインスタンスを受け取るまでの流れは、次のようになります。

  1. 呼び出し元がクラスのコンストラクタ式を実行する。
  2. クラスの__new__()が呼ばれる。
  3. クラスの__init__()が呼ばれる。
  4. 呼び出し元にクラスのインスタンスが返される。

そのため、__new__()__init__()も、 呼び出し元がクラスのコンストラクタ式を実行するたびに毎回、呼ばれます!

これは、私にとっては、ちょっとした罠でした。
というのも__new__()__init__()も、その名前から、初回のみ呼ばれるものと思い込んでいたのです。

一般的には__init__()に初期化処理などの前処理を書くと思います。
そのため、私も__init__()に初回のみ必要な処理を書いていました。
ですが、実際には毎回、呼ばれていたわけです。
そのため、性能劣化を起こしていた実装もありました。

本当に初回のみとしたい処理の書き方として、次の2つの方法が考えられます。
ただし、Pythonのお作法的に、どちらがよいのか(または両方ともよくないのか)は、わかっていません(すみません)。

  • 方法1: __new__()if not hasattr(cls, "_instance"):のブロック内に初回のみの処理を記述する。
  • 方法2: __init__()if not hasattr(self, "_instance"):のブロックを追加して、その中に初回のみの処理を記述する。

■ 方法1: __new__()の中に書く

class Singleton():
    def __new__(cls, *args, **kargs):
        if not hasattr(cls, "_instance"):
            cls._instance = super(Singleton, cls).__new__(cls)
            # ここに初回のみの処理を書く
        return cls._instance

■ 方法2: __init__()の中に書く

class Singleton():
    def __new__(cls, *args, **kargs):
        if not hasattr(cls, "_instance"):
            cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

    def __init__(self, *args, **karg):
        if not hasattr(self, "_instance"):
            # ここに初回のみの処理を書く

いくつかの処理から呼ばれるときの不整合に注意!

いくつかの処理から呼ばれるときの変数の不整合に注意しましょう。

どういうことかというと、次のようなイメージです。

  • 処理AでシングルトンクラスZの変数Zに1を格納する
  • 処理BでシングルトンクラスZの変数Zに2を格納する
  • 処理AでシングルトンクラスZの変数Zから1を取得するつもりが2を取得する
  • 処理BでシングルトンクラスZの変数Zから2を取得する

1つの器をいろんな処理からいじれるので、状態を変えられてもいいものは問題ありません。

しかし、処理ごとに状態をもちたい値は適しません。
例えば、シングルトンの変数に「DBコネクション」を保持させたりすると、SQL発行の前に別の処理でコネクションがcloseされてしまってエラーになる、といったことも起こります。

まとめ

今回はPythonでシングルトン(Singleton) を実装する方法と注意点をご紹介しました。
もう少し突っ込んだ注意点として、引数や__init__()に関する内容もご紹介しました。

なお、シングルトン以外でもPythonにはコツが必要なシーンが多々あります。
Python、Pandas、データ分析、に関するコツなどを次の記事にまとめてありますので、是非読んでみてください!
intellista.hatenablog.com