第三章 参数获取与解析:命令行参数、配置文件、环境变量、API参数

说实话,参数获取这个事儿,看着简单,坑却不少。我记得刚带团队那会儿,有个同事把数据库密码硬编码在代码里,结果代码被传到 GitHub 上了……嗯,后面的事儿你懂的。从那以后,我对参数获取的规范就特别上心。

3.1 命令行参数解析

命令行参数是最直接的交互方式。我个人习惯用 Python 的 argparse 模块,标准库自带,不用装额外依赖。

import argparse

parser = argparse.ArgumentParser(description='模型训练工具')
parser.add_argument('--batch-size', type=int, default=32,
                    help='每批样本数')
parser.add_argument('--lr', type=float, default=1e-3,
                    help='学习率')
parser.add_argument('--resume', action='store_true',
                    help='从检查点恢复训练')

args = parser.parse_args()
print(f'batch_size: {args.batch_size}')
print(f'lr: {args.lr}')
经验之谈:给参数起名时,用 --batch-size 而不是 --batch_size。为什么?因为 Unix 命令行习惯用连字符。虽然 Python 内部会自动转成 batch_size,但用户敲命令时少按个 Shift 键,体验会好很多。

3.2 配置文件加载

当参数超过 10 个时,全塞命令行就不太现实了。我的做法是:核心参数放命令行,批量参数放配置文件。

配置文件的格式我推荐三种:YAML、JSON、TOML。用哪个?看团队习惯。

格式 优点 缺点 我常用的场景
YAML 可读性最好,支持注释 缩进敏感,容易出错 复杂配置,如多环境部署
JSON 解析快,生态最广 不支持注释,写起来啰嗦 跨语言通信的配置
TOML 明确直观,支持类型 相对较新,工具链少 Python 项目的配置

我在生产项目中见过太多因为配置文件格式混乱导致的问题。后来我定了个规矩:一个项目只认一种格式。下面是个 YAML 的例子:

# config.yaml
model:
  name: "resnet50"
  num_classes: 1000

training:
  batch_size: 64
  epochs: 100
  optimizer: "adam"
  lr_scheduler:
    type: "cosine"
    warmup_epochs: 5

data:
  train_path: "./data/train"
  val_path: "./data/val"
  num_workers: 4

Python 那边加载起来也很简单:

import yaml

with open("config.yaml", "r") as f:
    config = yaml.safe_load(f)

batch_size = config["training"]["batch_size"]
print(f"训练批次大小:{batch_size}")
注意:别用 yaml.load() 不加参数!默认的 Loader 可能执行任意 Python 代码。始终用 safe_load(),除非你清楚自己在做什么。

3.3 环境变量的妙用

环境变量最适合存放敏感信息——数据库密码、API Key、Secret Token。这些你绝对不想写进配置文件里。

我曾经在一个项目里,有人把 AWS Secret Key 写进了配置文件的默认值。结果那个配置文件被当作示例发到了客户群里……嗯,那天我花了一整个下午轮换密钥。

import os

DB_HOST = os.environ.get("DB_HOST", "localhost")
DB_PORT = int(os.environ.get("DB_PORT", "5432"))
DB_PASS = os.environ.get("DB_PASSWORD")

if not DB_PASS:
    raise ValueError("必须设置 DB_PASSWORD 环境变量")

命名上有门道:全大写、下划线分隔,这是环境变量的约定俗成。千万别写成 db_password 或者 dbPass,会被老运维笑话的。

3.4 API 参数传递

现在做 AI 应用,少不了搭个 API 服务。参数怎么传?无非四种方式:查询参数、路径参数、请求体、请求头。

  • 查询参数(Query Params):GET 请求,适合简单筛选,比如 /api/infer?model=resnet50
  • 路径参数(Path Params):资源定位,比如 /api/models/resnet50
  • 请求体(Request Body):POST/PUT,传大块数据,JSON 格式最常用
  • 请求头(Headers):认证信息、内容类型等元数据

以 FastAPI 为例,写起来特别直观:

from fastapi import FastAPI, Query, Path, Header
from pydantic import BaseModel

app = FastAPI()

class InferRequest(BaseModel):
    prompt: str
    max_tokens: int = 512
    temperature: float = 0.7

@app.post("/v1/models/{model_name}/infer")
async def infer(
    model_name: str = Path(..., description="模型名称"),
    request: InferRequest = None,
    authorization: str = Header(None, description="Bearer token")
):
    # 校验 token
    if not authorization or not authorization.startswith("Bearer "):
        return {"error": "无效的认证信息"}
    
    # 实际推理逻辑
    result = run_inference(model_name, request.dict())
    return {"status": "ok", "result": result}

用 Pydantic 做请求体验证是个好习惯。你想想看,如果用户传了个负数的 temperature,或者 max_tokens 传了个字符串,API 直接崩了,多难看。加上类型校验至少能返回个友好的错误提示。

3.5 参数优先级与合并策略

实际项目中,这四种来源经常会同时存在。你想想看,如果命令行里指定了 --batch-size=128,配置文件里也写着 batch_size: 64,到底听谁的?

我个人定了一个优先级规则:命令行 > 环境变量 > 配置文件 > 代码默认值。为什么这么排?命令行是用户当前最明确的操作意图,环境变量是部署环境决定的,配置文件的变通性最差。

代码实现大概是这个意思:

import os
import yaml
import argparse

def merge_config():
    # 1. 读取默认配置
    config = {
        "batch_size": 32,
        "lr": 0.001,
        "epochs": 10
    }
    
    # 2. 加载配置文件(低优先级)
    config_file = os.environ.get("CONFIG_FILE", "config.yaml")
    if os.path.exists(config_file):
        with open(config_file) as f:
            file_config = yaml.safe_load(f)
            config.update(file_config)
    
    # 3. 环境变量覆盖(中优先级)
    env_batch = os.environ.get("BATCH_SIZE")
    if env_batch:
        config["batch_size"] = int(env_batch)
    
    # 4. 命令行参数覆盖(最高优先级)
    parser = argparse.ArgumentParser()
    parser.add_argument("--batch-size", type=int)
    args = parser.parse_args()
    if args.batch_size:
        config["batch_size"] = args.batch_size
    
    return config
核心思路:先加载默认值,再依次用低优先级覆盖高优先级。每个来源只覆写自己能控制的参数,不互相干扰。

3.6 参数解析的整体流程

梳理一下,整个参数获取与解析的流程可以用一张图说清楚:

参数获取与解析流程 参数获取与解析流程 命令行参数 argparse / click 配置文件 YAML / JSON / TOML 环境变量 os.environ API 参数 Query/Path/Body 参数优先级合并 命令行 > 环境变量 > 配置文件 > 默认值 类型解析与验证 int / float / bool / 枚举 / 自定义校验 最终配置对象 Config / NamedTuple / dict 四种来源 → 按优先级合并 → 类型校验 → 生成最终参数对象,供程序使用

3.7 避坑指南

这些年踩过的坑不少,列几个典型的:

  1. 类型陷阱:命令行传进来的全是字符串。--batch-size 64 拿到的是 "64",不是 64。我见过代码里直接拿字符串去算内存,结果算出来的数字大得离谱。
  2. 布尔参数:--verbose False 在 argparse 里会被当成 True。为什么?因为只要参数出现了就算 true。正确用法是用 store_true 和 store_false。
  3. 编码问题:配置文件用 utf-8,别偷懒用 gbk。我曾经在 Windows 上写了个 gbk 编码的配置文件,结果部署到 Linux 上全成了乱码。嗯,那天又是个加班夜。
  4. 敏感信息泄露:千万别把密码打在命令行里!Unix 的 ps 命令能看见所有进程的命令行参数。用环境变量或者交互式输入才安全。
特别提醒:如果项目涉及分布式训练,参数同步也是个坑。主节点的参数变化没能广播给所有 worker,会导致训练不一致。建议用统一的配置中心,或者至少在所有节点上跑相同的配置文件和命令行。

参数获取看起来是小事,但做好了,能省很多 debug 时间。你想想看,如果一个参数来源不明、优先级混乱、类型自动转换出了问题,查起来得花多少时间?老老实实按规范来,后面的人会感谢你的。