Pythonの自作パッケージで定数を使いたい

# 目次
1. [背景](#背景)
2. [試したこと1](#試したこと1)
3. [試したこと2](#試したこと2)
4. [ディレクトリ構造とファイルの中身](#ディレクトリ構造とファイルの中身)
5. [おわりに](#おわりに)

# 背景
物理シミュレーションにおける基礎物理定数など、自作パッケージで定数を複数ファイルで使いたいなと思う機会がありました。
Python で定数を使おうと思った時、1ファイルだけなら、ファイルの上の方に

```py
CONST = 100
```

とか書いておけば良いのですが、複数のモジュール全てにいちいちこれを書いているようでは管理は大変です。
なので、パッケージ内に `const.py` を作ってそこにひたすら定数を書き、各モジュールでそれを呼び出すことで、1つのファイルで管理できるようにしようと思いました。

今回試した `Python` のバージョンは `3.9.7` です。

```txt
$ python --version
Python 3.9.7
```


# 試したこと1
楽ではあるのですが、個人的には推奨しない方法です。
単純に定数管理モジュール内に定数をグローバル変数としてひたすら書き連ね、 `const2.HOGE` とかで呼び出す方法です。

`const2.py` を以下のように書いたとします。

```ruby:const2.py
A = 100
B = 1000
```

すると、以下のように簡単に呼び出せることがわかります。

```txt
$ python
>>> import package
>>> package.const2.A
100
```

同様に `module4.py` の中でも使って、自作関数を実装していきましょう。

```ruby:module4.py
from . import const2

def add(a: int) -> int:
    return const2.A + a

def add2(b: int) -> int:
    const2.B = b
    return const2.B + b
```

お気づきの方もいるかと思いますが、 `add2` 関数の中で、定数のはずなのに代入しちゃってますね。こういうことは暗黙の了解でやらないとは思いますし、こういうコード書いたら怒られるエディタとかもあるかもしれませんが、ひとまずこのミスに気づくことができなかったと仮定して、実行していこうと思います。

```txt
$ python
>>> import package
>>> package.module4.add(10)
110
>>> package.module4.add(123)
223
>>> package.module4.add2(123)
246
>>> package.module4.add2(30)
60
>>> package.const2.B
30
```

実行結果からもわかる通り、定数としての期待する挙動をしていませんね。代入が可能となっているため、 `add2` 関数は `add` 関数とは違って、定数 `B` に `b` を代入したうえで、 `b` を2倍した数を返すという挙動になってしまっています。こういうミスがあっても気づけずに間違った値を返し続けてしまうなんて怖いですよね。


# 試したこと2
個人的にはこれを推奨したいのですが、クラスと `@property` を使う方法です。
クラスのプライベート変数として定数を実装し、プロパティで呼び出すことを考えます。

この方法で定数を記述した `const.py` の中身を見ていきましょう。（基礎物理定数を使っています。）

```ruby:const.py
class Const(object):
    __c0 = 299_792_458.0
    __mu0 = 1.256_637_06 * 1e-6
    __epsilon0 = 8.854_187_81 * 1e-12
    __h = 6.626_070_15 * 1e-34

    @property
    def c0(self) -> float:
        return self.__c0

    @property
    def mu0(self) -> float:
        return self.__mu0

    @property
    def epsilon0(self) -> float:
        return self.__epsilon0

    @property
    def h(self) -> float:
        return self.__h
```

プライベート変数とすることでアクセスを防ぎ、プロパティを用いることで読み込みだけを可能としました。
この定数クラス `Const` を使って、モジュールを実装していきます。何通りか使い方が考えられると思うので、自分なりに思いついた使い方をいくつか紹介します。

### 使い方1
単純に `Const().hoge` のように、毎回インスタンスを生成して使うという方法です。

```ruby:module1.py
from .const import Const

def E2D(E: float) -> float:
    return Const().epsilon0 * E

def D2E(D: float) -> float:
    return D / Const().epsilon0
```

以下のように計算できていることが分かります。また、 `Const` クラスを呼び出すことも可能です。

```txt
$ python
>>> import package
>>> package.module1.E2D(3e7)
0.0002656256343
>>> CONST = package.module1.Const()
>>> CONST.h
6.62607015e-34
```

### 使い方2
クラスで使うとき、クラスの継承を使う方法です。（使い方1でも全然良いと思いますが。）グローバル変数として `CONST = Const()`

```ruby:module2.py
from .const import Const

class MagneticField(Const):
    def __init__(self) -> None:
        super().__init__()

    def B2H(self, B: float) -> float:
        return B / self.mu0

    def H2B(self, H: float) -> float:
        return self.mu0 * H
```

以下を確認するとこちらも計算できていることが分かります。子クラスでも呼び出すことはできますが、代入したり、元のプライベート変数にアクセスしたりはできないようになっているので、定数としての役割を果たせていることが分かります。
`Const` クラスの `__init__()` 内で定数を宣言しても良いとは思いますが、その場合は子クラスの `__init__()` で `super().__init__()` を忘れないようにしましょう。

```txt
$ python
>>> import package
>>> mag = package.module2.MagneticField()
>>> mag.H2B(1234567)
1.5514026452530199
>>> mag.mu0
1.25663706e-06
>>> mag.mu0 = 100
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> mag.__mu0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MagneticField' object has no attribute '__mu0'
```

### 使い方3
クラスで使うとき、クラスのプライベート変数を利用する方法です。定数の値すら見せたくない場合に使えると思います。

```ruby:module3.py
class Atom:
    def __init__(self, _lambda) -> None:
        from .const import Const
        self.__const = Const()
        self._lambda = _lambda

    def energy(self) -> float:
        return self.__const.h * self.__const.c0 / self._lambda

    def momentum(self) -> float:
        return self.__const.h / self._lambda
```

こちらも正しく計算されている様子が分かります。当然 `__const` にはアクセスできません。

```txt
$ python
>>> import package
>>> atom = package.module3.Atom(5.5 * 1e-7)
>>> atom.energy()
3.611719740270779e-19
```

今回は `module1.py` などで定数を公開する感じの実装をしているため、簡単にアクセスできてしまいますが、定数モジュールの場所や使っている変数名が分からないようになっていれば、定数の中身を知ることができない実装となっています。
`package` の `__init__.py` でこの `module3` のみを `import` すればそのような状況を作り出せますが、その状況でも、定数モジュールの場所とクラス名が分かってしまえば普通に使えてしまうので、気をつけましょう。（他にも抜け道があるかもしれません。）

```txt:ruby:module3&ensp;のみ&ensp;import&ensp;する場合
$ python
>>> import package
>>> CONST = package.const.Const()
>>> CONST.mu0 * CONST.epsilon0 * CONST.c0 ** 2
0.9999999979966796
```

# ディレクトリ構造とファイルの中身
これまで話してきたファイル関係と、触れていないファイルの中身をここで示します。

```txt
.
└── package
    ├── __init__.py
    ├── const.py
    ├── const2.py
    ├── module1.py
    ├── module2.py
    ├── module3.py
    └── module4.py
```

```ruby:__init__.py
from . import const
from . import const2
from . import module1
from . import module2
from . import module3
from . import module4

__all__ = ["const", "const2", "module1", "module2", "module3", "module4"]
```


# おわりに
もっと簡単にできる方法とか、ここに書いてある方法だと実は問題があるとか、そもそも間違っているとか、意見や感想などがあれば教えてくださると助かります。
そういえば numpy の円周率といった定数の実装とかはどうなっているか調べてみても良かったなとこの記事を書き終わってから思いました。
わざわざプライベート変数（_クラス名__変数名でアクセスできてしまう）を使わなくても setter とかでエラー出してくれるようにすれば良かったなとかも思い始めましたが、また今度の機会に試したいと思います。