基础使用
跑起来
安装包:pip install hydra-core
初始代码:
from omegaconf import DictConfig, OmegaConf
import hydra # 使用hydra.main装饰器,指定配置文件路径和名称
# config_path: 配置文件所在目录
# config_name: 配置文件名称
@hydra.main(version_base=None, config_path=".", config_name="config")
def my_app(cfg: DictConfig) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()
missing_value
对于???占位的,可以赋值后输出
@hydra.main(version_base=None, config_path=".", config_name="config")
def my_app(cfg: DictConfig) -> None: OmegaConf.resolve(cfg) cfg.node.waldo = 11 # 给???node.waldo赋值,还可以在运行的时候通过命令行赋值 print(cfg.node.waldo) print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()
cli命令行输入
# 没有配置文件,只使用命令行输入参数,命令行输入参数的格式为:python my_app.py ++key=value
# 必须加上++,要不然参数传不进去
# python my_app.py ++db=10
@hydra.main(version_base=None)
def my_app(cfg: DictConfig) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()
配置文件组
文件是
- conf- dbmysql.yamlpostgresql.yaml
# python my_app.py +db=mysql
# 通过命令行输入参数,指定配置文件路径,格式为:python my_app.py +db=mysql
# +db=mysql 表示在配置文件中添加一个新的配置项 db,并将其值设置为 mysql
# 因为只指定了config_path的值
# 外面这个db文件夹就是一个配置文件组,里面有mysql.yaml和postgresql.yaml两个配置文件,分别对应不同的数据库配置
@hydra.main(config_path="conf")
def my_app(cfg: DictConfig) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()
| 操作符 | 行为 | 如果键 不存在 | 如果键 已存在 |
|---|---|---|---|
+ (加号) |
添加, 即创建新键 | ✅ 成功 (创建新键) | ❌ 失败 (报错) |
++ (双加号) |
强制覆盖或添加 | ✅ 成功 (创建新键) | ✅ 成功 (覆盖旧值) |
一个问题:Hydra 的代码怎么知道 +db=mysql 里的 db 是一个配置组(要去文件夹里找文件),而不是一个普通字段(直接赋个字符串值)? |
Hydra会自动去找,知道配置目录是
conf,那就会找db文件夹,找到了就说明是配置组,没找到就是普通文件。
如果 Hydra 在
conf/目录下找不到db这个文件夹(也就是不存在名为db的配置组),那么+db=mysql就会被当作一个普通的字段赋值,最终cfg.db的值就是字符串"mysql"。
默认配置
目录结构:
- conf- dbmysql.yamlsql.yaml- hosta.yamla2.yaml
config.yaml
config.yaml
# 有多个组时,在这里配置默认值,用户可以在运行时覆盖这些值 # 所以此时的config.yaml就是新的配置文件
defaults: - db: mysql- host: a
代码配置
@hydra.main(version_base=None, config_path="conf", config_name="config")
def my_app(cfg: DictConfig) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()
配置的继承
- confconfig.yaml- dbbase_mysql.yaml继承本组.yaml继承其他组.yaml- db_schemabase_mysql.yaml
同组可以直接继承某个yaml文件的配置
base_mysql.yaml
host: localhost
port: 3306
user: ???
password: ???
同组继承:(直接覆盖参数或者添加)
# 继承同组配置,直接引用
defaults: - base_mysql user: omry
password: secret
port: 3307
encoding: utf8
继承其他组的配置:
# 继承另一个组的配置
# 格式:组路径/组名@_here_
# @here_表示: 当前组继承自另一个组,不加就是插入,而不是覆盖继承
defaults: - /db_schema/base_mysql@_here_ user: omry
password: secret
port: 3307
encoding: utf8
实验组
实验组的意思是:config中设置了默认的配置,但是我们有其他常用的东西,我们可以创建一个实验组,然后在命令行中指定这个实验组,这样,我们就不需要每次都去修改配置文件了
比如db组合server组,不同的数据库配置不同的server服务器。
我们可以在默认config中写最常用的db+server,但是其他的配置组合可以单独写到experient组中,需要的时候导入。
package指令
很多时候,你需要在一个程序里使用两份相同类型但不同角色的配置。比如:从数据库 A 读数据,写到数据库 B。两个数据库的配置结构是一样的(都有 host、port、user 等),但值不同。
如果用普通的 - db: mysql,你只能生成一个 cfg.db,没法同时得到两个独立的配置块。
而 db@source: mysql 和 db@destination: mysql 这种语法就能完美解决:它把同一个 db 配置组(mysql.yaml)加载两次,并分别放到 source 和 destination 这两个不同的键下面。
@hydra.main(version_base=None, config_path="conf", config_name="two_packages")
def my_app(cfg: DictConfig) -> None: print(OmegaConf.to_yaml(cfg)) # 配置文件内容如下:
# defaults:
# - db@source: mysql
# - db@destination: mysql
# 意思是:source和destination都使用mysql,之后文件中就有两个配置项source和destination
if __name__ == "__main__": my_app()
db@source: mysql
db:配置组的名字(对应conf/db/文件夹)。mysql:选项的名字(对应conf/db/mysql.yaml文件)。@source:安装路径。意思是:“不要把加载的内容放在默认的db键下,而是放到source键下。”
如果不写 @source,默认 - db: mysql 会把内容放到 cfg.db。
加上 @source 后,内容会被放到 cfg.source;同理,db@destination: mysql 会把内容放到 cfg.destination。
所以最后算是一个文件被导入两次,成了两个不同的对象,便于分开操作。
instantiate实例化
通过配置来动态创建对象。
核心就是 hydra.utils.instantiate 这个函数。它可以根据配置yaml中的 _target_ 字段,自动找到对应的类,并用其余字段作为参数来创建该类的实例。
yaml文件:
# _target_的作用:告诉Hydra如何实例化这个类
_target_: my_app.MySQLConnection
host: localhost
user: root
password: 1234
# 抽象基类
class DBConnection: def connect(self) -> None: ... class MySQLConnection(DBConnection): def __init__(self, host: str, user: str, password: str) -> None: self.host = host self.user = user self.password = password def connect(self) -> None: print(f"MySQL connecting to {self.host}") class PostgreSQLConnection(DBConnection): def __init__(self, host: str, user: str, password: str, database: str) -> None: self.host = host self.user = user self.password = password self.database = database def connect(self) -> None: print(f"PostgreSQL connecting to {self.host}") @hydra.main(version_base=None, config_path="conf", config_name="config")
def my_app(cfg: DictConfig) -> None: # instantiate函数的作用是:根据配置文件中的内容,创建一个对象 # 配置文件必须有_target_字段,才能被instantiate函数识别,_target_字段的值是一个字符串,表示要创建的对象的类名 connection = instantiate(cfg.db) connection.connect() if __name__ == "__main__": my_app()
instantiate(cfg.db) 大致做了这些事情:
- 读取
cfg.db中的_target_字段,它的值是一个字符串,比如"__main__.MySQLConnection"。 - 通过 Python 的导入机制找到这个类。
- 将
cfg.db中 除了_target_以外的所有字段 作为关键字参数,传递给该类的构造函数。 - 返回构造好的实例。
上面代码相当于执行:MySQLConnection(host="localhost", user="root", password="secret")
partial instantiate 部分实例化
model: _target_: my_app.Model optim_partial: _partial_: true _target_: my_app.Optimizer algo: SGD
Optimizer类,algo和lr参数,配置文件中想先只固定algo,lr先不固定,等创建的时候再说。
# 优化器类Optimizer的参数algo和lr,在yaml中定义的时候,加上_partial_: true,表示只实例化部分参数,其他参数在Model类中实例化的时候再传入,这样就可以实现部分实例化了
# 这里是优化器的类Optimizer,包含了algo和lr两个参数,在yaml中定义的时候,只实例化了algo参数,lr参数在Model类中实例化的时候传入,这样就可以实现部分实例化了
class Optimizer: algo: str lr: float def __init__(self, algo: str, lr: float) -> None: self.algo = algo self.lr = lr def __repr__(self) -> str: return f"Optimizer(algo={self.algo},lr={self.lr})" class Model: def __init__(self, optim_partial: Any): super().__init__() self.optim = optim_partial(lr=0.1) def __repr__(self) -> str: return f"Model(Optimizer={self.optim})" # 部分实例化,不想全实例化,只实例化部分参数
# yaml中加上_partial_: true,表示只实例化部分参数
@hydra.main(version_base=None, config_path=".", config_name="config")
def my_app(cfg: DictConfig) -> None: model = instantiate(cfg.model) print(model) if __name__ == "__main__": my_app()
instantiate(cfg.model)- 创建
Model实例。 - 在创建
Model时,需要传入optim_partial参数。 - 这个
optim_partial参数来自配置中的子字典,它包含_target_,_partial_: true,algo: SGD。
- 创建
instantiate处理_partial_: true- 因为它看到了
_partial_: true,所以 不会立即创建Optimizer对象。 - 相反,它返回一个 可调用对象(一个 partial 函数),这个可调用对象已经绑定了
algo='SGD',但还没有绑定lr。 - 返回值类似于:
partial(Optimizer, algo='SGD')。
- 因为它看到了
- 将这个可调用对象赋值给
optim_partial
在Model的构造函数中:optim_partial就是上面那个 partial 函数。- 调用
optim_partial(lr=0.1)时,它会将lr=0.1补充进去,然后完整调用Optimizer(algo='SGD', lr=0.1),最终得到真正的Optimizer实例。 - 这个实例被赋值给
self.optim。
- 最终打印
model
递归实例化(Recursive Instantiation)
car: _target_: my_app.Car driver: _target_: my_app.Driver name: James Bond age: 7 wheels: - _target_: my_app.Wheel radius: 20 width: 1 - _target_: my_app.Wheel radius: 20 width: 1 - _target_: my_app.Wheel radius: 20 width: 1 - _target_: my_app.Wheel radius: 20 width: 1
递归创建,代码只写了一次,但是wheels四个配置,直接创建了四个
class Driver: def __init__(self, name: str, age: int) -> None: self.name = name self.age = age class Wheel: def __init__(self, radius: int, width: int) -> None: self.radius = radius self.width = width class Car: def __init__(self, driver: Driver, wheels: List[Wheel]): self.driver = driver self.wheels = wheels def drive(self) -> None: print(f"Driver : {self.driver.name}, {len(self.wheels)} wheels") @hydra.main(version_base=None, config_path=".", config_name="config")
def my_app(cfg: DictConfig) -> None: car: Car = instantiate(cfg.car) car.drive() if __name__ == "__main__": my_app()
结构化配置StructuredConfig和dataclass结合
通过 ConfigStore 将 Python 的 dataclass 注册为配置,从而完全替代 YAML 配置文件。
@dataclass
class MySQLConfig: host: str = "localhost" port: int = 3306 # 作用:创建一个ConfigStore对象,并将MySQLConfig类注册到ConfigStore中,以便在Hydra应用程序中使用。
cs = ConfigStore.instance()
# 注册MySQLConfig类到ConfigStore中,指定名称为"config",以便在Hydra应用程序中通过名称引用该配置类。
cs.store(name="config", node=MySQLConfig) # 使用@hydra.main装饰器,指定配置文件名称为"config",并将MySQLConfig类作为参数传递给my_app函数,以便在函数中使用配置对象。
@hydra.main(version_base=None, config_name="config")
def my_app(cfg: MySQLConfig) -> None: print(f"Host: {cfg.host}, port: {cfg.port}") if __name__ == "__main__": my_app()
Hydra 结构化配置的嵌套用法:用多个 dataclass 组合成一个大的配置类,并通过 ConfigStore 注册,从而完全替代 YAML 文件。
相当于一个类中有俩子类
@dataclass
class MySQLConfig: host: str = "localhost" port: int = 3306 @dataclass
class UserInterface: title: str = "My app" width: int = 1024 height: int = 768 @dataclass
class MyConfig: db: MySQLConfig = MySQLConfig() ui: UserInterface = UserInterface() cs = ConfigStore.instance()
cs.store(name="config", node=MyConfig) # 这里直接调用的是和dataclass结合的配置文件类MyConfig,cfg就是MyConfig的实例了,可以直接访问它的属性
@hydra.main(version_base=None, config_name="config")
def my_app(cfg: MyConfig) -> None: print(f"Title={cfg.ui.title}, size={cfg.ui.width}x{cfg.ui.height} pixels") if __name__ == "__main__": my_app()
配置组
db: Any:Config中的db字段类型是Any,可以接受任意类型的对象。- 命令行必须指定:
python my_app.py +db=mysql
因为Config.db没有默认值,Hydra 不知道要加载哪个数据库配置,所以必须通过命令行+db=mysql告诉它选择"db"组下的mysql选项。
@dataclass
class MySQLConfig: driver: str = "mysql" host: str = "localhost" port: int = 3306 @dataclass
class PostGreSQLConfig: driver: str = "postgresql" host: str = "localhost" port: int = 5432 timeout: int = 10 @dataclass
class Config: # 这里的db字段被注解为Any类型,这意味着它可以接受任何类型的值。 # 我们是在命令行中输入参数:python my_app.py +db=mysql,这样Hydra会根据输入的参数来决定db字段的具体类型。 db: Any cs = ConfigStore.instance()
cs.store(name="config", node=Config)
# 注册MySQLConfig类到ConfigStore中,指定名称为"mysql",以便在Hydra应用程序中通过名称引用该配置类。
# 指定group="db"表示这个配置类属于"db"组,这样在使用时可以通过"group=db, name=mysql"来引用它。
cs.store(group="db", name="mysql", node=MySQLConfig)
cs.store(group="db", name="postgresql", node=PostGreSQLConfig) @hydra.main(version_base=None, config_name="config")
def my_app(cfg: Config) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()
配置继承
这里很简单,继承类就是继承配置了
@dataclass
class DBConfig: host: str = "localhost" port: int = MISSING driver: str = MISSING @dataclass
class MySQLConfig(DBConfig): # 这是继承父类DBConfig driver: str = "mysql" port: int = 3306 @dataclass
class PostGreSQLConfig(DBConfig): driver: str = "postgresql" port: int = 5432 timeout: int = 10 @dataclass
class Config: # We can now annotate db as DBConfig which # improves both static and dynamic type safety. db: DBConfig cs = ConfigStore.instance()
cs.store(name="config", node=Config)
cs.store(group="db", name="mysql", node=MySQLConfig)
cs.store(group="db", name="postgresql", node=PostGreSQLConfig) # python my_app_with_inheritance.py +db=mysql
@hydra.main(version_base=None, config_name="config")
def my_app(cfg: Config) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()
defaults为配置组提供默认值
@dataclass
class MySQLConfig: driver: str = "mysql" host: str = "localhost" port: int = 3306 user: str = "omry" password: str = "secret" @dataclass
class PostGreSQLConfig: driver: str = "postgresql" host: str = "localhost" port: int = 5432 timeout: int = 10 user: str = "postgres_user" password: str = "drowssap" defaults = [ # config group name db will load config named mysql {"db": "mysql"}
] @dataclass
class Config: # this is unfortunately verbose due to @dataclass limitations # 这里有显式的defaults字段,Hydra会根据defaults列表来填充db字段 # 所以命令行调用的时候,不用+,python my_app.py db=mysql defaults: List[Any] = field(default_factory=lambda: defaults) # Hydra will populate this field based on the defaults list db: Any = MISSING cs = ConfigStore.instance()
cs.store(group="db", name="mysql", node=MySQLConfig)
cs.store(group="db", name="postgresql", node=PostGreSQLConfig)
cs.store(name="config", node=Config) @hydra.main(version_base=None, config_name="config")
def my_app(cfg: Config) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()
