如何检查两个字典是否相等,除了某些字段?

12
有两个字典:old 和 updated。我想检查它们是否相等,除了 "status"、"latitude" 和 "longitude" 键之外。
assert old_dict['status'] != updated_dict['status']
assert old_dict['latitude'] != updated_dict['latitude']
assert old_dict['longitude'] != updated_dict['longitude']

for field in ('status', 'latitude', 'longitude'):
    updated_dict.pop(field)
    old_dict.pop(field)

assert old_dict == updated_dict

这个问题有更符合Python风格的解决方法吗?

1
你的代码中是否必须像暗示的那样状态(和纬度/经度)不同? - L3viathan
你是否想要检查预期的键也不同 (例如状态),还是你最初的断言只是为了证明这些字典本来被认为是不相等的?也就是说,给定两个完全相同的字典,如果期望的键的值相等,你希望比较的结果为假吗? - Dunes
你是只对更Pythonic的解决方案感兴趣,还是也对效率/速度感兴趣呢? - norok2
@norok2 你们能不能提供一下“效率/速度”的解决方案? - indigo153
我已经添加了一些时间记录。它们会有一点依赖于我在解决您提出的问题的歧义的方式以及被测试数据的实际情况。 - norok2
8个回答

3

有点不太正统的建议,请听我解释:

differing = {"status", "latitude", "longitude"}
assert all(
    (old_dict[key] != updated_dict[key]) == (key in differing)
    for key in old_dict
)

对于每个key,我们断言如果且仅如果这个key是不同的键之一,那么相应的值就会有所不同。


1
如果 update_dict 中存在 old_dict 中没有的键,这会怎样?不清楚这是否与 OP 有关。 - jpp
我喜欢这种方法(假设键确实相同),但我更倾向于使用类似于(old_dict[key] == updated_dict[key]) ^ (key in differing)的东西。 - tobias_k

3

您可以断言两个字典项的对称差集是由这三个键组成的:

assert {k for k, _ in old_dict.items() ^ updated_dict.items()} == {'status', 'latitude', 'longitude'}

2

您可以通过字典推导式过滤两个字典,然后检查它们是否相等:

def compare_dicts(d1, d2, exc_keys):
    dct1 = {k: v for k, v in d1.items() if k not in exc_keys}
    dct2 = {k: v for k, v in d2.items() if k not in exc_keys}
    return dct1 == dct2

assert compare_dicts(old_dict, updated_dict, {'status', 'latitude', 'longitude'})

1
我会假设完整的测试需要 异常排除 字典键必须不同,并且这两个字典可能没有所有相同的键。
一些测试用例可以编写如下:
import string
import random

random.seed(0)

keys = list(string.ascii_letters)
excluded = 'r', 'm', 'e'

# the original dict
base_dict = {key: random.randint(1, 100) for key in keys}

# some keys, different from excluded are different
unequal_dict = {key: (val if key not in ('q') else random.randint(1, 100)) for key, val in base_dict.items()}

# only the excluded keys are different
equal_dict = {key: (val if key not in excluded else random.randint(1, 100)) for key, val in base_dict.items()}

# only some of the excluded keys are different
partial_dict = {key: (val if key not in excluded[1:] else random.randint(1, 100)) for key, val in base_dict.items()}

# a copy of the base dict
identical_dict = base_dict.copy()

# one more key is added
not_same_keys_dict = base_dict.copy()
not_same_keys_dict['aa'] = 1

现在,old_dict基本上是base_dict,而unequal_dictequal_dictpartial_dictidentical_dictnot_same_keys_dict涵盖了不同的边角情况。

然后,我们定义一些辅助函数来同时测试不同的输入。

def multi_test(func, many_args):
    return [func(*args) for args in many_args]

many_args = (
    (base_dict, unequal_dict, updated),
    (base_dict, equal_dict, updated),
    (base_dict, partial_dict, updated),
    (base_dict, identical_dict, updated),
    (base_dict, not_same_keys_dict, updated))

原始代码经过函数化后的样子如下:

import copy

def dicts_equal_except_orig(dict1, dict2, excluded):
    dict1 = dict1.copy()
    dict2 = dict2.copy()
    result = True
    for key in excluded:
        result = result and (dict1[key] != dict2[key])
        dict1.pop(key)
        dict2.pop(key)
    result = result and (dict1 == dict2)
    return result

print(multi_test(dicts_equal_except_orig, many_args))
# [False, True, False, False, False]

%timeit multi_test(dicts_equal_except_orig, many_args)
# 13.1 µs ± 183 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

这是在假设要比较的字典有一些不共同键的情况下,生成测试中可以获得的最快速度。 所有其他方法都明显较慢,尽管可能更加清晰,并且在某些情况下甚至可能更快,例如当需要排除的键数很大时等等。 此外,如果不需要 not_same_key 情况,即字典始终具有相同的键,则基于 all() 的解决方案将更快,因为它们将具有显式短路,并且可以通过更改来进行转换:
keys = dict1.keys() | dict2.keys()

转换成例如

keys = dict1.keys()

并删除其他类似于 if key in dict1 and key in dict2 的检查。


为了完整起见,我报告了我测试过的所有其他选项:

我的解决方案和明确的测试

def dicts_equal_except(dict1, dict2, excluded):
    keys = dict1.keys() | dict2.keys()
    return all(
        (dict1[key] != dict2[key] if key in excluded else dict1[key] == dict2[key])
        if key in dict1 and key in dict2 else False
        for key in keys)


print(multi_test(dicts_equal_except, many_args))
# [False, True, False, False, False]

%timeit multi_test(dicts_equal_except, many_args)
# 28.3 µs ± 186 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

一个对@blhsing解决方案进行函数化的实现
def check_dict_except(dict1, dict2, excluded):
    return {k for k, _ in dict1.items() ^ dict2.items()} == set(excluded)

print(multi_test(check_dict_except, many_args))
# [False, True, False, False, False]

%timeit multi_test(check_dict_except, many_args)
# 30.8 µs ± 498 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

@L3viathan的解决方案变体

def dicts_equal_all(dict1, dict2, excluded):
    keys = dict1.keys() | dict2.keys()
    return all((dict1[key] == dict2[key]) ^ (key in excluded) for key in keys)

print(multi_test(dicts_equal_all, many_args))
# [False, True, False, False, False]

%timeit multi_test(dicts_equal_all, many_args)
# 29.7 µs ± 316 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

并且

def dicts_equal_all2(dict1, dict2, excluded):
    keys = dict1.keys() | dict2.keys()
    return all((dict1[key] != dict2[key]) == (key in excluded) for key in keys)

print(multi_test(dicts_equal_all2, many_args))
# [False, True, False, False, False]

%timeit multi_test(dicts_equal_all2, many_args)
# 29.9 µs ± 435 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

一种对@jpp答案的改编:

def compare_dicts(dict1, dict2, excluded):
    filter_dict1 = {key: val for key, val in dict1.items() if key not in excluded}
    filter_dict2 = {key: val for key, val in dict2.items() if key not in excluded}
    excluded_dict1 = {key: dict1[key] for key in excluded if key in dict1}
    excluded_dict2 = {key: dict2[key] for key in excluded if key in dict2}
    return filter_dict1 == filter_dict2 and all(dict1[key] != dict2[key] if key in dict1 and key in dict2 else False for key in excluded)

print(multi_test(compare_dicts, many_args))
# [False, True, False, False, False]

%timeit multi_test(compare_dicts, many_args)
# 57.5 µs ± 960 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)


0

试试这个:

old_dict.keys() == updated_dict.keys()

如果old_dict的键是update_dict的子集,则返回True。


1
这不是OP的意思。old_dicts实际上也有这三个键,但只是具有不同的值,因此在OP的代码中断言不等式。 - blhsing
@blhsing 我已经更新了我的回答,谢谢提醒。 - Mehrdad Pedramfar

0

几行代码,但应该非常快,因为它不会改变要比较的字典对的结构。

EXC = {"status", "latitude", "longitude"}
saved = {}

for key in EXC:
   saved[key], updated_dict[key] = updated_dict[key], old_dict[key]
cmp = old_dict == updated_dict
for key in EXC:
   old_dict[key], updated_dict[key] = updated_dict[key], saved[key]

0

怎么样?

ignore = {'status', 'latitude', 'longitude'}
equal = all([old_value == new[key]
             for key, old_value in old.items()
             if key not in ignore])

这个循环会遍历一次字典(我认为没有绕过这个的方法)。


1
我认为这个测试不完整,因为它假设两个字典具有相同的键。但如果它们不同,测试可能会产生KeyError或误报。 - VPfB

0

有一个叫做 deepdiff 的库。

from deepdiff import DeepDiff
expected = {'couponCode': '1234', 'usedOnOrder': ['1'], 'usedOnOwners': [], 'sth':{'sth':'sth'}}
actual = {'couponCode': '1234', 'usedOnOrder': ['1'], 'usedOnOwners': [], 'usageCount': 1}
DeepDiff(expected, actual)

它将以以下字典的形式输出任何差异

{'dictionary_item_added': [root['usageCount']],
 'dictionary_item_removed': [root['sth']]}

在传入之前,您可以删除预期的字段。如果输入字典相同,则返回一个空字典

以下是您的案例示例

from deepdiff import DeepDiff
expected = {'status': '1234', 'latitude': ['1'], 'longitude': [], 'sth':{'sth':'sth'}}
actual = {'status': '123', 'latitude': ['2'], 'longitude': [], 'sth':{'sth':'sth'}}
ignored_fields = ['status', 'latitude', 'longitude']


dict_filter = lambda x: {k:v for k,v in x.items() if k not in ignored_fields}

diff = DeepDiff(dict_filter(expected), dict_filter(actual))
assert not diff, diff


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