我如何递归地遍历两个字典,根据另一个字典修改原始字典?

3

我希望能够遍历一个字典(其中包含许多字符串、字典和字典列表),并将其与另一个字典进行比较。

这里是一个例子:

data = {
  "topic": "Seniors' Health Care Freedom Act of 2007",
  "foo": "bar",
  "last_update": "2011-08-29T20:47:44Z",
  "organisations": [
    {
      "organization_id": "22973",
      "name": "National Health Federation",
      "bar": "baz"
    },
    {
      "organization_id": "27059",
      "name": "A Christian Perspective on Health Issues"
    },
]}

validate = {
  "topic": None,
  "last_update": "next_update",
  "organisations": [
      {
        "organization_id": None,
        "name": None
      }
    ]
}

基本上,如果项目在“数据”中存在,但当前点的“验证”中不存在,那么应将其从数据中删除。

因此,在这种情况下,我希望从数据字典中删除data ["foo"]和data ["organisations"] [x] ["bar"]。

此外,如果验证中的键具有字符串值并且不是“None”,则我希望将数据中的键名称更新为它,即“last_update”应更改为“next_update”。

我不确定在Python中如何有效地实现这一点,我的当前版本可以删除“foo”,但我正在尝试删除类似organizations[x][bar]的嵌套键。

这是我的当前尝试:

def func1(data, validate, parent = None):
  for k, v in sorted(data.items()):
    if not parent:
      if k not in validate:
        data.pop(k, None)

    if isinstance(v, dict):
        func1(v, validate)
    elif isinstance(v, list):
      for val in v:
          func1(val, validate, parent = k)

func1(data, validate)

我曾试着使用类似这样的方法来比较键,但是发现如果数据有额外的键(似乎会删除错误的键),就不起作用了,因为字典是无序的,所以对我没有帮助:
for (k, v), (k2, v2) in zip(sorted(data.items()), sorted(validate.items())):

我读过类似的帖子,例如如何递归地从多维(深度未知)python字典中删除某些键?,但这似乎使用了一个扁平集合进行过滤,因此它没有考虑到键位于字典中的位置,这对我很重要 - 因为“last_update”可能出现在其他列表中,我需要保留它。
3个回答

1

这里是一个简单的递归函数。嗯,它曾经很简单;但是我添加了大量检查,现在它变成了一个if森林。

def validate_the_data(data, validate):
  for key in list(data.keys()):
    if key not in validate:
      del data[key]
    elif validate[key] is not None:
      if isinstance(data[key], dict):
        validate_the_data(data[key], validate[key])
      elif isinstance(data[key], list):
        for subdata, subvalidate in zip(data[key], validate[key]):
          if isinstance(subdata, dict) and isinstance(subvalidate, dict):
            validate_the_data(subdata, subvalidate)
      else:
        data[key] = validate[key]

如何工作:如果data[key]是一个字典并且key是有效的,则我们希望检查data[key]中的键与validate[key]中的键是否匹配。因此,我们进行递归调用,但是在递归调用中,我们将validate替换为validate[key]。同样,如果data[key]是一个列表,则也要进行相同的操作。 假设:如果data中的列表包含非字典元素,则上述代码将失败;或者当validate[key]存在但不是字典或None时,data[key]是一个字典;或者当validate[key]存在但不是列表或None时,data[key]是一个列表。

关于if语句顺序的重要说明: if/else/if/elif/else的顺序很重要。特别是在我们没有列表的情况下,只有在这种情况下才会执行data[key] = validate[key]。如果validate[key]是一个列表,那么data[key] = validate[key]将导致data[key]成为同一个列表,而不是列表的副本,这显然不是您想要的。

关于list(data.keys())的重要说明: 我使用了迭代for key in list(data.keys()):,而不是for key in data:for key, value in data:。通常,这不是迭代字典的首选方式。但是我们在for循环中使用了del来从字典中删除值,这将干扰迭代。因此,我们需要在删除任何元素之前获取密钥列表,然后使用该列表进行迭代。


如果'subdata'不是一个字典,这种方法不会失败吗?在最后一行之前,您可能需要添加类型检查。 - jeremyr
@jeremyr 嗯,是的,它会失败。我只检查 data[key] 是否是字典或列表;更安全的做法是还要检查 validate[key] 是否相应地为字典或键。 - Stef
这个问题现在已经被修复了。感谢 @jeremyr。 - Stef
我忍不住自己试了一下 :) - jeremyr

0
有趣的问题!为了避免大量的if...else...,您需要找到一种方法,无论传入值的类型如何都允许递归。
因此,我认为您需要以下规则:
  1. 如果validate中的任何值为None,则应保留data中的值。
  2. 如果datavalidate的值是字典,仅保留在validate中存在的data键,并对其他键递归应用这些规则。
  3. 如果datavalidate的值是列表,则仅保留在validate中存在的data项,并对其他项递归应用这些规则。
  4. 如果validate中的任何值不是None且规则(2)和(3)不适用,则应使用validate中的值替换data中的值。
这是我的建议:
def sanitize(data1, data2):
    """Sanitize *data1* depending on *data2*
    """
    # If value2 is None, simply return value1
    if data2 is None:
        return data1

    # Update value1 recursively if both values are dictionaries.
    elif isinstance(data1, dict) and isinstance(data2, dict):
        return {
            key: sanitize(_value, data2.get(key))
            for key, _value in data1.items()
            if key in data2
        }

    # Update value1 recursively if both values are lists.
    elif isinstance(data1, list) and isinstance(data2, list):
        return [
            sanitize(subvalue1, subvalue2)
            for subvalue1, subvalue2
            in zip(data1, data2)
        ]

    # Otherwise, simply return value2.
    return data2

使用您的值,您将获得以下输出:

> sanitize(data, validate)
{
    'topic': "Seniors' Health Care Freedom Act of 2007",
    'last_update': 'next_update',
    'organisations': [
        {
            'organization_id': '22973',
            'name': 'National Health Federation'
        }
    ]
}

根据规则3,我认为您想要从data中删除所有不在validate中的列表项,因此删除了"organisations"中的第二个项。

如果规则3应该是:

  1. 如果datavalidate的值是列表,则递归地将这些规则应用于其他项。

然后,您可以简单地将zip函数替换为itertools.zip_longest


0
字典和列表推导式可以快速解决这个问题 -
def from_schema(t, s):
  if isinstance(t, dict) and isinstance(s, dict):
    return { v if isinstance(v, str) else k: from_schema(t[k], v) for (k, v) in s.items() if k in t }
  elif isinstance(t, list) and isinstance(s, list):
    return [ from_schema(v, s[0]) for v in t if s ]
  else:
    return t

一些换行可能会使理解更加...易于理解 -
def from_schema(t, s):
  if isinstance(t, dict) and isinstance(s, dict):
    return \
      { v if isinstance(v, str) else k: from_schema(t[k], v)
          for (k, v) in s.items()
            if k in t
      }
  elif isinstance(t, list) and isinstance(s, list):
    return \
      [ from_schema(v, s[0])
          for v in t
            if s 
      ]
  else:
    return t

result = from_schema(data, validate)
print(result)

{
    "topic": "Seniors' Health Care Freedom Act of 2007",
    "next_update": "2011-08-29T20:47:44Z",
    "organisations": [
        {
            "organization_id": "22973",
            "name": "National Health Federation"
        },
        {
            "organization_id": "27059",
            "name": "A Christian Perspective on Health Issues"
        }
    ]
}

你能解释一下 [ from_schema(v, s[0]) for v in t if s ] 吗?为什么在列表推导式中要对 s 进行测试? - Stef
@Stef 很好的问题。风险在于如果模式s为空,则s[0]可能会引发IndexError异常。通过在推导式中添加if s条件,我们可以避免这种陷阱。 - Mulan
你为什么更喜欢那种形式,即整个列表只有一个 if,而不是每个元素都有一个 if[from_schema(v, s[0]) for v in t] if s else [] - Stef
那也不错,我之所以选择这样做是为了保持一致性。处理复合数据的两个代码分支都使用推导式返回一个新的字典或列表。将其中一个改为使用“外部”if会稍微破坏一致性。如果你的意思是建议性能会更好,我认为这是微观优化。特别是当你考虑到if s是一个静态(不变)条件,编译后的程序可能会进行分支预测优化。 - Mulan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接