第三章 参数获取与解析:命令行参数、配置文件、环境变量、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 参数解析的整体流程
梳理一下,整个参数获取与解析的流程可以用一张图说清楚:
3.7 避坑指南
这些年踩过的坑不少,列几个典型的:
- 类型陷阱:命令行传进来的全是字符串。
--batch-size 64拿到的是"64",不是64。我见过代码里直接拿字符串去算内存,结果算出来的数字大得离谱。 - 布尔参数:
--verbose False在argparse里会被当成True。为什么?因为只要参数出现了就算 true。正确用法是用store_true和store_false。 - 编码问题:配置文件用 utf-8,别偷懒用 gbk。我曾经在 Windows 上写了个 gbk 编码的配置文件,结果部署到 Linux 上全成了乱码。嗯,那天又是个加班夜。
- 敏感信息泄露:千万别把密码打在命令行里!Unix 的
ps命令能看见所有进程的命令行参数。用环境变量或者交互式输入才安全。
参数获取看起来是小事,但做好了,能省很多 debug 时间。你想想看,如果一个参数来源不明、优先级混乱、类型自动转换出了问题,查起来得花多少时间?老老实实按规范来,后面的人会感谢你的。