pathlib、JSON/YAML 設定、環境変数
pathlib でどのフォルダからでも動くパス、ネストした JSON と YAML の設定、.env と環境変数。そして値が不正なときに起動の時点で止まる設定ローダー。
どうやって学びますか?
解決する問題
サポートボットのプロジェクトには、config/app.json を読み込んでモデル名と temperature を表示する小さなスクリプト scripts/show_config.py があります。作者はいつもプロジェクトのルートに立って python scripts/show_config.py を実行し、問題なく動いていました。ある日、同僚が scripts/ フォルダに入ってから同じスクリプトを実行すると、FileNotFoundError が出ました。ファイルは相変わらずいつもの場所にあったのです。
同じ週に、さらに 2 つのことが起きました。サーバーでは APP_DEBUG=false が設定されているのに、ログではデバッグモードがオンになっていました。また、誰かがモデルの回答を毎回同じにするために設定ファイルへ temperature: 0 と書きましたが、プログラムは 0.7 で動いていました。どれもエラーは出なかったのです。
3 つのどれも、コードのロジックの問題ではありません。問題は、プログラムが外の世界(ファイルシステム、設定ファイル、環境変数)をどう読むかにあります。この章では、その読み込みを確実にします。pathlib によるパス、JSON と YAML によるネストした設定、.env による秘密情報を扱い、それぞれに検証を付けます。そうすれば、不正な設定は黙って実行されるのではなく、起動の時点で止まります。
この章を終える頃には
os.pathの文字列をつなげる代わりに、/とpathlib.Pathを使って、どの OS でも動くパスを作れる- ターミナルがどのフォルダにいるかに関係なく、スクリプト自身の場所からファイルを探せる
- ネストした JSON と YAML の設定を読め、
yaml.safe_loadが型を黙って変えてしまう場合を見分けられる - 環境変数と
.envファイルから値を読み、正しい型に変換して検証できる - 値が不正または欠けているときに、はっきりしたメッセージで止まる設定ローダーを書ける
前提条件: pyproject.toml、ロックファイル、src/ レイアウト — 特に uv add と uv run。
pathlib: パスは文字列ではなくオブジェクト
古い Python のコードでは、パスは文字列でした。+ "/" + や os.path.join でつなぎ合わせていたのです。pathlib モジュールの Path クラスは、パスを、その部品を取り出せるオブジェクトとして扱います。
from pathlib import Path
config_file = Path("config") / "models" / "chat.yaml"
print(config_file.as_posix())
print(config_file.name)
print(config_file.stem)
print(config_file.suffix)
print(config_file.parent.as_posix())
print(config_file.with_suffix(".json").as_posix())
print(config_file.parts)config/models/chat.yaml
chat.yaml
chat
.yaml
config/models
config/models/chat.json
('config', 'models', 'chat.yaml')ここでの / は割り算ではありません。Path と一緒に使うと、パスの部品をつなぎ、その OS に合った区切り文字を選びます。出力を .as_posix() で表示しているので、どのマシンでも同じに見えます。print(config_file) と書くと、Windows では config\models\chat.yaml、macOS と Linux では config/models/chat.yaml と表示されます。
文字列をつなげると、その違いがはっきり分かります:
from pathlib import Path
base = "config"
print(base + "/" + "app.json")
print(Path(base) / "app.json")
print(repr(Path(base) / "app.json"))Windows では次のように表示されます:
config/app.json
config\app.json
WindowsPath('config/app.json')1 行目は手で組み立てた文字列です。Windows は通常 / を受け付けますが、そうしたパスを別のツールに渡したり、文字列として比べたりすると、2 種類の区切り文字が混ざって混乱を招きます。Path は、そのシステムの規則に従って各パスを組み立てます。macOS や Linux では、2 行目は config/app.json、3 行目は PosixPath('config/app.json') になります。
/は、どちらか一方がPathのときにだけ働きます。"config" / "app.json"では両方が文字列なので、Python はそれを割り算とみなし、TypeErrorを出します。左端をPathにすれば、残りは文字列のままで構いません。
日常の作業でよく使うメソッドをさらにいくつか挙げます。以下の例でも使っています:
path.exists()、path.is_file()、path.is_dir()— そのパスが存在するか、そして何であるか(ファイルかフォルダか)を調べるpath.read_text(encoding="utf-8")とpath.write_text(text, encoding="utf-8")—openを使わずに、ファイル全体を読み書きするpath.mkdir(parents=True, exist_ok=True)— 途中のフォルダも含めてフォルダを作る。すでに存在してもエラーにはならないfolder.glob("*.txt")— パターンに合うフォルダ内のファイルをすべて取り出す。rglobは下位のフォルダも検索するpath.resolve()— 相対パスを絶対パスに変換する
encoding="utf-8" は毎回書いてください。Windows では、Python の既定のエンコーディングが UTF-8 でないことが多いです。ベンガル語の文字や絵文字を含むプロンプトファイルは、Mac では正しく読めても、Windows では文字化けするか、UnicodeDecodeError になります。
相対パスは何を基準にするのか
この章の最初にあったスクリプトは、次のようになっています:
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))プロジェクトのルートから python scripts/show_config.py を実行すると、設定が表示されます。しかし scripts/ に入って python show_config.py を実行すると:
Traceback (most recent call last):
File "show_config.py", line 4, in <module>
print(config_path.read_text(encoding="utf-8"))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "...\Lib\pathlib.py", line 1028, in read_text
with self.open(mode='r', encoding=encoding, errors=errors) as f:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "...\Lib\pathlib.py", line 1014, in open
return io.open(self, mode, buffering, encoding, errors, newline)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'config\\app.json'(お使いのマシンでは、トレースバックにフルパスが表示されます。ここでは短くしてあります。)相対パスは、常にカレントワーキングディレクトリ、つまりターミナルがいる場所を基準に探されます。スクリプトファイルがあるフォルダからではありません。scripts/ の中には config/app.json がないので、ファイルは見つからなかったのです。
本番環境では、これはさらに起こりやすくなります。cron ジョブ、サービスマネージャー、コンテナが、プログラムをどのフォルダから起動するのかを、常に把握しているとは限らないからです。解決策は、パスをスクリプト自身の場所に結びつけることです。すべてのモジュールの中では、__file__ にそのファイルのパスが入っています:
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
config_path = PROJECT_ROOT / "config" / "app.json"
print(config_path.read_text(encoding="utf-8")){"model": "chat-small", "temperature": 0.2}これで、プロジェクトのルートから実行しても、scripts/ から実行しても、あるいはフルパスで指定してディスクの別の場所から実行しても、同じ行が表示されます。順に見ていきましょう。Path(__file__) は show_config.py です。.resolve() でそれを絶対パスにします。最初の .parent は scripts/、2 番目の .parent はプロジェクトのルートです。
.resolve()を省略しないでください。状況によっては__file__が相対パスになることがあり、その場合.parent.parentは、また現在のフォルダに依存してしまいます。避けたかった問題そのものです。
JSON: ネストした設定と、ゼロの罠
AI プロジェクトの設定は、ほぼ必ずネストしています。アプリの設定、モデルの設定、検索の設定が、別々のグループに分かれているのです。json.loads は JSON の文字列をネストした辞書に変換します(ファイルから読むなら json.loads(path.read_text(encoding="utf-8"))):
import json
raw = """
{
"app": {"name": "support-bot", "debug": false},
"llm": {"model": "chat-small", "temperature": 0, "max_tokens": 512},
"retrieval": {"top_k": 4}
}
"""
config = json.loads(raw)
print(config["llm"]["model"])
print(config["retrieval"]["top_k"])
print(type(config["app"]["debug"]))
temperature = config["llm"].get("temperature") or 0.7
print(temperature)
temperature = config["llm"].get("temperature", 0.7)
print(temperature)chat-small
4
<class 'bool'>
0.7
0JSON の false は Python の False になり、数値は数値のまま残ります。JSON では型が保たれるのです。しかし最後の 2 行を見てください。設定には明らかに "temperature": 0 と書かれているのに、最初の方法では 0.7 になりました。
x or default は、「x が真とみなされるならば x、そうでなければ default」という意味です。そして Python では、0、0.0、""、[]、{} はすべて偽とみなされます。そのため or には、「値がない」ことと「値があって、それがゼロである」ことを区別できません。temperature が 0 のとき、モデルは毎回もっとも確からしい答えを返します。これはテストや評価で必要な動作です。しかし or は、それを黙って 0.7 に変えてしまい、見えるのは実行のたびに答えが違う、という結果だけです。
既定値には.get(key, default)を使いましょう。これは、キーが本当に存在しないときにだけ既定値を使います。orが正しいのは、ゼロや空の値も捨ててよいと本当に考えている場合だけです。
YAML: 人には読みやすいが、型を推測する
多くのチームは設定を YAML で書きます。YAML はコメントを書けて、かっこや引用符が少なくて済むからです。Python には YAML を読む機能が標準では付いていないので、プロジェクトに追加します:
uv add pyyamlパッケージ名は pyyaml ですが、import するときは import yaml と書きます。では設定を読み込んでみましょう:
import yaml
raw = """
llm:
model: chat-small
temperature: 0.2
features:
streaming: yes
cache: off
python_version: 3.10
api_version: "3.10"
"""
config = yaml.safe_load(raw)
print(config["llm"])
print(config["features"])
print(config["python_version"], type(config["python_version"]).__name__)
print(config["api_version"], type(config["api_version"]).__name__){'model': 'chat-small', 'temperature': 0.2}
{'streaming': True, 'cache': False}
3.1 float
3.10 strJSON と違って、YAML では型を明示する必要がありません。パーサーが推測するのです。ほとんどの場合、推測は正しいのですが、2 か所だけ黙って間違えます。yes、no、on、off はブール値になります。ファイルには cache: off と書いたのに、得られるのは False です。そして 3.10 は、値が 3.1 の小数として読まれます。バージョン番号としては、まったく別物になってしまいます。引用符で囲んだ "3.10" は、文字列のままです。
ルールは単純です。数値や yes/no のように見えるけれど実際にはテキストである値、たとえばバージョン番号、ID、郵便番号は、YAML では引用符で囲みます。
そして必ず yaml.safe_load を使ってください。古いチュートリアルにある yaml.load(text) は、今では TypeError: load() missing 1 required positional argument: 'Loader' で必ず失敗します。この変更は意図的なものです。一部のローダーは、YAML ファイルの中の命令から任意の Python オブジェクトを作れてしまいます。そのため、信頼できない YAML ファイルを読むことは、他人のコードを実行することと同じになります。safe_load が作るのは、辞書、リスト、文字列、数値、ブール値、None といった単純な型だけです。
環境変数: すべてが文字列
API キー、データベースのアドレス、どのモデルを使うか。マシンごとに変わる値や、秘密にしておくべき値は、コードやコミットされた設定ファイルではなく、環境変数に置きます。Python は os.environ または os.getenv で環境変数を読みます。この例では、2 つの変数をコードの中で設定しています。実際の作業では、それらはシェル、サーバー、コンテナから渡されます:
import os
os.environ["APP_DEBUG"] = "false"
os.environ["APP_PORT"] = "8000"
debug = os.getenv("APP_DEBUG")
print(repr(debug), bool(debug))
if debug:
print("debug mode is ON")
port = os.getenv("APP_PORT")
print(port + "1")
print(int(port) + 1)
print(os.getenv("LLM_API_KEY"))
print(os.getenv("LLM_TIMEOUT", "30"))'false' True
debug mode is ON
80001
8001
None
30環境変数は常に文字列です。"false" は空でない 5 文字の文字列なので、if debug: はそれを真とみなします。冒頭のサーバーがデバッグモードで動いていたのは、まさにこのためです。同様に、"8000" + "1" は足し算ではなく連結になります。エラーは出ません。ただ、プログラムが誤った動作をするだけです。
ですから、環境変数を読んだら、すぐに正しい型に変換してください。数値なら、int() や float() で十分です。不正な入力の場合、これらは自分で ValueError を出します。ブール値については、Python には組み込みの変換がありません(bool("false") は True です)。そこで自分で関数を書きます:
def parse_bool(value: str) -> bool:
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
raise ValueError(f"not a boolean: {value!r}")
for raw in ["false", "True", " yes ", "0"]:
print(repr(raw), "->", parse_bool(raw))
parse_bool("flase")'false' -> False
'True' -> True
' yes ' -> True
'0' -> False
Traceback (most recent call last):
File "main.py", line 13, in <module>
parse_bool("flase")
File "main.py", line 7, in parse_bool
raise ValueError(f"not a boolean: {value!r}")
ValueError: not a boolean: 'flase'最後の行こそ、この関数の一番の価値です。"flase" と綴り間違いをしても、黙って True や False になることはありません。プログラムは起動の時点で止まり、理解できなかった値を教えてくれます。
os.environ["NAME"] と os.getenv("NAME") の違いも覚えておいてください。前者は変数がないと KeyError: 'LLM_API_KEY' を出します。後者は黙って None を返します。それが API 呼び出しで数分後に 401 Unauthorized として現れると、原因を見つけるのははるかに難しくなります。プログラムが動くために必ず必要な値は、起動の最初にチェックしてください。
.env ファイル: 自分のマシンのための環境
ターミナルを開くたびに 10 個の変数を設定するのは現実的ではありません。よくある解決策は、プロジェクトのルートに .env ファイルを置き、python-dotenv パッケージを使うことです。このパッケージはそのファイルを読み、値を os.environ に入れてくれます:
uv add python-dotenvここでもパッケージ名と import 名は異なります。パッケージは python-dotenv、import は dotenv です。.env ファイルは次のとおりです:
# Local secrets. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
LLM_MODEL=chat-small
APP_DEBUG=true(sk-test-placeholder-1234 は架空の値です。本物のキーを、ドキュメント、スクリーンショット、コミットに決して載せないでください。)それを読むコードは次のとおりです。ここでもパスはスクリプトの場所から組み立てています:
import os
from pathlib import Path
from dotenv import load_dotenv
env_file = Path(__file__).resolve().parent / ".env"
found = load_dotenv(env_file)
print(found)
print(os.getenv("LLM_MODEL"))
print(os.getenv("APP_DEBUG"))
print(os.getenv("LLM_API_KEY")[:7] + "...")uv run main.py を実行すると:
True
chat-small
true
sk-test...では同じプログラムを、今度はシェルで LLM_MODEL がすでに設定されている状態で実行します(macOS/Linux または Git Bash):
LLM_MODEL=chat-large uv run main.pyTrue
chat-large
true
sk-test...load_dotenv は、すでに設定されている変数を置き換えません。これは意図的で、重要な設計です。.env は自分のマシン用の便利な仕組みにすぎず、サーバーやコンテナに設定された本物の環境は、常に優先されます。(load_dotenv(path, override=True) を使えば順序を逆にできますが、そうしたい理由はほとんどありません。)
また、load_dotenv はファイルがなくても例外を出さず、ただ False を返します。ですから「.env は読み込まれた」と決めつけないでください。必要な値は、次の例のように、個別に確認しましょう。
プロの現場のリポジトリは、どれも次の 2 つのルールを守っています:
.envはコミットしません。.gitignoreに入れます。キーがいったん Git の履歴に入ると、ファイルを削除した後も履歴に残ります。唯一安全な対処は、そのキーを無効化して新しいキーを発行することです。.env.exampleはコミットします。 同じ変数名を、値を空にするか、無害な値にして含めます。新しく参加した同僚は、それを.envにコピーすれば、どの値が必要かがすぐに分かります。
完全な例
ここまでの内容をすべてまとめます。サポートボットの設定ローダーを作りましょう。プロジェクトは uv init --app --python 3.12 support-bot で作成し、それから uv add pyyaml python-dotenv を実行しました。構造は次のとおりです:
support-bot/
├── .env
├── .env.example
├── config/
│ └── app.yaml
├── prompts/
│ ├── summarise.txt
│ └── system.txt
├── pyproject.toml
├── settings.py
└── uv.lockconfig/app.yaml — コミットします。秘密情報は含みません:
app:
name: support-bot
debug: false
llm:
model: chat-small
temperature: 0.2
max_tokens: 512
retrieval:
top_k: 4.env — コミットしません:
# Local values for this machine. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
APP_DEBUG=true.env.example — コミットします:
# Copy to .env and fill in. This file is committed; .env is not.
LLM_API_KEY=
APP_DEBUG=falseprompts/system.txt には 1 行、You are a support assistant. Answer only from the provided context. と書きます。prompts/summarise.txt には Summarise the ticket below in two sentences. と書きます。
そして settings.py です:
import os
import sys
from pathlib import Path
import yaml
from dotenv import load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parent
CONFIG_FILE = PROJECT_ROOT / "config" / "app.yaml"
PROMPTS_DIR = PROJECT_ROOT / "prompts"
class ConfigError(Exception):
"""Configuration is missing or invalid."""
def parse_bool(value: str) -> bool:
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
raise ConfigError(f"not a boolean: {value!r}")
def load_yaml(path: Path) -> dict:
if not path.is_file():
raise ConfigError(f"config file not found: {path.relative_to(PROJECT_ROOT).as_posix()}")
data = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ConfigError(f"{path.name} must contain a mapping at the top level")
return data
def load_prompts(folder: Path) -> dict[str, str]:
return {path.stem: path.read_text(encoding="utf-8").strip() for path in sorted(folder.glob("*.txt"))}
def require_env(name: str) -> str:
value = os.getenv(name, "").strip()
if not value:
raise ConfigError(f"missing required environment variable: {name}")
return value
def load_settings() -> dict:
load_dotenv(PROJECT_ROOT / ".env")
config = load_yaml(CONFIG_FILE)
temperature = float(os.getenv("LLM_TEMPERATURE", config["llm"]["temperature"]))
if not 0 <= temperature <= 2:
raise ConfigError(f"LLM_TEMPERATURE must be between 0 and 2, got {temperature}")
debug = config["app"]["debug"]
if "APP_DEBUG" in os.environ:
debug = parse_bool(os.environ["APP_DEBUG"])
return {
"app_name": config["app"]["name"],
"debug": debug,
"model": os.getenv("LLM_MODEL", config["llm"]["model"]),
"temperature": temperature,
"top_k": int(config["retrieval"]["top_k"]),
"api_key": require_env("LLM_API_KEY"),
"prompts": load_prompts(PROMPTS_DIR),
}
def mask(secret: str) -> str:
return secret[:3] + "*" * 8
if __name__ == "__main__":
try:
settings = load_settings()
except ConfigError as exc:
print(f"config error: {exc}")
sys.exit(1)
for key in ["app_name", "debug", "model", "temperature", "top_k"]:
print(f"{key:<12} {settings[key]}")
print(f"{'api_key':<12} {mask(settings['api_key'])}")
print(f"{'prompts':<12} {', '.join(settings['prompts'])}")4 通りの実行です。通常の実行、シェルから 2 つの値を変更した実行、.env を脇へ移した実行、そして不可能な temperature を指定した実行です:
uv run settings.py
LLM_TEMPERATURE=0 LLM_MODEL=chat-large uv run settings.py
uv run settings.py
LLM_TEMPERATURE=5 uv run settings.pyapp_name support-bot
debug True
model chat-small
temperature 0.2
top_k 4
api_key sk-********
prompts summarise, system
app_name support-bot
debug True
model chat-large
temperature 0.0
top_k 4
api_key sk-********
prompts summarise, system
config error: missing required environment variable: LLM_API_KEY
config error: LLM_TEMPERATURE must be between 0 and 2, got 5.0(3 回目の実行の前に .env ファイルは名前を変えて退避させ、4 回目の前に元に戻しました。)最後の 2 回の実行は終了コード 1 で終わり、最初の 2 回は 0 で終わります。そのため、デプロイのスクリプトや CI は、設定が間違っていることを簡単に判断できます。
このローダーに込めた設計判断は次のとおりです:
すべてのパスは PROJECT_ROOT から作ります。 Path(__file__).resolve().parent を一度だけ計算し、ほかのすべてのパスはそこから組み立てます。プログラムをどのフォルダから起動しても、同じファイルを読みます。
優先順位を明示的に決めています。 まず YAML の既定値、その上に .env、そしてさらにその上に、シェルやサーバーの本物の環境があります。2 回目の実行では、ファイルを 1 行も編集せずにモデルと temperature が変わりました。本番環境で設定を 1 か所で変える方法がまさにこれです。
temperature 0.0 は残ります。 os.getenv("LLM_TEMPERATURE", ...) は、変数が本当に存在しないときだけ YAML の値にフォールバックします。どこにも or はありません。同様に debug は、値が真かどうかではなく、"APP_DEBUG" in os.environ を確認しています。
型変換と範囲チェックは 1 か所にまとめています。 float()、int()、parse_bool()、そして temperature の 0 <= temperature <= 2 はすべて load_settings の中にあります。プログラムの残りの部分は、settings["temperature"] が正しい数値であると前提にできます。
専用の ConfigError を用意しています。 設定に関する問題はすべて 1 種類の例外として扱われ、__main__ ブロックがそれを、どの値を直せばよいかが分かる 1 行のはっきりしたメッセージに変えます。トレースバックは表示されません。
秘密情報は、全文が表示されることはありません。 mask は最初の 3 文字だけを表示するので、どの種類のキーが読み込まれたかは分かりますが、キー自体がログに出ることはありません。マスクの長さも固定されているので、キーの長さから何かを推測することもできません。
glob でプロンプトファイルを読みます。 新しい .txt を prompts/ に入れると、コードを変えなくても settings["prompts"] に現れます。sorted() を使っているのは、glob がファイルを返す順序がオペレーティングシステムによって異なるからです。
ひとつ制限があります。load_settings は単なる辞書を返すので、settings["tempreature"] のような綴りの間違いは、実行時の KeyError としてしか捕まえられません。また、新しい設定を増やすたびに、その変換と検査を手で書く必要があります。宣言的にこれを行うツールは、このコースで後ほど扱います。ここで示した原則、つまり 1 か所で読み込むこと、正しい型を使うこと、起動の時点で止めることは、そのツールを使っても変わりません。
うまくいかないとき
FileNotFoundError: [Errno 2] No such file or directory: 'config\\app.json' パスが相対パスで、プログラムが別のフォルダから起動されています。Path(__file__).resolve().parent からパスを組み立ててください。検索がどのフォルダから始まっているかを確かめるには、Path.cwd() を表示します。
TypeError: unsupported operand type(s) for /: 'str' and 'str' "config" / "app.json" は、両側が文字列になっています。左端を Path にしてください:Path("config") / "app.json"。
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 36 (char 35) JSON の最後の項目の後ろに末尾のカンマがある({"top_k": 4,})か、キーがシングルクォートで囲まれています。Python の辞書はどちらも受け付けますが、JSON は受け付けません。エラーメッセージの列番号は、パーサーがどこで止まったかを正確に示しています。
TypeError: load() missing 1 required positional argument: 'Loader' 古いコードが yaml.load(text) を呼んでいます。代わりに yaml.safe_load(text) と書いてください。
KeyError: 'LLM_API_KEY' コードが os.environ["LLM_API_KEY"] を読んでいますが、変数が設定されていません。.env がプロジェクトのルートにあるか、load_dotenv に正しいパスが渡されているか、そして変数名がすべての場所で同じ綴りになっているかを確認してください。値がないことを分かりやすく伝えるには、上の require_env のような関数を使います。
ModuleNotFoundError: No module named 'dotenv' python-dotenv がプロジェクトに追加されていないか、uv run を使わずにプログラムを実行したため、プロジェクトの環境が使われませんでした。uv add python-dotenv を実行し、プログラムは uv run で起動してください。
ステップ 4 / 6 — 予想する
理解度をチェック
サーバーでは APP_DEBUG=false が設定されています(ここではコードの中で設定して示しています)。何が表示されるでしょうか?
import os
os.environ["APP_DEBUG"] = "false"
debug = os.getenv("APP_DEBUG")
if debug:
print("debug on")
else:
print("debug off")- A`debug on`
- B`debug off`
- C`False`
- D`KeyError: 'APP_DEBUG'`
評価の実行のため、設定では temperature を 0 にして、モデルが毎回同じように答えるようにしています。しかし回答は実行のたびに異なり、エラーも出ません。バグはどこにあるでしょうか?
config = {"llm": {"model": "chat-small", "temperature": 0}}
temperature = config["llm"].get("temperature") or 0.7
print(f"calling {config['llm']['model']} at temperature {temperature}")- A`0` は偽とみなされるため `or` がそれを捨てて `0.7` を採用する。`config["llm"].get("temperature", 0.7)` にすべきである
- B`.get("temperature")` はネストした辞書に対して動かないので、`config["temperature"]` と書かなければならない
- Cf 文字列の中でシングルクォートを使うと、`temperature` の値が変わってしまう
- DJSON の `0` は、実は文字列 `"0"` なので、比較が失敗する
このコードは scripts/show_config.py にあり、設定は config/app.json としてプロジェクトのルートにあります。ルートから実行すると動きますが、scripts/ から実行すると FileNotFoundError になります。最善の修正は何でしょうか?
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))- Aパスを `"../config/app.json"` と書く
- B`config/app.json` のコピーを `scripts/` フォルダの中にも置いておく
- C`Path(__file__).resolve().parent.parent / "config" / "app.json"` — スクリプト自身の場所からパスを組み立てる
- Dスクリプトはプロジェクトのルートからしか実行してはいけない、とみんなに伝える
プレイは Pro の機能です
+30 XP が待っています
Pro でこの章をプレイ
読むのは無料で、章はすべて公開されています。無料コースでは、最初の 5 章を無料でプレイできます。第 6 章からは、問題への回答、XP の獲得、連続記録の更新が Pro の機能になります。
Pro でプレイを解放やってみよう
ベースの設定の上に、環境ごとの設定を重ねて読み込む設定ローダーを書いてみましょう。uv init --app --python 3.12 config-lab でプロジェクトを作り、uv add python-dotenv を実行してから、次のファイルを作ってください:
config/base.json:{"llm": {"model": "chat-small", "temperature": 0.2}, "retrieval": {"top_k": 4}}config/staging.json:{"llm": {"temperature": 0}, "retrieval": {"top_k": 8}}.env: 1 行だけ。APP_ENV=staging
main.py を次のように書いてください:
- すべてのパスを
Path(__file__).resolve().parentから組み立てて、どのフォルダから実行しても同じ結果になるようにする APP_ENVは環境変数か.envから取得し、指定がなければdevを既定値にする。ファイルのない環境(config/prod.jsonなど)の場合は、プログラムがconfig error: no config/prod.jsonと表示し、終了コード1で止まる- 2 つの設定を深くマージする。ネストした辞書は、丸ごと置き換えるのではなくキーごとにマージする。そのため、
staging.jsonにmodelがなくても、base.jsonのmodelは残る temperatureの0は0のまま残す。orは使わない
プロジェクトのルートで uv run main.py を実行すると、ちょうど次の出力になるはずです:
env staging
model chat-small
temperature 0
top_k 8そのあと、2 つのことを確かめてください。まず、APP_ENV=prod uv run main.py を実行し、プログラムがはっきりしたメッセージで止まることを確かめます。次に、深いマージの代わりに単純な {**base, **overlay} を試し、config["llm"] を表示してください。model はどこへ消えたのでしょうか? そして、そのマージの行は、なぜエラーを出さなかったのでしょうか?