两个字典相交
- 2025-03-13 09:07:00
- admin 原创
- 110
问题描述:
我正在开发一个基于倒排索引的搜索程序。索引本身是一个字典,其键是术语,其值本身是短文档的字典,以 ID 号为键,文本内容为值。
因此,要对两个术语执行“AND”搜索,我需要将它们的帖子列表(词典)相交。在 Python 中,有什么清晰(不一定非常聪明)的方法可以做到这一点?我首先尝试了以下方法iter
:
p1 = index[term1]
p2 = index[term2]
i1 = iter(p1)
i2 = iter(p2)
while ... # not sure of the 'iter != end 'syntax in this case
...
解决方案 1:
一个鲜为人知的事实是,您不需要构造set
s 来执行此操作:
Python 3
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'c': 3}
print(d1.keys() & d2.keys()) # {'b'}
Python 2
在 Python 2 中,我们keys
用替换。 ( ) 和( )viewkeys
同样适用。values
`viewvaluesitems
viewitems`
In [78]: d1 = {'a': 1, 'b': 2}
In [79]: d2 = {'b': 2, 'c': 3}
In [80]: d1.viewkeys() & d2.viewkeys()
Out[80]: {'b'}
来自以下文档viewitems
:
In [113]: d1.viewitems??
Type: builtin_function_or_method
String Form:<built-in method viewitems of dict object at 0x64a61b0>
Docstring: D.viewitems() -> a set-like object providing a view on D's items
对于较大的dict
s 这也比构造set
s 然后使它们相交稍微快一些:
In [122]: d1 = {i: rand() for i in range(10000)}
In [123]: d2 = {i: rand() for i in range(10000)}
In [124]: timeit d1.viewkeys() & d2.viewkeys()
1000 loops, best of 3: 714 µs per loop
In [125]: %%timeit
s1 = set(d1)
s2 = set(d2)
res = s1 & s2
1000 loops, best of 3: 805 µs per loop
For smaller `dict`s `set` construction is faster:
In [126]: d1 = {'a': 1, 'b': 2}
In [127]: d2 = {'b': 2, 'c': 3}
In [128]: timeit d1.viewkeys() & d2.viewkeys()
1000000 loops, best of 3: 591 ns per loop
In [129]: %%timeit
s1 = set(d1)
s2 = set(d2)
res = s1 & s2
1000000 loops, best of 3: 477 ns per loop
我们在这里比较纳秒,这对您可能重要也可能不重要。无论如何,您都会得到一个set
,因此使用viewkeys
/keys
可以消除一些混乱。
解决方案 2:
一般来说,要在 Python 中构造字典的交集,可以首先使用&
运算符计算字典键集合的交集(在 Python 3 中,字典键是集合类对象):
dict_a = {"a": 1, "b": 2}
dict_b = {"a": 2, "c": 3}
intersection = dict_a.keys() & dict_b.keys() # {'a'}
在 Python 2 上,你必须自己将字典键转换为集合:
keys_a = set(dict_a.keys())
keys_b = set(dict_b.keys())
intersection = keys_a & keys_b
然后,给定键的交集,您就可以根据需要构建值的交集。您必须在这里做出选择,因为集合交集的概念并没有告诉您如果相关值不同该怎么做。(这大概就是为什么&
Python 中没有直接为字典定义交集运算符的原因)。
在这种情况下,听起来同一个键的值是相等的,因此您可以从其中一个字典中选择值:
dict_of_dicts_a = {"a": {"x":1}, "b": {"y":3}}
dict_of_dicts_b = {"a": {"x":1}, "c": {"z":4}}
shared_keys = dict_of_dicts_a.keys() & dict_of_dicts_b.keys()
# values equal so choose values from a:
dict_intersection = {k: dict_of_dicts_a[k] for k in shared_keys } # {"a":{"x":1}}
其他合理的组合值的方法取决于字典中值的类型以及它们所代表的内容。例如,您可能还需要字典的字典共享键的值的并集。由于字典的并集不依赖于值,因此它定义明确,在 Python 中,您可以使用|
运算符获取它:
# union of values for each key in the intersection:
dict_intersection_2 = { k: dict_of_dicts_a[k] | dict_of_dicts_b[k] for k in shared_keys }
在这种情况下,如果两个字典中的键值相同"a"
,则结果将是相同的。
解决方案 3:
In [1]: d1 = {'a':1, 'b':4, 'f':3}
In [2]: d2 = {'a':1, 'b':4, 'd':2}
In [3]: d = {x:d1[x] for x in d1 if x in d2}
In [4]: d
Out[4]: {'a': 1, 'b': 4}
解决方案 4:
在 Python 3 中,你可以使用
intersection = dict(dict1.items() & dict2.items())
union = dict(dict1.items() | dict2.items())
difference = dict(dict1.items() ^ dict2.items())
解决方案 5:
到目前为止,没有任何解决方案能够解决 N 个字典相交的一般情况。
N
因此,如果您想处理任意字典的交集:
from functools import reduce
def dict_intersection(*dict_list):
return reduce(lambda a,b: dict(a.items() & b.items()), dict_list)
a = {k:k for k in range(0,5)} # {0: 0, 1: 1, 2: 2, 3: 3, 4: 4}
b = {k:k for k in range(2,7)} # {2: 2, 3: 3, 4: 4, 5: 5, 6: 6}
c = {k:k for k in range(3,8)} # {3: 3, 4: 4, 5: 5, 6: 6, 7: 7}
dict_intersection(a,b,c) # {3:3, 4:4}
# or if you have a list of dicts
dicts = [{k:k for k in range(0+n,5+n)} for n in (0,2,3)] # == [a,b,c]
dict_intersection(*dicts) # {3:3, 4:4}
使用functools.reduce
可以在字典列表上进行一次迭代即可完成操作,而不像某些解决方案那样需要多次循环。它也不执行任何额外的条件语句。
权衡
更改dict_intersection_v1
为,dict_intersection_v2
我们可以看到它在字典列表和/或字典较大时执行速度更快(设计适当的实验来测试哪个因素更大超出了此解决方案的范围)。此性能提升是由于减少了字典实例的数量。
def dict_intersection_v1(*dict_list):
return reduce(lambda a,b: dict(a.items() & b.items()), dict_list)
def dict_intersection_v2(*dict_list):
return dict(reduce(lambda a,b: a & b, (d.items() for d in dict_list)))
dict_lst1 = [{k:k for k in range(0+n,5+n)} for n in (0,2,3)] # = [a,b,c]
dict_lst2 = [{k:k for k in range(0,50,n)} for n in range(1,5)]]
dict_lst3 = [{k:k for k in range(0,500,n)} for n in range(40)]
dict_lst4 = [{k:k for k in range(0+n,500+n)} for n in range(400)]
字典列表 | kv 对数 | dict_intersection_v1 | dict_intersection_v2 | 相对差异 |
---|---|---|---|---|
1 | 15 | 每循环 808 ns ± 4.31 ns(7 次运行的平均值 ± 标准差,每次 1000000 次循环) | 每循环 821 ns ± 0.785 ns(7 次运行的平均值 ± 标准差,每次 1000000 个循环) | + 1.6% |
2 | 105 | 每循环 3.14 µs ± 11.9 ns(7 次运行的平均值 ± 标准差,每次 100000 次循环) | 每循环 2.38 µs ± 5.76 ns(7 次运行的平均值 ± 标准差,每次 100000 次循环) | -24.2% |
3 | 2155 | 每循环 36.9 µs ± 61.9 ns(7 次运行的平均值 ± 标准差,每次 10000 次循环) | 每循环 25.1 µs ± 131 ns(7 次运行的平均值 ± 标准差,每次 10000 次循环) | -32.0% |
4 | 200_000 | 每循环 9.08 毫秒 ± 22 微秒(7 次运行的平均值 ± 标准差,每次 100 次循环) | 每循环 4.88 毫秒 ± 5.31 微秒(7 次运行的平均值 ± 标准差,每次 100 次循环) | -46.3% |
结果的回归dict_lst1
主要归因于每次交集后创建字典的开销与dict.items()
生成器内的调用(以及 python 的一般函数调用开销)之间的开销差异。
注意:我确实测试了使用预先计算的字典列表
dict.items()
而不是 v2 动态构建生成器。我测试了在时间之外和时间之内传递预先计算的列表,虽然这在统计上是显著的,但分别少于 30 μs 和 10 μs。如果你想获得这些收益,可以考虑使用其他语言或 Cython 可能会更好。
解决方案 6:
通过键和值找到完全交集
d1 = {'a':1}
d2 = {'b':2, 'a':1}
{x:d1[x] for x in d1 if x in d2 and d1[x] == d2[x]}
>> {'a':1}
解决方案 7:
好的,这是上面代码在 Python3 中的通用版本。它经过优化,可以使用足够快的理解和集合类字典视图。
函数与任意多个字典相交并返回一个具有公共键和每个公共键的一组公共值的字典:
def dict_intersect(*dicts):
comm_keys = dicts[0].keys()
for d in dicts[1:]:
# intersect keys first
comm_keys &= d.keys()
# then build a result dict with nested comprehension
result = {key:{d[key] for d in dicts} for key in comm_keys}
return result
使用示例:
a = {1: 'ba', 2: 'boon', 3: 'spam', 4:'eggs'}
b = {1: 'ham', 2:'baboon', 3: 'sausages'}
c = {1: 'more eggs', 3: 'cabbage'}
res = dict_intersect(a, b, c)
# Here is res (the order of values may vary) :
# {1: {'ham', 'more eggs', 'ba'}, 3: {'spam', 'sausages', 'cabbage'}}
此处的字典值必须是可哈希的,如果不是,那么您可以简单地将括号 { } 更改为列表 [ ]:
result = {key:[d[key] for d in dicts] for key in comm_keys}
解决方案 8:
您的问题不够精确,无法给出单一的答案。
关键交叉点
如果您想要ID
与帖子(感谢 James)相交,请执行以下操作:
common_ids = p1.keys() & p2.keys()
但是,如果你想迭代文档,你必须考虑哪个帖子具有优先级,我认为是p1
。迭代文档common_ids
,collections.ChainMap
将最有用:
from collections import ChainMap
intersection = {id: document
for id, document in ChainMap(p1, p2)
if id in common_ids}
for id, document in intersection:
...
或者,如果您不想创建单独的intersection
词典:
from collections import ChainMap
posts = ChainMap(p1, p2)
for id in common_ids:
document = posts[id]
项目交集
如果您想要交叉两个帖子的项目,即匹配ID
s 和文档,请使用下面的代码(感谢 DCPY)。但是,这仅在您寻找重复项时才有用。
duplicates = dict(p1.items() & p2.items())
for id, document in duplicates:
...
迭代
p1
“AND”p2
。
如果通过“ 'AND'搜索”并且使用iter
您想要搜索的两个帖子,那么collections.ChainMap
最好再次迭代多个帖子中的(几乎)所有项目:
from collections import ChainMap
for id, document in ChainMap(p1, p2):
...
解决方案 9:
只需用一个简单的类包装字典实例,即可获得所需的两个值
class DictionaryIntersection(object):
def __init__(self,dictA,dictB):
self.dictA = dictA
self.dictB = dictB
def __getitem__(self,attr):
if attr not in self.dictA or attr not in self.dictB:
raise KeyError('Not in both dictionaries,key: %s' % attr)
return self.dictA[attr],self.dictB[attr]
x = {'foo' : 5, 'bar' :6}
y = {'bar' : 'meow' , 'qux' : 8}
z = DictionaryIntersection(x,y)
print z['bar']
解决方案 10:
def two_keys(term_a, term_b, index):
doc_ids = set(index[term_a].keys()) & set(index[term_b].keys())
doc_store = index[term_a] # index[term_b] would work also
return {doc_id: doc_store[doc_id] for doc_id in doc_ids}
def n_keys(terms, index):
doc_ids = set.intersection(*[set(index[term].keys()) for term in terms])
doc_store = index[term[0]]
return {doc_id: doc_store[doc_id] for doc_id in doc_ids}
In [0]: index = {'a': {1: 'a b'},
'b': {1: 'a b'}}
In [1]: two_keys('a','b', index)
Out[1]: {1: 'a b'}
In [2]: n_keys(['a','b'], index)
Out[2]: {1: 'a b'}
我建议更改你的索引
index = {term: {doc_id: doc}}
两个索引,一个用于术语,另一个用于保存值
term_index = {term: set([doc_id])}
doc_store = {doc_id: doc}
这样你就不会存储同一数据的多个副本
扫码咨询,免费领取项目管理大礼包!