歩いたら休め

If the implementation is easy to explain, it may be a good idea.

【Python】クラス内のプライベート変数の名前解決方法についての調査

モジュールの外からアクセスするクラス( from sample import SampleClass )のために、以下のようなモジュール内のprivateな関数に利用するコードを書いていました。

# sample.py
class SampleClass(object):
    def public_method(self):
        __private_function()

def __private_function():
    print("ok")

if __name__ == "__main__":
    instance = SampleClass()
    instance.public_method()

そうすると次のようなエラーが出ました。どうやら"__"が冒頭につく変数の探索時に、自動的に '_SampleClass__private_function' という名前に置き換えられています。

docs.python.org

今まで self.__private_method() みたいに呼び出す場合だけ名前の置き換えが発生すると思っていたので少々びっくりしました。初めて知ったのですが、名前マングリング( name mangling )と呼ぶそうです。

$ python sample.py 
Traceback (most recent call last):
  File "sample.py", line 14, in <module>
    instance.public_method()
  File "sample.py", line 3, in public_method
    __private_function()
NameError: global name '_SampleClass__private_function' is not defined

実験

ここで気になるのが通常のローカル変数がセットされる時点でも、同様に名前がマングリングされているのかということです。そこで以下のサンプルコードで実験してみました。 locals は、そのスコープのローカル変数を辞書として取得する関数です。

class ExampleClass(object):
    def public_method(self):
        __val = 1
        print(locals())

if __name__ == "__main__":
    instance = ExampleClass()
    instance.public_method()

結果、 '_ExampleClass__val' に名前が書き換えられていることが分かります。

$ python sample.py 
{'_ExampleClass__val': 1, 'self': <__main__.ExampleClass object at 0x10dd12310>}

また、外の関数名を _SampleClass__private_function に直したちょっと無理やりな感じのコードも動作しました。

# sample.py
class SampleClass(object):
    def public_method(self):
        __private_function()

def _SampleClass__private_function():
    print("ok")

if __name__ == "__main__":
    instance = SampleClass()
    instance.public_method()

クラスではなく、関数だとどうでしょうか?

def main():
    __val = 1
    print(locals())


if __name__ == "__main__":
    main()

こちらは名前が置き換えられていません。

$ python sample.py
{'__val': 1}

まとめ

というわけで、Pythonのクラス内のプライベートな変数("__"がつくもの)は以下のような挙動をすることが確認できました。

  • クラス内の変数の宣言時にも探索時に名前が置き換えられた上で動作する
  • クラス外ではこのような動作をしない

実は公式ドキュメントにも該当する項目はありました。「コードが生成される前により長い形式に変換されます」とあるので、実行前のASTの変換処理あたりで挟まれてるんじゃないかと思います。

docs.python.org

プライベートな名前のマングリング: クラス定義内に書かれた識別子で、2つ以上のアンダースコアから始まり、末尾が2つ以上のアンダースコアで終わっていないものは、そのクラスの プライベートな名前 とみなされます。プライベートな名前は、コードが生成される前により長い形式に変換されます。この変換によって、クラス名の先頭にアンダースコアがあれば除去し、先頭にアンダースコアを1つ付加し、名前の前に挿入されます。