🐍

Pythonで定数を扱う方法 (mypy無し版)

2025/03/08に公開

前回のあらすじ

以前、Pythonで定数を作成するときは、mypyのFinalを使うと良いという記事を書きました。(定数関数は避ける)

https://zenn.dev/shundeveloper/articles/ede53caa9632f5

とはいえ、所属組織によっては、既に大きいコードがあり、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.

https://peps.python.org/pep-0557/#frozen-instances

列挙型(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

https://peps.python.org/pep-0435/

特殊メソッド 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__は属性(変数)に値を設定するときに必ず呼ばれる特殊メソッドです。この例では次のように動作します。

  1. クラスは特別な内部辞書 _constants を持っていて、各属性が定数として登録されたかどうかを記録
  2. 任意の属性に値を代入するとき、以下の手順が実行
    a. 属性名が _constants の場合は、通常通り値が設定
    b. 属性名がすでに _constants 辞書に登録されている場合は、定数として登録済みであると判断し、上書きしようとすると TypeError を発生
    c. 新しい属性の場合、まずその名前を _constants 辞書に追加し、その後に__setattr__で属性値を設定

初期化の際は、aが呼ばれ、新しい定数であれば、cが呼ばれるので正常に終了します。

constants = Constants() # 初期化
constants.PI = 3.14 # 新しい定数

一方で、再代入の際は、bが呼ばれるのでエラーになります。

参考: PEP 726 – Module setattr and delattr
https://peps.python.org/pep-0726/

補足

ちなみに、この方法は直接操作を避けれます。

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で定数で作る場合、enumdataclassを使うと良いでしょう。何にも依存したくない場合は自分で__setattr__を書くと定数に近い挙動を実現できます。

あとがき

今回の記事内容をまとめたRepositoryを作りました。よければ、Star押していってください!!

https://github.com/shunsock/constant_with_python

Discussion