目次
背景
物理シミュレーションにおける基礎物理定数など、自作パッケージで定数を複数ファイルで使いたいなと思う機会がありました。
Python で定数を使おうと思った時、1ファイルだけなら、ファイルの上の方に
CONST = 100
とか書いておけば良いのですが、複数のモジュール全てにいちいちこれを書いているようでは管理は大変です。
なので、パッケージ内に const.py
を作ってそこにひたすら定数を書き、各モジュールでそれを呼び出すことで、1つのファイルで管理できるようにしようと思いました。
今回試した Python
のバージョンは 3.9.7
です。
$ python --version
Python 3.9.7
試したこと1
楽ではあるのですが、個人的には推奨しない方法です。
単純に定数管理モジュール内に定数をグローバル変数としてひたすら書き連ね、 const2.HOGE
とかで呼び出す方法です。
const2.py
を以下のように書いたとします。
A = 100
B = 1000
すると、以下のように簡単に呼び出せることがわかります。
$ python
>>> import package
>>> package.const2.A
100
同様に 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
関数の中で、定数のはずなのに代入しちゃってますね。こういうことは暗黙の了解でやらないとは思いますし、こういうコード書いたら怒られるエディタとかもあるかもしれませんが、ひとまずこのミスに気づくことができなかったと仮定して、実行していこうと思います。
$ 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
の中身を見ていきましょう。(基礎物理定数を使っています。)
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
のように、毎回インスタンスを生成して使うという方法です。
from .const import Const
def E2D(E: float) -> float:
return Const().epsilon0 * E
def D2E(D: float) -> float:
return D / Const().epsilon0
以下のように計算できていることが分かります。また、 Const
クラスを呼び出すことも可能です。
$ python
>>> import package
>>> package.module1.E2D(3e7)
0.0002656256343
>>> CONST = package.module1.Const()
>>> CONST.h
6.62607015e-34
使い方2
クラスで使うとき、クラスの継承を使う方法です。(使い方1でも全然良いと思いますが。)グローバル変数として CONST = Const()
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__()
を忘れないようにしましょう。
$ 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
クラスで使うとき、クラスのプライベート変数を利用する方法です。定数の値すら見せたくない場合に使えると思います。
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
にはアクセスできません。
$ python
>>> import package
>>> atom = package.module3.Atom(5.5 * 1e-7)
>>> atom.energy()
3.611719740270779e-19
今回は module1.py
などで定数を公開する感じの実装をしているため、簡単にアクセスできてしまいますが、定数モジュールの場所や使っている変数名が分からないようになっていれば、定数の中身を知ることができない実装となっています。
package
の __init__.py
でこの module3
のみを import
すればそのような状況を作り出せますが、その状況でも、定数モジュールの場所とクラス名が分かってしまえば普通に使えてしまうので、気をつけましょう。(他にも抜け道があるかもしれません。)
$ python
>>> import package
>>> CONST = package.const.Const()
>>> CONST.mu0 * CONST.epsilon0 * CONST.c0 ** 2
0.9999999979966796
ディレクトリ構造とファイルの中身
これまで話してきたファイル関係と、触れていないファイルの中身をここで示します。
.
└── package
├── __init__.py
├── const.py
├── const2.py
├── module1.py
├── module2.py
├── module3.py
└── module4.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 とかでエラー出してくれるようにすれば良かったなとかも思い始めましたが、また今度の機会に試したいと思います。