Pythonのカプセル化について備忘録としてまとめます。
環境
- Ubuntu20.04
- Python 3.8.10
筆者の認識
- インスタンス変数の頭にアンダーバーを2つ着けるとプライベート変数として扱われ、クラスの外部から参照・再代入できなくなる。
Class Hoge(object):
def __init__(self, hoge: str):
# 外部からアクセスできなくする
self__hoge = hoge
hoge = Hoge("fuga")
# エラーになるはず
hoge.__hoge = "foo"
# エラーになるはず
print(hoge.__hoge)
実際の挙動
class Hoge(object):
def __init__(self, hoge: str):
self__hoge = hoge
hoge = Hoge("fuga")
hoge.__hoge = "foo"
print(hoge.__hoge)
Hoge.pyをコマンドライン上で実行すると再代入・変数へのアクセスともにできてしまいます。
$ python3 Hoge.py
foo
再代入せず、参照だけした場合、エラーになりました。(意図した挙動)
参照はできないが代入はできるというのが不可解です。再代入したら参照もできてしまうのもよくわかりません。
class Hoge2(object):
def __init__(self, hoge: str):
self__hoge = hoge
hoge = Hoge("fuga")
print(hoge.__hoge)
$ python3 Hoge2.py
Traceback (most recent call last):
File "Hoge2.py", line 9, in <module>
print(hoge.__hoge)
AttributeError: 'Hoge2' object has no attribute '__hoge'
検証
1. 本当に変数名の頭にアンダースコアを2つ付けるとプライベート変数になるのか
Pythonの公式ドキュメントには下記の記述がありました。
“Private” instance variables that cannot be accessed except from inside an object don’t exist in Python.
どうやら一般的な意味でのプライベート変数はPythonには無いようです。( 同じページに Python classes provide all the standard features of Object Oriented Programming なんて書いてるのは何なんだ)
なんで、「変数名の頭にアンダースコアを2つつけると(厳密な意味での)プライベート変数になる」 というのは偽であると言えそうです。
Pythonのマングリング(アンダースコア2つから始まる変数の名前を置換する機能)によりこのような挙動になります。
例: Hogeクラスの __hoge
変数は _Hoge__hoge
という変数名になる
この機能は本来は継承元と継承先で変数名が重複しないようにするのが目的のようです。
class Hoge3(object):
def __init__(self, hoge: str):
self.__hoge = hoge
hoge = Hoge3("fuga")
hoge.__hoge = "foo"
print(vars(hoge))
上記のスクリプトを実行すると以下のような結果になります。
$ python3 hoge3.py
{'_Hoge3__hoge': 'fuga', '__hoge': 'foo'}
挙動をまとめると
- Hogeクラス内で定義した
__hoge
変数は実行前に_Hoge__hoge
という名前に置換される。 - 実行時には hogeクラスのオブジェクトには
__hoge
という変数は存在しないので参照できない。 - hogeクラスのオブジェクトが
__hoge
変数を持たないため、再代入できるように見えた。(実際は新しい属性がオブジェクトに追加された ※ hoge3.pyの実行結果より)
ということになります。
参照はできるのに再代入ができるように見えたのはマングリングが原因でした。
課題
Pythonのこの仕様には2つの問題があるように思われます。
- 本来の意味でインスタンス変数を隠蔽できていない(見かけ上は再代入できる)
- オブジェクトに自由に属性を追加(おそらく削除も?)できてしまう
これらの課題の解消方法を今後調査・検討していきたいと思います。