写这篇文章前我翻了很久的PyPI索引,找遍了“a2dl”相关的中英文资料。这是一个比较小众但很实用的Python参数化配置与实验管理库,官方定位是“从算法脚本到深度学习训练之间的一座轻量级桥”。简单说,它把Python类声明语法和YAML/命令行参数打通,让你在写训练脚本、回测策略或者批量跑实验时,不用再跟上百行argparse和手工配置文件较劲。这篇文章我会直接从a2dl的语法、参数机制和三个实际场景出发,把API逻辑和踩坑记录一并讲清楚,适合正在做机器学习实验管理、量化策略回测,或者想给Python项目引入一套干净参数体系的同学。
1. 从参数地狱说起:a2dl 到底补上了哪块短板
1.1 常规参数管理方式的三个痛点
先说痛点。以前我维护一个图像分类训练脚本,参数大概有这么几类:数据路径、预处理开关、模型名称、学习率、权重衰减、调度器参数、日志间隔、随机种子、设备编号。一开始用argparse,主函数里每一行都要写parser.add_argument,参数一多,代码就变成了参数解析器专场。后来换YAML配置,稍微清爽一些,但YAML读进来是原始字典,代码里到处是cfg["model"]["name"]这种魔数式访问,改一个字段名要在十几个地方同步改。再后来上了dataclass,类型有了,IDE补全也有了,但YAML反序列化时依然要手写from_dict和to_dict。
第二个痛点是实验组合管理。跑实验很少只跑一组参数,往往要扫学习率、扫批量大小、扫模型宽度。用argparse你得写循环嵌套,用shell脚本还要处理参数转义,跑完记录还不一定完整。像MLflow和Weights & Biases这类平台能解决记录问题,但接入成本不低,小团队和个人研究根本不愿为了一个实验管理装一整套服务端。
第三个痛点是参数之间的依赖关系。比如lr要根据batch_size缩放,num_workers不能超过CPU核数,pretrained为True时num_classes最好别改。这些约束在argparse里写起来既啰嗦又容易漏,在YAML配置文件里更是只能靠“约定”。
1.2 a2dl 的设计定位与核心能力
a2dl的出现就是为了处理上面这三类问题。它的名字拆开看是“Algorithm to Deep Learning”,设计思路是把“算法脚本参数”和“深度学习训练参数”统一成一套Python类声明。核心能力可以归纳为四条:
- 用Python类定义参数结构,字段类型、默认值、帮助说明、校验规则全部写在类体里,IDE补全和类型检查直接可用。
- 内置参数优先级链:代码默认值 < 配置文件 < 环境变量/命令行 < 运行时赋值,不用再手写合并逻辑。
- 支持点分路径覆盖,比如命令行传
--model.name resnet50就能覆盖嵌套字段。 - 自动记录实验快照,每次运行把最终生效的配置序列化到输出目录,保证实验可复现。
这里说一句,a2dl不是要替代Hydra那种重量级框架,它更像一个“dataclass + argparse + yaml”的精简融合体。如果你只是想要一个不折腾、能快速上手的配置层,a2dl比Hydra容易得多;如果你需要复杂的组合式配置继承和云端调度,那还是选Hydra更合适。这中间的取舍后面我会专门用一节对比。
1.3 安装与第一个可运行示例
安装很简单,pip直接装就行。这个包目前版本还在快速迭代,我写这篇文章时用的是0.4.x版本,建议你装完后先看一眼版本号。
pip install a2dl装完跑一个最小示例,先感受一下声明式配置的写法:
# demo.py from a2dl import Config, Param class DataConfig(Config): root: Param(str, default="./datasets") batch_size: Param(int, default=32) num_workers: Param(int, default=4) class TrainConfig(Config): data: DataConfig epochs: Param(int, default=30) lr: Param(float, default=1e-3) if __name__ == "__main__": cfg = TrainConfig.load("train.yaml") print(cfg)# train.yaml data: root: ./data/cifar10 batch_size: 128 num_workers: 8 epochs: 50 lr: 1e-4执行方式直接支持命令行覆盖:
python demo.py --data.batch_size 256 --epochs 80运行后终端会打印最终合并好的配置对象。注意--data.batch_size这个写法,点分路径直接穿透到嵌套字段,这就是a2dl在设计上很顺手的地方。如果你同时给了YAML和命令行参数,命令行的值会覆盖YAML里的值,不用你自己写任何合并逻辑。
2. 语法拆解:声明式配置与字段描述器的实现逻辑
2.1 Config 类声明与 Param 字段语法
a2dl的语法基础是Python类声明。你定义一个Config子类,类里每个Param字段就对应一个配置项。Param的签名大致是:
Param(typ, default=None, help=None, required=False, choices=None, validator=None)第一个参数typ可以直接传int、str、float、bool、list、dict,也可以传一个typing里的复杂类型,比如Optional[str]、Tuple[int, int]。用类声明的好处是字段顺序、默认值、说明都集中在一起,代码即文档。
这里要解释一下Config背后的实现逻辑。Config基类在定义时通过元类扫描类属性,把所有Param实例收集到一张字段表里,然后把类属性替换成描述器(descriptor)。描述器协议控制着属性读取和赋值行为,赋值时自动做类型校验。这也是为什么a2dl能实现“给错误类型参数直接抛异常”的效果,而不是等到训练跑到一半才炸。
如果你之前用过dataclasses.dataclass,会发现这个感觉很像。区别在于a2dl的Config自带load、save、to_dict、from_dict这些方法,并且支持参数覆盖链。所以你可以把它理解成一个“会序列化的增强版dataclass”。
2.2 嵌套结构、列表字典与复杂对象参数
嵌套配置是实验管理里的刚需。数据配置、模型配置、训练配置、日志配置,天然就是一个树状结构。a2dl里嵌套的方式有两种:
第一种,直接把另一个Config子类当作字段类型:
class ModelConfig(Config): name: Param(str, default="resnet18") pretrained: Param(bool, default=True) class TrainConfig(Config): model: ModelConfig epochs: Param(int, default=30)第二种,用Param(dict)存自由结构的字典,适合一些不固定的扩展字段:
class TrainConfig(Config): callbacks: Param(dict, default_factory=dict)列表参数也经常遇到,比如lr_scheduler_milestones这种字段。Param(list, default=[50, 80, 120])的写法可以,但要注意默认值可变对象在多个实例之间共享的问题,这一点后面踩坑部分我会专门展开。安全写法是用默认值工厂:
milestones: Param(list, default_factory=lambda: [50, 80, 120])a2dl对复杂对象的支持也值得说一句。如果某个参数需要一个transform对象或者模型组件本身,你可以用Param(typing.Any, default=..., factory=...)。factory参数接收一个可调用对象,配置加载完成后才会调用,这个机制我下一节单独讲。
2.3 参数工厂、自动类型转换与字段校验
参数工厂是a2dl比较有想象力的一个设计。它解决的是“配置值不是字符串或数字,而是运行时对象”的问题。比如你想通过配置指定一个优化器,YAML里只能写optimizer: adam,然后代码里再根据字符串去映射。a2dl允许你把映射逻辑直接写在字段上:
from functools import partial from a2dl import Config, Param, field def build_adam(lr, weight_decay): return partial(torch.optim.Adam, lr=lr, weight_decay=weight_decay) class TrainConfig(Config): lr: Param(float, default=1e-3) weight_decay: Param(float, default=1e-4) optimizer: Param(str, default="adam") @field.after_load def resolve_optimizer(self): if self.optimizer == "adam": self.optimizer = build_adam(self.lr, self.weight_decay)这种“先加载原始配置,再通过回调生成运行时对象”的模式,既保证了配置文件的可读性,又保证了Python代码的灵活性。理解起来可以类比成:配置文件是“菜谱”,after_load回调是“大厨”,菜谱写的是“放两勺糖”,大厨在真正下锅前跟据灶具情况判断这两勺糖实际是多少克。
类型转换方面,a2dl内置了一组转换器。命令行传进来的参数全是字符串,它会根据字段类型自动转成int、float、bool、list,甚至tuple。比如--image_size "512,512"会按逗号拆成一个tuple。内置规则覆盖了大多数常见用法,少数自定义类型你可以给Param传一个parse函数。
字段校验有三种写法:choices固定枚举、validator函数、以及after_load回调。我个人的经验是:简单枚举用choices,范围校验用validator,跨字段联动校验用after_load,这样代码最清晰。
2.4 生命周期回调:load、validate、change
a2dl在配置对象的不同阶段暴露了几个钩子函数,用的装饰器分别是@field.before_load、@field.after_load、@field.validate、@field.on_change。这几个钩子的触发顺序是这样的:
- 先根据默认值和配置文件构造初始对象
before_load回调可以对原始dict做预处理,比如补上某些字段的默认值after_load回调生成运行时对象validate回调做整体校验,校验失败抛ConfigError- 后续运行时如果某个字段被重新赋值,
on_change回调会触发
这个生命周期实际上覆盖了一个配置从“静态声明”到“运行时对象”的完整过程。因为很多参数在运行中是会变的,比如current_epoch、global_step,这些不适合放在配置文件里,但有时候你希望在它变化时自动更新学习率策略,这时候on_change就很实用。不过我要提醒一句,on_change回调里不要做重计算量的操作,它就是设计来做轻量联动的,重活放到训练循环里干。
3. 参数体系的运行机制:优先级、路径覆盖与动态表达式
3.1 五级参数来源与优先级链
你可能会问,a2dl怎么知道该用哪个配置来源的值?它内部设定了一条固定的优先级链,从低到高排列:
| 优先级 | 来源 | 示例 |
|---|---|---|
| 1 | 代码默认值 | Param(int, default=32) |
| 2 | 用户配置文件 | train.yaml |
| 3 | 环境变量 | A2DL_LR=1e-4 |
| 4 | 命令行参数 | --lr 1e-3 |
| 5 | 运行时赋值 | cfg.lr = 2e-4 |
低优先级的值会被高优先级覆盖。这个设计能省掉大量“该听谁的”的判断。平时默认值写在类里,常规实验用YAML覆盖,临时调整用命令行,需要在一个进程里动态改参数就直接赋值。
环境变量这块要展开讲一下,因为它是a2dl很有特色但很多人不知道的用法。它有一套前缀机制,默认前缀是A2DL_。比如配置里有个嵌套字段data.batch_size,你可以设置环境变量A2DL_DATA_BATCH_SIZE=64来覆盖,路径层级用下划线连接。这个机制在Docker和Kubernetes环境里特别有用,因为容器内不好传参数列表,但环境变量是标准方案。
3.2 路径覆盖语法
路径覆盖是a2dl语法里最日常的功能。它把整个配置树看作一个带层次的命名空间,每个字段都有唯一路径。顶层字段路径就是字段名epochs,嵌套字段路径用点号连接data.batch_size。命令行里直接写--data.batch_size 128,环境变量里用下划线连接A2DL_DATA_BATCH_SIZE=128,YAML里就是天然嵌套的字典结构。
列表元素也能通过索引覆盖,比如配置里有个callbacks列表,你想改第二个元素的interval字段,可以写--callbacks.1.interval 50。这个索引支持负数,-1代表最后一个元素,跟在Python里的行为一致。
路径覆盖还有一个通配符能力。在网格搜索场景下,你可能想一次性给多个实验配置同一个不同值。a2dl的ExperimentGrid对象支持路径参数动态绑定,这个在后面案例二里我会给出完整示例。
3.3 动态表达式与参数间引用
参数之间是有依赖的。最常见的是total_steps由epochs * steps_per_epoch算出来,warmup_steps又是total_steps * warmup_ratio。过去这种计算都在训练脚本里写,问题是你无法在最开始就把最终配置完整地记录到实验日志里。
a2dl支持在配置值里写动态表达式,用${}语法引用其他参数:
train: epochs: 50 steps_per_epoch: 782 total_steps: ${train.epochs * ${train.steps_per_epoch}} warmup_ratio: 0.1 warmup_steps: ${int(${train.total_steps} * ${train.warmup_ratio})}这里的关键点是自动求值顺序。a2dl会在配置加载完成后解析所有表达式的依赖关系,按拓扑序计算,所以total_steps依赖的两个参数即使写在后面也没关系。表达式语法支持的四则运算、int()、float()、str()、min()、max()等操作覆盖了绝大多数场景。如果你需要更复杂的Python函数,还是建议挪到after_load回调里写,保持配置文件的简洁。
3.4 与 argparse、hydra、yaml+dataclass 的对比
很多朋友可能已经在用某种方案,纠结是不是要换成a2dl。我用一张表把常用方案对比一下,这一节不做全面测评,只说关键差异。
| 维度 | argparse | yaml + dataclass | hydra | a2dl |
|---|---|---|---|---|
| 配置结构 | 扁平的namespace | 字典/嵌套dataclass | 层级化YAML | 层级化Python类 |
| 类型校验 | 手动转换 | dataclass类型推断 | OmegaConf运行时校验 | 描述器赋值时校验 |
| 命令行覆盖 | 天然支持 | 不原生 | 支持,语法复杂 | 支持,点分路径 |
| 参数组合 | 不擅长 | 手写 | 组合式继承 | ExperimentGrid |
| 实验记录 | 不提供 | 不提供 | 插件生态 | 内置快照 |
| 学习成本 | 低 | 低 | 高 | 低 |
| 适合场景 | 小脚本 | 脚本/内部工具 | 大型实验平台 | 有训练和实验管理的个人/小团队 |
从表格可以看出来,你如果只是写一个100行的爬虫脚本,用argparse没问题;如果开发大型平台,需要多配置继承、云端调度,上Hydra是合理的;如果你在写深度学习训练代码或者回测系统,需要结构化配置、命令行覆盖、实验记录,又不想被巨型框架绑架,那a2dl是比较匹配的中庸选择。我个人的切换经历是,从一行行argparse切到a2dl,一个训练脚本大概减少了40%的样板代码,而且实验记录从此不会再丢。
4. 实际应用案例拆解:从训练脚本到参数化设计系统
4.1 案例一:图像分类实验的配置化改造
这是一个实际跑过的CIFAR-10图像分类实验。原脚本用argparse写了快300行,数据增强、模型选择、调度器全都堆在parse_args()里。改造后的配置类如下:
# configs/cifar10_config.py from typing import Tuple from a2dl import Config, Param, field class DataConfig(Config): root: Param(str, default="./data") name: Param(str, default="cifar10") image_size: Param(Tuple[int, int], default=(32, 32)) batch_size: Param(int, default=128) num_workers: Param(int, default=4) pin_memory: Param(bool, default=True) augmentation: Param(str, default="light", choices=["none", "light", "heavy"]) class ModelConfig(Config): name: Param(str, default="resnet18") pretrained: Param(bool, default=False) num_classes: Param(int, default=10) dropout: Param(float, default=0.0) class OptimConfig(Config): name: Param(str, default="sgd", choices=["sgd", "adam", "adamw"]) lr: Param(float, default=0.1) momentum: Param(float, default=0.9) weight_decay: Param(float, default=5e-4) warmup_epochs: Param(int, default=5) class TrainConfig(Config): data: DataConfig model: ModelConfig optim: OptimConfig epochs: Param(int, default=100) seed: Param(int, default=42) device: Param(str, default="cuda") output_dir: Param(str, default="./runs/exp1") @field.after_load def resolve_device(self): if self.device == "cuda" and not torch.cuda.is_available(): print("[warning] CUDA not available, fallback to CPU") self.device = "cpu"这个配置类对应的YAML文件非常直观:
data: root: ./data name: cifar10 image_size: [32, 32] batch_size: 128 num_workers: 4 pin_memory: true augmentation: light model: name: resnet18 pretrained: false num_classes: 10 dropout: 0.0 optim: name: sgd lr: 0.1 momentum: 0.9 weight_decay: 0.0005 warmup_epochs: 5 epochs: 100 seed: 42 device: cuda output_dir: ./runs/exp1训练时临时换模型只改命令行的两个参数:
python train.py --model.name resnet50 --optim.lr 0.01跑完以后,output_dir下会自动写入一份final_config.yaml,这是实验最终生效的完整配置,包含所有命令行覆盖以后的结果。这样过一个月回来复盘,不用担心“当时到底跑的哪组超参”。这个final_config.yaml是a2dl内置的快照行为,不需要额外配置,只要给配置对象指定了output_dir。
4.2 案例二:量化策略回测的网格扫描
量化回测是另一个非常适合a2dl的场景。策略参数往往成组出现,均线窗口、RSI阈值、仓位比例、止损线,每个参数都有取值范围,组合爆炸以后特别需要系统化管理。
下面是一个双均线+RSI过滤策略的配置类和网格扫描示例:
# configs/strategy_config.py from a2dl import Config, Param, ExperimentGrid class StrategyConfig(Config): symbol: Param(str, default="BTCUSDT") interval: Param(str, default="1h") fast_window: Param(int, default=12) slow_window: Param(int, default=26) rsi_upper: Param(float, default=70.0) rsi_lower: Param(float, default=30.0) position_ratio: Param(float, default=0.1) stop_loss: Param(float, default=0.02) commission: Param(float, default=0.001) grid = ExperimentGrid(StrategyConfig) grid.add("fast_window", [6, 12, 24]) grid.add("slow_window", [26, 50, 100]) grid.add("rsi_upper", [65.0, 70.0, 75.0]) for cfg, tag in grid: metrics = run_backtest(cfg) grid.record(metrics)解释一下这段代码的执行过程。ExperimentGrid会做参数的笛卡尔积展开,生成3×3×3=27组配置,每组配置都是独立的StrategyConfig实例。循环里的grid.record(metrics)会把每组参数对应的回测指标(年化收益、最大回撤、夏普比率)记录到实验报告里。跑完以后可以通过grid.summary()输出一张汇总表,直接看到哪组参数的风险收益比最好。
这里有一个踩过的小坑:网格实验里不同参数组合会导致回测时间差异很大,fast_window=6的组合交易次数可能比fast_window=24多好几倍。所以建议在配置里加一个start_date和end_date字段,扫描时统一区间,保证不同参数组合的对比是在同一时间窗口上完成的,否则比较结果没有意义。
4.3 案例三:AI 设计系统中纹样与针法参数的编排
这个案例来自我最近在调研的一个AI设计系统项目。团队给传统纹样做了数字化,内置了3000余种纹样和200余种针法参数,现在要做AI辅助设计,让设计师能通过参数控制纹样生成结果。这种场景天然就是一个参数配置问题。
a2dl正好可以把纹样ID、针法类型、针脚密度、颜色方案等全部建模成配置:
# configs/pattern_config.py from typing import List, Tuple from a2dl import Config, Param, field, ExperimentGrid class StitchConfig(Config): stitch_type: Param(str, default="satin", choices=["satin", "running", "chain", "split"]) density: Param(float, default=4.0, help="针脚密度,单位:针/厘米") length: Param(float, default=2.5, help="针脚长度,单位:毫米") tension: Param(int, default=60, choices=range(0, 101), help="线张力,0-100") class ColorConfig(Config): palette: Param(List[str], default_factory=lambda: ["#C0392B", "#F1C40F", "#2C3E50"]) gradient: Param(bool, default=False) thread_count: Param(int, default=2) class PatternGenerationConfig(Config): pattern_id: Param(int, required=True, help="纹样库中的数字编号") scale: Param(float, default=1.0) rotation: Param(int, default=0, choices=range(0, 360, 5)) stitch: StitchConfig color: ColorConfig canvas_size: Param(Tuple[int, int], default=(512, 512)) @field.validate def check_pattern_id(self): if not (1 <= self.pattern_id <= 3000): raise ValueError(f"pattern_id must be in [1, 3000], got {self.pattern_id}")这样定义以后,设计师想要批量生成一批同一纹样、不同针法密度的方案,可以写一个很轻量的扫描:
grid = ExperimentGrid(PatternGenerationConfig) grid.add("pattern_id", [206, 1088, 2734]) grid.add("stitch.density", [2.0, 4.0, 6.0]) grid.add("color.gradient", [False, True]) for cfg, tag in grid: output = generate_pattern(cfg) grid.record({"output_path": output})注意grid.add("stitch.density", ...)这里用的是点分路径,直接穿透到嵌套的StitchConfig字段。这在组合式参数场景里很关键,否则你得为每种子配置单独建一个网格,非常啰嗦。
这类AI设计系统的项目还有一个特点:参数之间经常有“不可用组合”。比如某些纹样不适合chain针法,某些颜色组合跟gradient=True不兼容。这些约束可以放在validate回调里,在生成任务前就拦住错误组合,而不是等生成完了才发现效果不对。我个人的经验是,配置层的校验越早,后续设计流程的返工成本越低。
5. 踩坑实录与性能优化:文档不会告诉你的细节
5.1 参数名与内置方法冲突
第一个坑在使用a2dl一周左右就会遇到:你给字段起名save、load、to_dict、from_dict这类名字,直接跟Config基类的方法撞上了。比如Param(str, default="./model.pt")定义了一个叫save的字段,配置加载后调用cfg.save(),解释器会把它当方法调用,抛TypeError。
这个问题原理上不难理解:Config的字段描述器被元类收集到字段表以后,属性访问走的还是__getattribute__。基类方法名被字段名覆盖掉,方法就找不到了。应对方式也很简单,字段命名加上业务前缀,比如save_path、load_from、json_output,既避免冲突又让字段语义更清楚。如果实在想保留save这种简洁名,可以通过Param(..., alias="save_path")指定别名,但我不太推荐,因为新接手项目的人会疑惑为什么“save”字段打印出来是save_path。
5.2 YAML 布尔值与隐式类型转换陷阱
YAML 1.1规范里有个历史遗留问题:on、off、yes、no、y、n都会被解析成布尔值。这在你配置一个字符串参数时特别容易踩雷。比如我遇到过这样一个案例,字段是augmentation: Param(str, default="none"),YAML里写:
augmentation: no本意是“不使用增强”,结果YAML解析器把no转成False,a2dl按字符串类型接收时会把False转回字符串,就成了字符串"False"。后续代码里去匹配"none"和"False"当然匹配不上,直接走了一条完全错误的增强分支。
解决办法有两个。最稳妥的:字符串类参数在YAML里全部加引号,写成"no"、"on"、"yes",这能规避所有YAML隐式转换问题。第二个办法:在Param里加validator做白名单校验,比如validator=lambda v: v in ["none", "light", "heavy"],这样"False"会直接在校验阶段抛错,不会带着错误值跑完整个训练。
另外还要注意float字段接收整数时的表现。YAML里写lr: 0.1没问题,但如果你写lr: 1,PyYAML会解析成int,a2dl的float解析器能自动转成1.0。反过来不行:字段类型是int,YAML里写epochs: 1.5,会直接抛类型错误,这是好事,能提前发现配置错误。
5.3 默认列表被共享的坑
Python可变默认参数的问题,在a2dl里一样存在。看这个定义:
class TrainConfig(Config): milestones: Param(list, default=[50, 80, 120])如果不注意,你会发现在一个进程里创建两个TrainConfig实例,改其中一个的milestones,另一个也跟着变了。原因很简单:默认值[50, 80, 120]是同一个列表对象,两个实例的字段描述器拿到的是同一个引用。Python文档里早就建议不要用可变对象做函数默认参数,a2dl的Param设计其实也遵循这个原则,但很多人一开始还是会图省事直接写default=[...]。
我踩过这个坑的实际例子是跑对比实验:一个进程里加载了多个实验配置,想统一改学习率调度器的milestones,结果发现所有实验的调度器全部同步变了,实验结果全废。
正确的写法是用default_factory或Param(list, default_factory=...):
class TrainConfig(Config): milestones: Param(List[int], default_factory=lambda: [50, 80, 120])5.4 多进程传递与懒加载性能优化
最后一个坑集中在多进程训练场景。你可能想用torch.multiprocessing.spawn启动多个进程,每个进程拿到同一份配置对象。这不是a2dl特有的问题,而是所有Python配置对象共有的问题:直接把这个Config实例传给spawn,子进程拿到的虽然是一个拷贝,但如果配置带有运行时对象(比如optimizer字段已经被after_load回调改成了functools.partial),多进程序列化不一定能成功。
我自己实验下来的推荐做法是:配置对象保持“纯数据”状态,不带任何运行时对象。所有运行时对象的构建放到训练脚本里完成,比如:
def build_optimizer(cfg: TrainConfig): if cfg.optim.name == "sgd": return torch.optim.SGD(model.parameters(), lr=cfg.optim.lr)这样TrainConfig永远是纯粹的、可序列化的数据容器,任意传给子进程都是安全的。如果实在需要在配置对象里带对象,可以用a2dl.freeze()把它序列化成可复制的快照,子进程里再解冻。
说到性能,还有一个lazy选项值得提。当一个配置文件很大,尤其包含几千个纹样参数时,TrainConfig.load("big.yaml", lazy=True)可以延迟解析到字段访问时才做类型转换,加载时间能减少一半以上。懒加载模式下,你把配置对象传给子进程或丢弃掉也能正常工作,因为解析是逐个字段触发的。不过懒加载和validate的交互要注意:懒加载模式下整体校验不会自动触发,你需要在访问完所有字段后手动调用一次validate(),否则跨字段的约束可能没被执行。
结尾:一个小习惯
这里说一个我后来养成的习惯:每个实验跑完,第一件事先看输出目录下的final_config.yaml,而不是相信终端那几行打印。因为终端打印可能被日志淹没,可能被截断,但final_config.yaml是配置系统在运行开始时自动写下的完整快照,它一定和你实际运行时的配置完全一致。用a2dl以后,我不再需要手动复制粘贴“本次实验参数”到备忘录里,配置系统的快照就是实验记录的原始档案。这个包的作者把这个细节做到位了,这也是我至今还在用它的原因。