Pythonで定数を扱う方法 (mypy無し版)
前回のあらすじ
以前、Pythonで定数を作成するときは、mypyのFinalを使うと良いという記事を書きました。(定数関数は避ける)
とはいえ、所属組織によっては、既に大きいコードがあり、mypyを導入するのが難しいということもあるかと思います。今回はmypyが使えない状況で定数を作る方法を解説します。
データクラス(dataclass) で実装
実装
from dataclasses import dataclass
@dataclass(frozen=True)
class Constants:
PI: float = 3.14
def main():
constants = Constants()
print(f"Initialized!! PI: {constants.PI}")
constants.PI = "OVERWRITTEN"
if __name__ == "__main__":
main()
実行結果
constant_with_python (main) » uv run reassign_dataclass.py
Initialized!! PI: 3.14
Traceback (most recent call last):
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_dataclass.py", line 16, in <module>
main()
~~~~^^
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_dataclass.py", line 12, in main
constants.PI = "OVERWRITTEN"
^^^^^^^^^^^^
File "<string>", line 15, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'PI'
解説
Python 3.7で導入された機能で、データを格納するためのクラスを作成できます。
@dataclassはデコレータと呼ばれるもので、クラスに特別な機能を追加します。frozen=Trueを指定すると、インスタンス作成後に属性を変更できなくなります。
実装が非常に簡単な所がメリットです。
参照: PEP 557 – Data Classes, frozen-instances
It is not possible to create truly immutable Python objects. However, by passing frozen=True to the @dataclass decorator you can emulate immutability. In that case, Data Classes will add setattr and delattr methods to the class. These methods will raise a FrozenInstanceError when invoked.
列挙型(Enum)で実装
実装
from enum import Enum
class Constants(Enum):
PI = 3.14
def main():
print(f"Initialized!! PI: {Constants.PI.value}")
Constants.PI = "OVERWRITTEN"
if __name__ == "__main__":
main()
実行結果
constant_with_python (main) » uv run reassign_enum.py
Initialized!! PI: 3.14
Traceback (most recent call last):
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_enum.py", line 14, in <module>
main()
~~~~^^
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_enum.py", line 10, in main
Constants.PI = "OVERWRITTEN"
^^^^^^^^^^^^
File "/Users/shunsuke.tsuchiya/.local/share/uv/python/cpython-3.13.1-macos-aarch64-none/lib/python3.13/enum.py", line 835, in __setattr__
raise AttributeError('cannot reassign member %r' % (name, ))
AttributeError: cannot reassign member 'PI'
解説
Enumは関連する定数のグループを作成するためのクラスです。Enumのメンバーは自動的に変更不可になります。値にアクセスするには.valueを使います。
Pythonの場合、Enumは3.4で標準ライブラリに追加されているので、バージョンが低い環境でも使えるメリットがあります。
参照: PEP 435 – Adding an Enum type to the Python standard library
特殊メソッド setattr で実装
実装
class Constants:
def __init__(self):
self._constants = {}
def __setattr__(self, name, value):
if name == "_constants":
super().__setattr__(name, value)
return
if name in self._constants:
raise TypeError(f"Can not overwrite constant: {name}")
self._constants[name] = True
super().__setattr__(name, value)
def main():
constants = Constants()
constants.PI = 3.14
print(f"Initialized!! PI: {constants.PI}")
constants.PI = "OVERWRITTEN"
if __name__ == "__main__":
main()
実行結果
constant_with_python (main) » uv run reassign_class.py
Initialized!! PI: 3.14
Traceback (most recent call last):
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_class.py", line 25, in <module>
main()
~~~~^^
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_class.py", line 21, in main
constants.PI = "OVERWRITTEN"
^^^^^^^^^^^^
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_class.py", line 11, in __setattr__
raise TypeError(f"Can not overwrite constant: {name}")
TypeError: Can not overwrite constant: PI
解説
この実装は力技ですが、import無しで機能します。
Pythonには「特殊メソッド」(マジックメソッド、ダンダーメソッド)と呼ばれる、__名前__
の形式のメソッドがあります。__setattr__は属性(変数)に値を設定するときに必ず呼ばれる特殊メソッドです。この例では次のように動作します。
- クラスは特別な内部辞書
_constants
を持っていて、各属性が定数として登録されたかどうかを記録 - 任意の属性に値を代入するとき、以下の手順が実行
a. 属性名が_constants
の場合は、通常通り値が設定
b. 属性名がすでに_constants
辞書に登録されている場合は、定数として登録済みであると判断し、上書きしようとするとTypeError
を発生
c. 新しい属性の場合、まずその名前を_constants
辞書に追加し、その後に__setattr__
で属性値を設定
初期化の際は、aが呼ばれ、新しい定数であれば、cが呼ばれるので正常に終了します。
constants = Constants() # 初期化
constants.PI = 3.14 # 新しい定数
一方で、再代入の際は、bが呼ばれるのでエラーになります。
参考: PEP 726 – Module setattr and delattr
補足
ちなみに、この方法は直接操作を避けれます。
constants = Constants()
constants.PI = 3.14
print(f"Initialized!! PI: {constants.PI}")
constants.__setattr__("PI", "OVERWRITTEN")
print(f"Overwritten!! PI: {constants.PI}")
constant_with_python (main*) » uv run reassign_class.py
Initialized!! PI: 3.14
Traceback (most recent call last):
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_class.py", line 26, in <module>
main()
~~~~^^
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_class.py", line 21, in main
constants.__setattr__("PI", "OVERWRITTEN")
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_class.py", line 11, in __setattr__
raise TypeError(f"Can not overwrite constant: {name}")
TypeError: Can not overwrite constant: PI
辞書の直接操作は、エラーにはならないものの、値は変えられません。
constants = Constants()
constants.PI = 3.14
print(f"Initialized!! PI: {constants.PI}")
constants._constants["PI"] = "OVERWRITTEN"
print(f"Overwritten!! PI: {constants.PI}")
constant_with_python (main*) » uv run reassign_class.py
Initialized!! PI: 3.14
Overwritten!! PI: 3.14
なぜなら、辞書とattributeは別物だからです。(辞書はattributeが登録済みかという情報のみ持っている)
self._constants[name] = True
__slots__とプロパティ で実装 (非推奨)
実装
class Constants:
__slots__ = "_pi"
def __init__(self):
self._pi = 3.14
@property
def PI(self):
return self._pi
def main():
constants = Constants()
print(f"Initialized!! PI: {constants.PI}")
constants.PI = "OVERWRITTEN"
if __name__ == "__main__":
main()
実行結果
constant_with_python (main) » uv run reassign_slot.py
Initialized!! PI: 3.14
Traceback (most recent call last):
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_slot.py", line 19, in <module>
main()
~~~~^^
File "/Users/shunsuke.tsuchiya/Hobby/constant_with_python/reassign_slot.py", line 15, in main
constants.PI = "OVERWRITTEN"
^^^^^^^^^^^^
AttributeError: property 'PI' of 'Constants' object has no setter
解説
__slots__は特殊変数で、クラスのインスタンスが持つことができる属性を制限します。@propertyはデコレータの一種で、メソッドをプロパティ(属性のように扱えるメソッド)に変換します。
この例では:
- 実際の値は_piという内部変数に保存されています
- constants.PIのように属性のようにアクセスできますが、裏ではメソッドが呼び出されます
ただし、内部変数に直接アクセスされると上書きできてしまうのでお勧めしません。
constants = Constants()
print(f"Initialized!! PI: {constants.PI}")
constants._pi = "OVERWRITTEN"
print(f"Overwritten!! PI: {constants.PI}")
constant_with_python (main*) » uv run reassign_slot.py
Initialized!! PI: 3.14
Overwritten!! PI: OVERWRITTEN
まとめ
mypyを使わずにPythonで定数で作る場合、enum
かdataclass
を使うと良いでしょう。何にも依存したくない場合は自分で__setattr__
を書くと定数に近い挙動を実現できます。
あとがき
今回の記事内容をまとめたRepositoryを作りました。よければ、Star押していってください!!
Discussion