一覧PG-11-3 · ファイルと外部データ

設定ファイル・環境変数・起動引数の優先順位

この演習では既定値→JSONファイル→環境変数→起動引数の順に上書きします。ポートと真偽値を使い、最終値の由来と不正入力を反映しない手順を確認します。

説明

同じ設定値を既定値、ファイル、環境変数、起動引数から受け取る場合、どの値を優先するかを決める必要があります。環境変数は文字列なので、型と範囲の検査も必要です。

この演習では既定値→JSONファイル→環境変数→起動引数の順に上書きします。ポートと真偽値を使い、最終値の由来と不正入力を反映しない手順を確認します。

1既定値とファイルの設定

既定はポート8000・debug=Falseです。settings.jsonには8080・debug=trueを用意しています。

from pathlib import Path; import json; defaults = {"port": 8000, "debug": False}既定値; file_values = json.loads(Path("settings.json").read_text(encoding="utf-8"))ファイルを解析; config = defaults | file_valuesファイルを優先して統合; print(defaults, file_values, config)各設定元と候補

辞書の|では右側の同名キーが優先するため、最終候補は8080・Trueです。defaults自体は変更されません。この優先順位はPythonが全設定に強制するものではなく、このプログラムの設計です。

2環境変数の文字列を取得する

この演習で使う二つの環境変数へ固定値を設定します。読んだ値の型も表示します。

import os; os.environ["LBOX_LESSON_PORT"] = "9000"; os.environ["LBOX_LESSON_DEBUG"] = "false"固定した演習用の環境変数; port_text = os.environ["LBOX_LESSON_PORT"]; debug_text = os.environ["LBOX_LESSON_DEBUG"]値を取得; print(port_text, debug_text, type(port_text).__name__)値と型

ポートも真偽値もstrです。JSONから取得した整数やboolと同じ表記に見えても、環境変数をそのまま統合すると型が変わってしまいます。

3文字列falseをboolへ渡しても偽にはならない

Pythonの真偽判定と、設定文字列の解釈を比較します。

print(bool(debug_text)空でない文字列の真偽, debug_text.casefold() == "true"内容を比較する例)

空でない文字列falseはboolではTrueです。文字列の内容を解釈する条件が必要です。ただし単にtrue以外を全てFalseにすると、タイプミスまで無効設定として受け入れてしまいます。

4許可する真偽の表記を限定する

trueとfalseだけを受け入れ、それ以外はエラーにする変換関数を定義します。

def parse_bool(text): value = text.casefold()文字の大小をそろえる if value not in ("true", "false"):許可した二値だけ raise ValueError("expected true or false")誤った表記を拒否 return value == "true"boolへ変換

大文字・小文字はcasefoldでそろえますが、それ以外の表記は許可していません。受け入れる表記を増やすかどうかは、設定形式の仕様として決めます。

5型を変換して環境変数を反映する

ポートをint、debugをboolへ変換してから上書きします。

environment_values = {"port": int(port_text), "debug": parse_bool(debug_text)}環境変数を型変換; config = config | environment_values環境変数を優先して統合; print(environment_values, config)変換後と現在の設定

9000・Falseへ変わります。文字列のまま上書きせず、設定の型へ変換してから反映しています。

6起動引数を最優先にする

--port 9100を起動引数として解析します。省略した引数は辞書へ入れない指定にします。

import argparse; parser = argparse.ArgumentParser(); action = parser.add_argument("--port", type=int, default=argparse.SUPPRESS)整数型と省略時の扱い; arguments = vars(parser.parse_args(["--port", "9100"])固定した起動引数を解析); config = config | arguments起動引数を最優先; print(arguments, config)反映後の値; print(vars(parser.parse_args([])))引数を省略した場合

起動引数はportだけを上書きし、debugは環境変数由来のFalseを保ちます。省略時は空辞書なので、意図せず既定のNoneでファイルや環境の値を消すことを防げます。

7最終値の由来を確認

各層の辞書を優先度順に並べ、最後にそのキーを持つ層を探します。

layers = [("default", defaults), ("file", file_values), ("environment", environment_values), ("argument", arguments)]低い優先度から並べる; print({key: next(name for name, values in reversed(layers) if key in values)最後に値を指定した層 for key in config各設定値の由来})

portはargument、debugはenvironmentです。現在値だけでなく由来を残すと、ファイルを変更しても値が変わらない場合に、上位の設定が効いていることを確認できます。

8反映前に値の範囲も検査する

ポートが1〜65535の整数か検査してから、候補を返す関数を定義します。

def validate(candidate): port = candidate["port"]最終候補を取得 if type(port) is not int or not 1 <= port <= 65535型と範囲を検査: raise ValueError("port must be 1..65535")範囲外を拒否 return candidate検証済み候補を返す

intへ変換できることと、有効なポート範囲であることは別です。ここでは0を自動割当てに使う仕様ではなく、1〜65535だけを許可しています。

9不正な候補を現在値へ入れない

ポート70000の候補を検査し、その結果だけをconfigへ代入しようとします。

config = validate(config | {"port": 70000})候補を検査してから代入

ValueErrorで右辺が終わり、configへの代入は行われません。設定を直接部分更新してから検査するより、候補を作って検査し、成功後に採用する順番が分かりやすくなります。

10以前の有効な設定が残るか

configを表示し、真偽値のタイプミスも変換関数へ渡します。

print(config)未変更の有効設定; parse_bool("flase")誤記の拒否

configは9100・Falseのままです。flaseはFalseへ黙って変換されずValueErrorになります。優先順位、型変換、範囲検査を明示することで、設定の誤りを採用前に検出できます。

まとめ

設定元の優先順位と、最終値の由来を確認します。

文字列を意図する型へ変換し、候補を検査してから反映します。

操作履歴と解説を振り返れます。

Python 3

1
from pathlib import Path; import json; defaults = {"port": 8000, "debug": False}; file_values = json.loads(Path("settings.json").read_text(encoding="utf-8")); config = defaults | file_values; print(defaults, file_values, config)
{'port': 8000, 'debug': False} {'port': 8080, 'debug': True} {'port': 8080, 'debug': True}
2
import os; os.environ["LBOX_LESSON_PORT"] = "9000"; os.environ["LBOX_LESSON_DEBUG"] = "false"; port_text = os.environ["LBOX_LESSON_PORT"]; debug_text = os.environ["LBOX_LESSON_DEBUG"]; print(port_text, debug_text, type(port_text).__name__)
9000 false str
3
print(bool(debug_text), debug_text.casefold() == "true")
True False
4
def parse_bool(text): value = text.casefold() if value not in ("true", "false"): raise ValueError("expected true or false") return value == "true"
5
environment_values = {"port": int(port_text), "debug": parse_bool(debug_text)}; config = config | environment_values; print(environment_values, config)
{'port': 9000, 'debug': False} {'port': 9000, 'debug': False}
6
import argparse; parser = argparse.ArgumentParser(); action = parser.add_argument("--port", type=int, default=argparse.SUPPRESS); arguments = vars(parser.parse_args(["--port", "9100"])); config = config | arguments; print(arguments, config); print(vars(parser.parse_args([])))
{'port': 9100} {'port': 9100, 'debug': False}
{}
7
layers = [("default", defaults), ("file", file_values), ("environment", environment_values), ("argument", arguments)]; print({key: next(name for name, values in reversed(layers) if key in values) for key in config})
{'port': 'argument', 'debug': 'environment'}
8
def validate(candidate): port = candidate["port"] if type(port) is not int or not 1 <= port <= 65535: raise ValueError("port must be 1..65535") return candidate
9
config = validate(config | {"port": 70000})
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "<console>", line 4, in validate
ValueError: port must be 1..65535
10
print(config); parse_bool("flase")
{'port': 9100, 'debug': False}
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "<console>", line 4, in parse_bool
ValueError: expected true or false

演習完了