Python3.8+ 测试一个 yaml 是否是另一个 yaml 的子集

Sum*_*ron 5 python python-3.x

简而言之,我使用 Yaml 文件作为我正在使用的某些管道/函数的参数配置。在Python中,这是一个嵌套字典,参数本身可以是数组/字典。迭代所有配置文件并搜索指定的值子集将很有帮助,例如

# toy example of all parameters a config file might have
- param_a: 1
- param_b: 
  - b1: 'a'
  - b2: [1,2,3]
Run Code Online (Sandbox Code Playgroud)
# want all configs with these values
- param_a: 1
- param_b: 
  - b2: [1,2,3]
Run Code Online (Sandbox Code Playgroud)

当然,可以对每个嵌套字典进行递归,但我想知道是否有一个经过验证的真实解决方案,而不是重新发明轮子。

我看到了一些相关的问题(希望确认相同的词典)并且弹出Deep Diff 。然而,尚不清楚在测试子集时,DeepDiff 是否会返回所有丢失的键。想法?

现在我正在使用它并假设 yaml 已作为嵌套字典正确加载

def is_config_subset(truth, params):
    '''
    Arguments:
    ----------
        truth (dict): dictionary of parameters to compare to
        params (dict): dictionary of parameters to test
    Returns:
    ----------
        result (bool) whether or not `params` is a subset of `truth`
    '''
    if not type(truth) == type(params): return False
    for key, val in params.items():
        if key not in truth: return False
        if type(val) is dict:
            if not is_config_subset(truth[key], val): 
                return False
        else:            
            if not truth[key] == val: return False
    return True

print(is_config_subset({'a':1, 'b':2}, {'b':2}))
print(is_config_subset({'a':1, 'b':2}, {'b':2, 'c':3}))
print(is_config_subset({'a':1, 'b':2, 'c':[1,2,3]}, {'b':2, 'c':[1,2,3]}))
print(is_config_subset({'a':1, 'b':2, 'c':[1,2,3]}, {'b':2, 'c':[1,2]}))
print(is_config_subset({'a':1, 'b':2, 'c':[1,2,3]}, {'a':2, 'b':2}))

True
False
True
False
False
Run Code Online (Sandbox Code Playgroud)

这可能是一个简单的示例,并不适用于所有情况。

rv.*_*tch 1

我认为最简单(也许也是最有效)的解决方案是定义您自己的递归辅助函数,例如is_subset. 由于 YAML 和 JSON 在格式上非常相似,因此您可以首先将 YAML 数据加载到 Python 类型(alistdict),并将其传递给递归函数,该函数根据预定义的条件执行子集检查。

例如,这里有一个简单(大部分完整)的示例来帮助您入门。

def is_subset(superset, o):
    """Check if `o` is subset of `superset`"""
    ss_type, o_type = type(superset), type(o)

    if ss_type != o_type:
        return False

    # now we know that both `superset` and `o` are the same type

    if o_type is dict:

        for o_key, o_value in o.items():
            ss_value = superset.get(o_key)

            if not is_subset(ss_value, o_value):
                return False
        else:
            return True

    if o_type is list:

        # an empty list can be considered a subset
        if not o_type:
            return True

        # on other hand, if superset list is empty, we return false
        if not superset:
            return False

        first_o_type = type(o[0])
        first_ss_type = type(superset[0])

        if first_o_type != first_ss_type:
            return False

        if first_o_type is dict:
            merged_ss, merged_o = {}, {}
            for v in o:
                if type(v) is dict:  # type check to be safe
                    merged_o.update(v)
                    # On Python 3.9+, the syntax would be:
                    #   merged_o |= v

            for v in superset:
                if type(v) is dict:  # type check to be safe
                    merged_ss.update(v)

            return is_subset(merged_ss, merged_o)

        elif first_o_type is list:
            for idx, o_value in enumerate(o):
                try:
                    ss_value = superset[idx]
                except IndexError:
                    return False

                if not is_subset(ss_value, o_value):
                    return False
            else:
                return True

        else:  # a list of simple types, like [1, 2, 3]
            for o_value in o:
                if o_value not in superset:
                    return False
            else:
                return True

    # it's a simple type - not list or dict
    return superset == o
Run Code Online (Sandbox Code Playgroud)

请注意,它没有涵盖一些边缘情况 - 例如,listYAML 配置中的 s 具有混合数据类型,例如dictstr值的列表。我将让您决定如何处理这些边缘情况,以防也值得涵盖它们。

无论如何,以下是使用我们上面声明的辅助函数的方法:

import yaml


superset_config = yaml.safe_load("""
# toy example of all parameters a config file might have
- param_a: 1
- param_b:
  - b1: 'a'
  - b2: [1,2,3]
""")

subset_config = yaml.safe_load("""
# want all configs with these values
- param_a: 1
- param_b:
  - b2: [1,2,3]
""")


assert is_subset(superset_config, subset_config)

subset_config[0]['param_c'] = 'test'
assert not is_subset(superset_config, subset_config)
Run Code Online (Sandbox Code Playgroud)