【问题标题】:Hash table collision in python set?python集中的哈希表冲突?
【发布时间】:2019-12-01 09:38:52
【问题描述】:

如何在 python set 2 中拥有相同的元素?是python的bug吗?

type(data_chunks)
<class 'set'>

len(data_chunks)
43130

same = [x for x in data_chunks if x.md5==chunk.md5]
[<Model.Chunk.Chunk o...x0DB40870>, <Model.Chunk.Chunk o...x0DB40870>]

len(same)
2

same[0] is same[1]
True

same[0] == same[1]
True

len(set(same))
1

但是当我用它构建字典时,重复被删除了!

len({k:k.product_id for k in data_chunks})
43129

为什么它适用于字典而不适用于集合?我虽然认为这是哈希表中的冲突,但实际上重复的对象是同一个对象,所以在添加下一个元素时在集合查找中找不到它(?)

附加信息:

  • Chunk 定义了 __hash____eq__ 方法
  • python 3.7.2
  • 我已经意识到 Chunk 有一些会引发错误的属性 - 这应该不重要,因为它们没有被调用
  • 执行代码行是: data_chunks = data_chunks | another_set
  • 在 vscode 中调试会话期间的交互式提示
  • 在运行代码时有时
  • 但在这一调试会话期间,从 data_chunks 创建新集的长度始终相同

编辑

块实现

class Chunk(object):
    def __init__(self,
                 md5,
                 size=None,
                 compressedMd5=None,
                 # ... (more elements)
                 product_id=None):
       self.md5 = md5
       self.product_id = product_id
       # (etc.)

    def __eq__(self, other):
        if self.compressedMd5:
            return self.compressedMd5 == other.compressedMd5 and self.product_id == other.product_id
        return self.md5 == other.md5 and self.product_id == other.product_id

    def __hash__(self):
        return self.name.__hash__()

    @property
    def name(self):
        return self.compressedMd5 if self.compressedMd5 is not None else self.md5

===================

编辑 好的,所以代码中的内容如下:

repository - json 描述符

chunking_strategy = ... - 主要是存储设置的类,例如块将被压缩

result_handler = Strategy.DefaultResultHandler(repository) 在存储库中生成块对象的唯一哈希:块和相应的文件映射。 稍后它将调用压缩作业,然后设置compressedMd5 和现有块的其他属性。

generation_strategy = Strategy.CachingGenerationStrategy(
            result_handler,
            Settings().extra_io_threads,
        )

data_chunks = Strategy.DepotChunker(repository, chunking_strategy, generation_strategy)()

在 DeputChunker init 上:todo 分块作业是根据 chunking_strategy 设置准备的。然后generation_strategy.__call__ 方法处理所有作业:根据先前定义的 Chunk 对象将文件切成小块。这是在multiprocessing.Pool 中完成的。在创建物理块后,它们会检查 md5,并使用compressedMd5compressedSizeproduct_id更新 块对象。 然后(只是在更改 Chunk 对象之后)将块对象添加到 set。 这套是从DepotChunker返回的

然后将压缩后的块保存在缓存中。


然后所有 data_chunk 都在搜索具有小尺寸的小对象,从中创建由合并的小文件组成的物理块(在内存缓冲区中)。让我们称它们为 smallFilesChunks。它们被添加到data_chunks

     sfChunk = Chunk(
                sfCompressedContentMD5,  # yes I see that this is compressed md5 - it was intended for some reason I don't know
                size=sfSize,
                compressedMd5=sfCompressedContentMD5,
                compressedSize=sfCompressedSize,
                product_id=productId
            )
        if not sfChunk in data_chunks:  # purly sanity check
            data_chunks.add(sfcChunk)

最后元文件被创建,它们也被分块并添加到data_chunks

然后元文件被转储,它们也被分块。

for depot in manifest_depots:
        data_chunks = data_chunks | simpleChunker(depot)

此时调试器会话从一开始就被记录了

【问题讨论】:

  • 可能还需要查看Chunk 的实现,以确定散列的任何问题
  • 是的,我们来看看哈希函数和比较方法。
  • 另外,Chunk 是可变的吗?
  • @Mesco 请编辑您的帖子以包含适当的最小可重现示例 (stackoverflow.com/help/minimal-reproducible-example)
  • 似乎__hash__compressedMd5md5 上使用__hash__ 方法 - 他们是否以相同的方式实现__hash__?您正在构建列表 same 仅基于 md5 属性的相等性,而 __hash__ is 和 __eq__ 也在检查其他属性。

标签: python hash set hashtable hash-collision


【解决方案1】:

一个问题是__eq__ 对于一对具有compressedMd5 而另一个没有的对象是不可交换的(即其compressedMd5 设置为None)。这意味着可以构造两个对象ab,这样a == b 和同时b != a

一个相关的问题是__eq____hash__ 在类似情况下彼此不一致(如果self.compressedMd5None__eq__ 将拒绝查看other.compressedMd5。)

可变性也可能是一个问题,如下例所示:

class Chunk(object):
  def __init__(self, md5):
    self.md5 = md5

  def __hash__(self):
    return hash(self.md5)

s = set()
chunk = Chunk('42')
s.add(chunk)
chunk.md5 = '123'
s.add(chunk)
print(s)

在我的电脑上,这会产生set([&lt;__main__.Chunk object at 0x106d03390&gt;, &lt;__main__.Chunk object at 0x106d03390&gt;]),即同一个对象在集合中出现两次。

如果您更改md5 或设置/取消设置/更改compressedMd5,您的代码中可能会发生类似的情况。

【讨论】:

  • 谢谢,我明白了。但目前 len([x for x in data_chunks if x.compressedMd5 is None]) == 0 和事实上 - same 数组由两个相同的元素 object at 0x0DB40870
  • @Mesco:Chunk 对象是可变的吗?
  • 但是我们不会期待len(set(same)) -> 2吗? AFAIK setdict 也使用哈希表来构造密钥,所以我不知道为什么 set(data_chunks) 有骗子而 {k:k.product_id for k in data_chunks} 没有。如果我不得不猜测,我会说我同意 NPE 的观点,即 __eq____hash__ 在这里没有进行相同的比较,但这很奇怪。关于Chunks 上的操作,这里有一些我们不知道的地方。
  • @Mesco:对我来说,这些症状强烈暗示了Chunk 的可变性(特别是,块的哈希值在块的生命周期内发生变化)。
  • @NPE 有更多线程用于生成块,然后上传到服务器。在基于 Chunk 对象物理生成块之后,对它们进行压缩。然后用 compressMd5 更新 Chunks 对象。然后将块添加到data_chunks 集合中。 (有关详细信息,请参阅我的更新)
【解决方案2】:

好的,我知道这是一种难以可靠重现的行为,因此我们可以通过 ATM 提供的所有可能导致此类问题的建议......

除了 NPE 已经提到的之外,Chunk 是可变的确实存在潜在问题:md5compressedMd5 属性都可以随时更改,因此无法保证 hash(chunk) 的结果要稳定。您可能想在此处检查您的代码库是否有最终的 gremlins。如果您发现任何代码更新了这些属性之一 chunk 已添加到集合中,那么您可能就是罪魁祸首。 FWIW 记得 Python 从来没有隐含地复制任何类似这样的东西:

chunks = set()

def make_chunk(md5, ...)
   c = Chunk(md5, ...)
   chunks.add(c)
   return c

def do_something_bad(chunk):
    chunk.md5 = something_else

def main():
   c = make_chunk()
   # ... lots of code here
   do_something_bad(c)

将反映 chunks 的变化并搞砸一切(注意:是的,您很可能已经知道这一点,但这对于来自更主流语言的人们来说是一个非常常见的陷阱)

NB :当然,如果有任何东西改变了其中一个属性,那只是一个问题,但是将它们设为只读仍然会更安全(好吧,至少根据 Python 对“只读”和“安全”的定义,即xD)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-09-28
    • 1970-01-01
    • 2010-10-18
    • 1970-01-01
    • 1970-01-01
    • 2021-10-10
    • 2012-05-13
    • 1970-01-01
    相关资源
    最近更新 更多