Python|JSON、TOML形式のコンフィグファイルをコーディングしやすく読み込む方法
Python標準ライブラリを使ってJSONやTOML(Python3.11~)形式のファイルを読み込んだ場合は、dict型で読み込まれます。読み込んだ内容の値を取得するときは、下の様にコーディングします。
value1 = config["key1"]
# もしくは
value1 = config.get("key1")
今回は下の様にコンフィグ変数.キー名
で設定値を取得する方法について説明します。
value1 = config.key1
"key1"
のような文字列キーの指定が不要になりますので、スペルミスを防いだり、カッコや囲み文字分コード量が減ります。
コンフィグの値を取得する箇所は多くなりがちですので、その分、効果が期待できます。
2つの実装方法(2024/02/12追記)
もともとこの記事では「拡張したdictにコンフィグファイルを読み込むパターン」のみを紹介していましたが、「dataclassにコンフィグファイルを読み込むパターン」を追記しました。2つのメリット・デメリットは下の通りです。
拡張したdictにコンフィグファイルを読み込むパターン
【メリット】
- コンフィグの構成を変更する場合は、コンフィグファイルの変更のみで完了する。
【デメリット】
- 使用箇所はVSCodeなどの参照先検索でヒットしないため、使用箇所の調査がしづらい。
dataclassにコンフィグファイルを読み込むパターン
【メリット】
- 変数を代入不可(イミュータブル)にできる。
- VSCodeなどの参照先検索で使用箇所がヒットする。
- VSCodeなどで変数のコード補完機能が利用できる。
【デメリット】
- コンフィグの構成を変更する場合は、コンフィグファイルの変更とdataclassの2か所の修正が必要。
基本的には、コンフィグの構成を変更したときの手間が少し増えますが、「dataclassにコンフィグファイルを読み込むパターン」がいいと思います。
後々、コンフィグキーの名前を変更したり、削除したりするときに、VSCodeなどの参照先検索ができるのはとても優位になります。コードが大規模になるほど、この優位性は大きくなります。
拡張したdictにコンフィグファイルを読み込むパターン
コンフィグを読み込むデータ型をdictから、dictを継承したオリジナルクラスのデータ型に変更します。オリジナルクラスのコードはとても簡単で下の通りです。
class ConfigDict(dict):
__getattr__ = dict.get
何をしているのか説明します。__getattr__
は存在しない属性を呼び出した場合に実行されるObjectの特殊メソッドで、上の様にすると値を取得するdict.get
の処理を実行するようになります。つまり属性でdictのキーを指定し、値が取得できるようになります。
例えばconfig.get("key1")
とconfig.key1
で同じ値が取得可能です。
補足として、存在しないコンフィグキーにアクセスした場合は例外が発生せず、None
が返却されます。また、get
やcopy
などdictに存在する属性をコンフィグのキーにすることはできません。
JSONの場合のコード
JSON形式のコンフィグファイルを読み込むコードは下の通りです。json.load
関数のobject_hook
オプションにConfigDict
を指定することで、dict型の代わりにConfigDict型でファイルを読み込むようにしています。
import json
class ConfigDict(dict):
"""
__getattr__ は存在しない属性を呼び出した場合に実行されるObjectの
特殊メソッドで、下の様にするとdict.getの処理を実行する。
これにより属性でdictのキーを指定でき、値が取得できるようになる。
例 config.get("key1") と config.key1 で同じ値が取得可能。
"""
__getattr__ = dict.get
def get_json_config(config_path):
"""JSON形式のコンフィグファイルを読込み、ConfigDict型で返す"""
with open(config_path) as f:
return json.load(f, object_hook=ConfigDict)
TOMLの場合のコード
TOML形式のコンフィグファイルを読み込むコードは下の通りです。JSONのload
関数と異なりdict型の代わりを設定するオプションはないため、一度dict型で読み込みConfigDict型に変換するようにしています。変換する際は多層のコンフィグ設定でも対応できるよう、再帰的に変換処理を行うようにしています。
import tomllib
class ConfigDict(dict):
"""
__getattr__ は存在しない属性を呼び出した場合に実行されるObjectの
特殊メソッドで、下の様にするとdict.getの処理を実行する。
これにより属性でdictのキーを指定でき、値が取得できるようになる。
例 config.get("key1") と config.key1 で同じ値が取得可能。
"""
__getattr__ = dict.get
def _dict_to_config_dict(target):
"""targetを再帰的に探索し、dict型であればConfigDict型に変換する"""
if isinstance(target, dict):
for key, value in target.items():
target[key] = _dict_to_config_dict(value)
if isinstance(target, list):
for index, item in enumerate(target):
target[index] = _dict_to_config_dict(item)
if isinstance(target, dict):
target = ConfigDict(target)
return target
def get_toml_config(config_path):
"""TOML形式のコンフィグファイルを読込み、ConfigDict型で返す"""
with open(config_path, mode="rb") as f:
return _dict_to_config_dict(tomllib.load(f))
dataclassにコンフィグファイルを読み込むパターン
例として、下の階層構造を持つJSON形式のコンフィグファイルを読み込む場合で説明します。
{
"key01": "AAA",
"key02": "BBB",
"key03": "CCC",
"child_object": {
"child01": 1,
"child02": 2,
"child03": 3,
"object_list": [
{
"list_key01": "aaa",
"list_key02": "bbb",
"list_key03": "ccc"
},
{
"list_key01": "ddd",
"list_key02": "eee",
"list_key03": "fff"
}
]
}
}
コードはこちらです。@dataclass(frozen=True)
をクラスにつけることでイミュータブルなdataclassにしています。子要素をそのまま読み込むとdict型になりますので、__post_init__
関数で子要素のdataclassに変換しています。Config(**json_dict)
でdictをアンパックしてConfigクラスに渡しますので、コンフィグファイルのキーとdataclassの変数名を一致させる必要があります。
import json
import tomllib
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
@dataclass(frozen=True)
class ChildObject:
@dataclass(frozen=True)
class ObjectList:
list_key01: str
list_key02: str
list_key03: str
child01: int
child02: int
child03: int
object_list: list[ObjectList]
def __post_init__(self):
"""
子の要素の配列はdictのlistで読み込まれるため、
ループしてdataclassに変換する
"""
object.__setattr__(
self,
"object_list",
[self.ObjectList(**sub_value) for sub_value in self.object_list],
)
key01: str
key02: str
key03: str
child_object: ChildObject
def __post_init__(self):
"""
子の要素はdict型で読み込まれるため、
dataclassに変換する
"""
object.__setattr__(
self,
"child_object",
self.ChildObject(**self.child_object)
)
def read_config_json():
""" JSON形式のコンフィグファイルを読み込む """
with open("config.json", "rb") as f:
json_dict = json.load(f)
return Config(**json_dict)
def read_config_toml():
""" TOML形式のコンフィグファイルを読み込む """
with open("config.toml", "rb") as f:
toml_dict = tomllib.load(f)
return Config(**toml_dict)
使用した場合はこのようになります。
config = read_config_json()
print(config.key01)
# AAA
print(config.child_object.child01)
# 1
print(config.child_object.object_list[0].list_key01)
# aaa
# JSON形式のコンフィグファイルと同じ構造をもつ、TOMLファイルを読み込んだ場合
config = read_config_toml()
print(config.key01)
# AAA
print(config.child_object.child01)
# 1
print(config.child_object.object_list[0].list_key01)
# aaa
参考
この記事は以下の情報を参考にしました。
- Python Documentation json --- JSON エンコーダおよびデコーダ
- Python Documentation tomllib --- TOMLファイルの解析
- Python Documentation 3. データモデル
- [詳解] Pythonのdataclasses
- Pythonのdataclassをdictやjsonと相互に変換する方法を解説!
更新履歴
- 2023/09/09 初版
- 2024/02/12 「dataclassにコンフィグファイルを読み込むパターン」を追記