【Python3】@propertyで変数を疑似的にプライベート変数化して隠ぺいする

Pythonには変数をプライベート化(外部から参照・代入できなくする)する機能がありません。
ただしpropertyデコレータを利用するとそれっぽいものを実装することはできます。


## 環境
Ubuntu20.04
Python3.8.10

## propertyとは

Pythonの組み込みクラスで、オブジェクトの属性を操作する関数を実装しています。

#### 定義
``` property(fget=None, fset=None, fdel=None, doc=None) ``` 

- fget: 属性の値を取得する関数
- fset: 属性の値を設定する関数
- fdel: 属性の値を削除する関数
- doc : 属性のdocstringを作成する

基本的な使用方法は下記の通りです。(Pythonの公式ドキュメントから丸写し)

```python
class C:
    def __init__(self):
        self._x = None

    def getx(self):
        return self._x

    def setx(self, value):
        self._x = value

    def delx(self):
        del self._x

    x = property(getx, setx, delx, "I'm the 'x' property.")
```
[組み込み関数 ― Python 3.10.0b2 ドキュメント より](https://docs.python.org/ja/3/library/functions.html#property)

デコレータを利用するとより可読性が高くなります。(同じくPythonの公式ドキュメントから丸写し)

```python
class Parrot:
    def __init__(self):
        self._voltage = 100000

    @property
    def voltage(self):
        """Get the current voltage."""
        return self._voltage
```

[組み込み関数 ― Python 3.10.0b2 ドキュメント より](https://docs.python.org/ja/3/library/functions.html#property)

このような関数を実装すると_voltage変数を読み取り専用の変数にすることができます。

#### 動作確認

実際に動かしてみます。

```python:hoge.py
class Hoge(object):
    def __init__(self, hoge: str):
        self._hoge = hoge
    
    @property
    def hoge(self):
        return self._hoge

hoge = Hoge("fuga")
print(hoge.hoge)
#hoge.hoge = "aa"
```

```bash
$ python3 hoge.py 
fuga
```
Hogeクラスインスタンスのhoge変数を参照することができました。
では、```#hoge.hoge = "aa"``` のコメントアウトを外して再度実行するとどうなるでしょうか

```python:hoge.py
class Hoge(object):
```(省略)```

hoge = Hoge("fuga")
print(hoge.hoge)
hoge.hoge = "aa"
```
```bash
$ python3 hoge.py 
Traceback (most recent call last):
  File "hoge.py", line 12, in <module>
    hoge.hoge = "aa"
AttributeError: can't set attribute
```

期待通り_hoge変数への代入が失敗しました。これで意図しない値が代入されるリスクを減らすことができます。

## 注意点

#### 1. デコレートする関数名と変数名が同じだとインスタンスが初期化できない

```python:hoge.py
class Hoge(object):
    def __init__(self, hoge: str):
        self.hoge = hoge
    
    @property
    def hoge(self):
        return self.hoge

hoge = Hoge("fuga")
#hoge.hoge = "aa"
print(hoge.hoge)
```

このコードを実行すると失敗します。

```bash
$ python3 hoge.py 
Traceback (most recent call last):
  File "hoge.py", line 11, in <module>
    hoge = Hoge("fuga")
  File "hoge.py", line 5, in __init__
    self.hoge = hoge
AttributeError: can't set attribute
```

代入を禁止しているので初期化も失敗するという当たり前といえば当たり前の話ですが...
「変数名の頭にアンダースコアを1つつける」というルールを設けておくのが良いでしょう。(アンダースコアを2つつけるとマングリングされてしまうのであまりよくないと思われます。)

#### 2. 変数に直接代入することはできる(できてしまう)

@propertyを利用したインスタンス変数の隠蔽は変数それ自体をプライベート化するというよりは、関数でラッピングして隠蔽するというのが実態に近いです。(多分、おそらく)

したがって、クラスの外部から変数名を直接指定して代入すると値が上書きされてしまいます。

```python:hoge.py
class Hoge(object):
    def __init__(self, hoge: str):
        self._hoge = hoge
    
    @property
    def hoge(self):
        return self._hoge

hoge = Hoge("fuga")
# _hogeフィールドに直接再代入
hoge._hoge = "aa"
print(hoge.hoge)
```

```bash
$ python3 hoge.py 
# 値が上書きされている
aa
```
チーム開発の際はラッピングされた変数に直接アクセスしないというルールが必要になるでしょう。
(~~こんなめんどくさいことしたくないから早くプライベート変数を言語でサポートしてほしい~~)

## まとめ

なんとか(見た目上は)インスタンス変数を隠蔽することができました。
@propertyを使えばクラス変数も同じように不変にできますし、定数のようなものも作成できそうです。

また、裏技のような方法が普通に公式ドキュメントに書いてあったのも意外です。
一次情報をきっちり読み込むのが大事だと思い知りました