获取dict中类似值的键的最有效方法

时间:2011-07-19 14:46:20

标签: python

我有一个对象词典:

# I have thousands of objects in my real world scenario
dic = {'k1':obj1, 'k2':obj2, 'k3':obj3, ...}
# keys are string
# objs are MyObject

编辑:很抱歉在这个问题上有疑问。这是确切的类和like()函数:

class MyObject(object):
    def __init__(self, period, dimensions):
        self.id = None
        self.period = period # period is etree.Element
        self.dimensions = dict() # id -> lxml.XMLElements
        for dim in dimensions:
            # there must be only one child: the typed dimension
            self.dimensions[dim.get('dimension')] = dim[0]
        self._hash = None

    def __eq__(self, other):
        return isinstance(other, MyObject)
            and self.period == other.period
            and self.dimensions == other.dimensions

    def like(self, other):
        return (other is not None \
            and self.period == other.period \
           and self.dimensions.keys() == other.dimensions.keys())

我想知道如何在字典dic中找到与给定值val类似的对象的最佳实现。相当于:

的东西
def find_keys(dic, val):
    return [v for v in dic if v.like(val))

但是这个方法太慢了,因为我在find-keys()上有数千次迭代,字典中有数千个对象。

现在,我已在这些对象上实现了__hash__(self),并将密钥添加为属性:

    def __hash__(self):
        if self._hash is None:
            self._hash = hash(self.periodtype) ^ \
                hash(tuple(sorted(self.dimensions.values())))
        return self._hash

然后,我构建了一个

的查找字典
hash_dic = { hash(obj1): [obj1], hash(obj2): [obj2, obj3] }

这种新的搜索方法要快得多:

def find_keys_fast(dic, val):
    prefetched=hash_dic[hash(val)]
    return [x.key for x in prefetched if x.like(val)]

由于__hash__是集和字典内部使用的本机函数,我能做的更快或更优雅吗?

6 个答案:

答案 0 :(得分:3)

由于我不知道您的数据的结构或您正在寻找的相似性的性质,我只能猜测可能有用的东西。但也许您可以使用词典构建某种prefix tree。如:

trie = {'a':{'b':{'e':{}, 's':{}}, 'c':{'t':{}, 'k':{}}}}

这些最常用于查找具有公共前缀的字符串,但也许某种意义上,对象中的数据可以表示为字符串。这似乎特别好,如果有一些顺序可以放入数据,以便字符串中的早期数据必须比较为==。我想我甚至可以想象特里的叶子包括所有类似的,而不是所有严格等同的物体。

如何使用特里的小玩具示例:

>>> trie = {'a':{'b':{'e':{}, 's':{}}, 'c':{'t':{}, 'k':{}}}}
>>> def rec_print(trie, accum=''):
...     if trie:
...         for k in trie:
...             rec_print(trie[k], accum + k)
...     else:
...         print accum
... 
>>> rec_print(trie)
ack
act
abs
abe

答案 1 :(得分:2)

你的方法对我来说非常好如果你只想要几个对象的相似对象。

自己的类定义__hash__()也没有错。

如果您想在“喜欢”对象的类中对所有对象进行分组,那么有一种更快的方法:您可以使用{{1}的传递性 }} 方法。实际上,如果like()like(obj0, obj1)为真,则like(obj1, obj2)会自动为真,无需进一步计算。这意味着您可以使用高效的

直接将所有对象分组
like(obj0, obj2)

这会自动将对象组合在一起。这更简单,并且可能比定义signature = lambda obj: (obj.period, obj.typed_dimensions.keys()) sorted_objs = sorted(dic.values(), key=signature) objs_in_like_classes = [list(group) for (_, group) in itertools.groupby(sorted_objs, key=signature)] __hash__()以及自己进行预取更快,因为__eq__()使用groupby()的传递性。

PS :我更喜欢迈克尔·巴伯的“通过可散列签名分类的类似对象的字典”方法来解决这个问题,因为它可能有点快,而且更通用,因为没有排序是必要的。)

如果您需要保持当前的方法,可以稍微清楚一点:您可以检查是否确实需要这些==测试。如果您想正确处理比较(if other is not None),您还应该处理__eq__属于不同类的情况(而不是只检查other的身份);一个None会这样做。如果您只比较类isinstance()的对象,则like()可能会有所不同。在这种情况下,您的代码应该类似于:

MyObject

这会使代码更清晰(但不会更快)。

您可以通过而不是def __eq__(self, other): if isinstance(other, MyObject): return (self.period == other.period and self.typed_dimensions == other.typed_dimensions) else: return False def like(self, other): return (self.period == other.period # No need for a backslash and self.typed_dimensions.keys() == other.typed_dimensions.keys()) __hash__()并通过撰写:

来加快self._hash = None功能的速度
__init__()

事实上,def __hash__(self): try: return self._hash except AttributeError: self._hash = (hash(self.periodtype) ^ hash(tuple(sorted(self.dimensions.values())))) return self._hash 在没有引发异常的情况下很快(在你的情况下,这是迄今为止最常见的情况)。

至于你的try,它可以通过以下方式非常有效地构建:

hash_dict

(也许这就是你已经在做的事情)。

答案 2 :(得分:2)

现在我们可以看到like的实现,一个非常简单的方法似乎可行 - 比其他答案简单得多。在signature上定义新的MyObject方法:

def signature(self):
    return (self.period, frozenset(self.dimensions.keys()))

然后遍历对象:

import collections
sig_keys = collections.defaultdict(set)
for k, obj in dic.iteritems():
    sig_keys[obj.signature()].add(k)

有了这个,sig_keys.values()给出了相似对象的所有标识符集。如果更好的话,可以直接构造对象列表:

sig_objs = collections.defaultdict(list)
for obj in dic.itervalues():
    sig_objs[obj.signature()].append(obj)

如果需要,您可以将__hash__定义为return hash(self.signature())或等效。

答案 3 :(得分:0)

我没有完全遵循您的预取步骤,因为您没有详细解释它,但也许您可以预先计算完整的结果?

如果like方法看起来像是y值的索引,我会做的另一种可能性。

index = { 10 : [obj1], 12 : [obj2, obj3] ,... }之类的东西,它们的键是对象'y属性。因此,你最终得到:

def find_keys_in_constant_time(dic, val):
    precomputed = index[val.y]
    return precomputed

当然,它也会返回原始val对象,但原始方法也是如此。

答案 4 :(得分:0)

注意在看到like的实施后,所描述的方法看起来比必要的更复杂。我将其留在这里,因为该方法可以推广到模糊的相似性度量,例如,至少50%的维度必须相同。

你正在做的事情看起来很像inverted index,虽然如果不知道如何实施like就不可能说出来。对于反向索引,可以使用可能的对象值作为字典键,映射到采用这些值的对象的列表(或其他集合)。使用多个属性,您可以创建多个字典,处理不同类型的对象值。然后,在倒排索引中查找对象的所有属性,根据所有属性确定每个对象的聚合相似度。

要充分利用倒排索引,最好考虑从一个函数返回所有类似对象。这有助于您只处理一次可能的“喜欢”对象。作为一个极端的例子,只有当所有属性都相同时,你才可能有另一个对象;类似的对象是在倒排索引的所有相应列表中找到的那些对象。要获得所有类似的对象,您可以将列表转换为集合并获取交集。

以下是Python中的内容,略微缩写为专注于维度 - 包含period的扩展应该很容易。存在从对象标识符字符串到dic中的对象的映射。因此,您可以通过从维度映射到具有该维度的对象标识符的集合来构建反向索引。可能会这样做:

import collections
invind = collections.defaultdict(set)
for k, obj in dic.iteritems():
    for d in obj.dimensions:
        invind[d].add(k)

现在假设您要查找与特定对象test_obj具有相同维度的所有对象。只需查找具有至少一个维度的对象标识符集,并获取所有这些集合的交集。编写此类查询的简明方法是:

import operator
similar_keys = reduce(operator.and_, [invind[d] for d in test_ojb.dimensions])
similar_objects = [dic[k] for k in similar_keys]

operate.and_将计算集合交叉点,reduce将其扩展到整个集合列表。这通常不是实施交叉口的最快方法;相反,你可以使用集合的intersection_update方法就地操作一组结果,一旦集合为空就提前停止---我会留下细节,因为它们很容易但很冗长。

这种方法的优点是任何没有共同维度的对象都不会完全进行比较。根据尺寸的大小,可能会大大减少测试次数。您可以进一步采用这种想法,例如,使用成对的共同维度作为倒排索引中的键。生成密钥的成本更高,但通常会减少对象标识符集的大小 - 一些实验,或只是对维度的良好理解,应该有助于做出正确的权衡。

要在比较中包含句点,只需将另一个反向索引映射句点添加到对象标识符集。扩展类似对象的查询应该很简单。

答案 5 :(得分:0)

很难回答他的问题,因为我不知道你的要求是什么。我要做的是创建一些相关类并用它填充你的项目。如何实现它主要取决于你like函数的属性。如果你的关系是对称的(即,当a和b之类似a时,a就像b一样),你可以聚类相关项而不是迭代项,你迭代聚类并与其中的任何项进行比较;如果匹配,则群集中的所有项目都与您的元素相关。

但是,您示例中的关系不是对称的,因此您可能需要另一种方法。您仍然可以按yz进行聚类,并查找元素,将相应cluster_y的交集与cluster_z的联合保持z大于或等于正在查找的元素起来。但是,如果值差异很大,则可能会增加大量内存开销。

您可以通过检查您的关系属性来做其他事情。如果您提供了更多详细信息,我们可以提供帮助。