yiskw note

機械学習やプログラミングについて気まぐれで書きます

【Python】定数管理のための、値の追加・変更を不可にするクラスを実装する


概要

機械学習の実験を回していると、しばしば複数の定数を管理したくなることがあります。(例えば、実験を回すマシンごとのデータセットのパスなど) Pythonでは、他のプログラミング言語constのような定数がサポートされていません。 PEP8の慣例に従って、大文字とアンダースコアで命名して定数を表す変数を作成したとしても、 それらは基本的に外部から書き換え可能で、意図しない変数の変更が起きてしまいます。

そこで今回は、Pythonで定数を管理するためのクラスを作成してみました。 その定数はクラス変数として管理され、クラス変数の追加や変更はできないようになっております。 そのため、定数の意図しない書き換えは発生せず、バグを防ぐことが可能です。 もし他に良い方法があれば、コメントにて教えていただけると幸いです。

実装の方針

定数を管理するためのクラスを作成し、そのクラス変数の追加や変更を制限することで、定数の書き換えを防ぎます。 その際に、特殊メソッドである __setattr__を上書きすることで、クラス変数の書き込みの挙動を制限します。 特殊メソッドに関しては、以下の記事がわかりやすいので、よければご参照ください。

qiita.com

さらに今回は、複数の定数クラスを作成したいので、上記の__setattr__の処理をメタクラスに実装します。 メタクラスについては、以下の記事をご参照ください。

astropengu.in

実装

上記の方針をもとに実装したものが以下です。

class ConstantMeta(type):
    """定数を管理するクラスのためのメタクラス。
    クラス変数の上書きや新たなクラス変数の追加をできないようにする。
    """

    # クラスが初期化されたどうかを表す変数
    _initialized = False

    def __setattr__(cls, name, value):
        if cls._initialized:
            if name in cls.__dict__:
                raise ValueError(f"{name} is a read-only property")
            else:
                raise AttributeError("Cannot add new attribute to Constants class")
        super().__setattr__(name, value)

    def __init__(cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        cls._initialized = True


class SampleConstants(metaclass=ConstantMeta):
    constant1 = 1
    constant2 = 2


if __name__ == "__main__":
    # 使用例
    print(SampleConstants.constant1)  # 1 を出力
    print(SampleConstants.constant2)  # 2 を出力

    # SampleConstants.constant1 = 5  # ValueError: onstant1 is a read-only property
    # SampleConstants.constant2 = 7  # ValueError: constant2 is a read-only property
    # SampleConstants.constant3 = 3  # AttributeError: Cannot add new attribute to SampleConstants class

ConstantMetaメタクラスとして指定してクラスを作成することで、 使用例のように定数の追加、変更した際にエラーを吐くようにできます。

また、メタクラスを使用しているので、定数管理用のクラスを簡単に量産することができ、 例えばマシンごとの定数を管理したい場合は、以下にします。

class LocalMachineConstants(metaclass=ConstantMeta):
    data_path = "/home/xxx/yyy"
    anno_path = "/home/aaa/bbb"


class RemoteMachineConstants(metaclass=ConstantMeta):
    data_path = "/data/zzz"
    anno_path = "/anno/ccc"

dataclassとの違い

データ管理の方法の一つに、Pythonの標準ライブラリで提供されているdataclasses.dataclassというものもあり、 こちらを定数管理として使用する、ということも考えられます。

from dataclasses import dataclass


@dataclass(frozen=True)
class ExampleConsts:
    constant1: int = 1
    constant2: int = 2


const = ExampleConsts()

print(const.constant1) # 1 を出力
print(const.constant2) # 2 を出力
const.constant3 = 3 # dataclasses.FrozenInstanceError: cannot assign to field 'constant3'

しかし、こちらは、変数の追加や変更を禁止するために、一度ExampleConstsクラスをインスタンス化する必要があります。 ExampleConstsクラスをインスタンス化することなく使用しようとすると、変数の変更時や追加時にエラーが吐かれません。

ExampleConsts.constant1 = 1000
ExampleConsts.constant3 = 2000
print(ExampleConsts.constant1) # 1000 と出力。値の上書きができてしまっている。
print(ExampleConsts.constant3) # 2000 と出力。変数の追加ができてしまっている。

一方、クラス変数を用いた方法は、定数をクラス変数として管理しているので、インスタンス化することなく、使用することが可能です。

参考