You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在结合Pydantic的YAML文件中复用变量

在YAML中复用变量并加载为Pydantic模型

需求描述

我希望加载YAML文件并创建Pydantic BaseModel对象,同时实现YAML文件内部的变量复用。示例YAML文件如下:

config:
  variables:
    root_level: DEBUG
    my_var: "TEST"

  handlers_logs:
    - class: $my_var #<--- 需要替换的变量
      level_threshold: STATS
      block_level_filter: true
      disable: false
      args:
        hosts: $my_var #<--- 需要替换的变量
        topic: _stats

现有代码如下:

import os
from pprint import pprint

import yaml
from pydantic import BaseModel
from typing import Dict, Optional, Any
from yaml.parser import ParserError

class BaseLogModel(BaseModel):
    class Config:
        use_enum_values = True
        allow_population_by_field_name = True

class Config(BaseLogModel):
    variables: Optional[Dict[str, str]]
    handlers_logs: Any

def load_config(filename) -> Optional[Config]:
    if not os.path.exists(filename):
        return None

    with open(filename) as f:
        try:
            config_file = yaml.load(f.read(), Loader=yaml.SafeLoader)
            if config_file is not None and isinstance(config_file, dict):
                config_data = config_file["config"]
            else:
                return None
        except ParserError as e:
            return None

    return Config.parse_obj(config_data)

def main():
    config = load_config("config.yml")
    pprint(config)

当前输出:

Config(variables={'root_level': 'DEBUG', 'my_var': 'TEST'}, handlers_logs=[{'class': '$my_var', 'level_threshold': 'STATS', 'block_level_filter': True, 'disable': False, 'args': {'hosts': '$my_var', 'topic': '_stats'}}])

我希望将其中的$my_var替换为"TEST",避免重复编写相同值,请问能否通过Pydantic或其他YAML库实现这一需求?


解决方案

方法1:加载YAML后手动递归替换变量

先加载原始YAML数据,提取variables中的变量映射,再递归遍历整个配置数据,将所有$变量名格式的字符串替换为对应的值,最后传给Pydantic解析。

修改后的load_config函数:

def replace_variables(data, variables):
    if isinstance(data, dict):
        return {k: replace_variables(v, variables) for k, v in data.items()}
    elif isinstance(data, list):
        return [replace_variables(item, variables) for item in data]
    elif isinstance(data, str) and data.startswith('$'):
        var_name = data[1:]
        return variables.get(var_name, data)  # 变量不存在则保留原字符串
    else:
        return data

def load_config(filename) -> Optional[Config]:
    if not os.path.exists(filename):
        return None

    with open(filename) as f:
        try:
            config_file = yaml.load(f.read(), Loader=yaml.SafeLoader)
            if config_file is not None and isinstance(config_file, dict):
                config_data = config_file["config"]
            else:
                return None
        except ParserError as e:
            return None

    # 提取变量并替换
    variables = config_data.get('variables', {})
    config_data = replace_variables(config_data, variables)

    return Config.parse_obj(config_data)

运行后输出:

Config(variables={'root_level': 'DEBUG', 'my_var': 'TEST'}, handlers_logs=[{'class': 'TEST', 'level_threshold': 'STATS', 'block_level_filter': True, 'disable': False, 'args': {'hosts': 'TEST', 'topic': '_stats'}}])

方法2:自定义YAML Loader实现变量替换

继承yaml.SafeLoader实现自定义Loader,在加载YAML时直接完成变量替换,无需后续处理。

示例代码:

import yaml
from yaml.nodes import ScalarNode

class VariableLoader(yaml.SafeLoader):
    def __init__(self, stream, variables):
        super().__init__(stream)
        self.variables = variables

    def construct_scalar(self, node):
        value = super().construct_scalar(node)
        if isinstance(value, str) and value.startswith('$'):
            var_name = value[1:]
            return self.variables.get(var_name, value)
        return value

def load_config(filename) -> Optional[Config]:
    if not os.path.exists(filename):
        return None

    with open(filename) as f:
        try:
            # 先加载一次获取variables
            raw_config = yaml.load(f.read(), Loader=yaml.SafeLoader)
            if not raw_config or not isinstance(raw_config, dict):
                return None
            variables = raw_config.get('config', {}).get('variables', {})
            
            # 重新打开文件,用自定义Loader加载并替换变量
            f.seek(0)
            config_file = yaml.load(f.read(), Loader=lambda stream: VariableLoader(stream, variables))
            if config_file is not None and isinstance(config_file, dict):
                config_data = config_file["config"]
            else:
                return None
        except ParserError as e:
            return None

    return Config.parse_obj(config_data)

方法3:利用Pydantic的validator在模型解析时替换

通过Pydantic的@validator装饰器,将变量替换逻辑绑定到模型上,在验证阶段自动完成替换。

修改模型类:

from pydantic import validator

class Config(BaseLogModel):
    variables: Optional[Dict[str, str]] = {}
    handlers_logs: Any

    @validator('handlers_logs')
    def replace_handlers_variables(cls, v, values):
        variables = values.get('variables', {})
        
        def _replace(data):
            if isinstance(data, dict):
                return {k: _replace(val) for k, val in data.items()}
            elif isinstance(data, list):
                return [_replace(item) for item in data]
            elif isinstance(data, str) and data.startswith('$'):
                var_name = data[1:]
                return variables.get(var_name, data)
            else:
                return data
        
        return _replace(v)

这种方法无需修改加载函数,Pydantic解析模型时会自动处理变量替换。


内容的提问来源于stack exchange,提问作者Plaoo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 19:25:32