我有一些日志数据,如:
logs = [
{'id': '1234', 'error': None, 'fruit': 'orange'},
{'id': '12345', 'error': None, 'fruit': 'apple'}
]
每个法令都有相同的密钥:'id'
、'error'
和'fruit'
(在本示例中)。
我想从这个列表中删除remove duplicates,但是基于dict
和set
的简单方法不起作用,因为我的元素本身就是dict
,即not hashable:
>>> set(logs)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
另一种方法是sort and use itertools.groupby-但dict也不具有可比性,因此这也不起作用:
>>> from itertools import groupby
>>> [k for k, _ in groupby(sorted(logs))]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'dict' and 'dict'
我的想法是为每个日志条目计算一个散列值,并将其存储在set
中进行比较,如下所示:
def compute_hash(log_dict: dict):
return hash(log_dict.values())
def deduplicate(logs):
already_seen = set()
for log in logs:
log_hash = compute_hash(log)
if log_hash in already_seen:
continue
already_seen.add(log_hash)
yield log
但是,我发现compute_hash
对不同的字典给予相同的哈希值,即使是那些内容完全虚假的字典:
>>> logs = [{'id': '123', 'error': None, 'fruit': 'orange'}, {}]
>>> # The empty dict will be removed; every dict seems to get the same hash.
>>> list(deduplicate(logs))
[{'id': '123', 'error': None, 'fruit': 'orange'}]
经过一些实验,我似乎能够通过修改compute_hash
来解决这个问题,如下所示:
def compute_hash(log_dict: dict):
return hash(frozenset(log_dict.values()))
但是,我不明白为什么这会造成不同。为什么原始版本似乎对每个输入dict都给予相同的哈希值?为什么首先要将.values
结果转换为frozenset
来解决这个问题?除此之外:这个算法是正确的吗?或者有一些反例,错误的值将被删除?
3条答案
按热度按时间kpbwa7wx1#
哪里出错了
关于最初的尝试,我想指出的第一件事是它看起来过于工程化了,当输入是可哈希的,手动迭代只需要to preserve order,即使这样,在Python 3.7及更高版本中,我们可以依赖
dict
s的顺序保持属性。仅仅因为它是可散列的并不意味着散列有用
在
log_dict.values()
上调用hash
也不是特别有用,虽然log_dict
是不可哈希的,但它的.values()
(Python 3.x中)是dict_values
类型的示例(名称没有在内置函数中定义,但示例是这样标识自己的),而dict_values
类型**是可哈希的:因此,我们可以很容易地将
.values()
直接用作“散列”:......但这会带来一个新的bug --现在每个散列都会不同:
但是为什么?
因为
dict_values
类型没有定义__hash__
,也没有定义__eq__
。object
是直接超类,所以对这些方法的调用会退回到object
的默认值:事实上,
dict_values
无法合理地实现这些方法,因为它是(间接)可变的-作为视图,它依赖于底层的dict:由于没有一种明显的通用方法来散列任何对象,同时还考虑其实际属性,因此默认方法 * 不 * 考虑属性,而是简单地基于对象标识。例如,在我的平台上,结果如下所示:
换句话说:
因此,如果原始代码中的
compute_hash
被临时对象重复调用,它不会给予有用的结果--结果不依赖于对象的内容,并且通常是相同的,因为循环中的临时(即直接GCd)对象通常会在同一个内存位置结束。(Yes,这意味着对象默认为可散列和等价比较的,
dict
类型本身重写__hash__
以显式禁止它,而奇怪的是重写__eq__
以比较内容。)frozenset
具有有用的哈希另一方面,
frozenset
是用来长期存储一些不可变的数据的,因此定义一个__hash__
是非常重要和有用的,它做到了:字典、散列和冲突检测
尽管这些年来已经有了很多调整和优化,但Python
dict
和set
类型基本上都是based on hash tables。(通常为整数值),然后将该值减小(通常使用模)转换为基础表存储的索引。类似地,当查找值时,散列被计算和简化以便确定在表中何处查找该值。当然,也有可能在这个位置已经存储了其他值,有多种可能的策略来处理这个问题(我最近检查了一下,文献中对它们的命名并不一致),但最重要的是,对于我们的目的来说:当通过键在X1 M22 N1 X中查找值或者检查值在X1 M23 N1 X中的存在时,容器还必须在弄清楚查找的位置之后进行相等性检查,以便确认实际上已经找到了正确的东西。
因此,任何简单地手动计算散列并天真地将这些散列与原始值相关联的方法都将失败。两个输入字典很容易具有相同的计算散列值,* 即使实际上考虑了它们的内容 *。例如,
frozenset
的散列是基于元素散列的异或,因此如果我们的两个输入字典具有相同的值 * 以不同的顺序分配给键 *,散列将是相同的:这样的哈希冲突也有可能发生在完全不相关的值上,但对于64位的哈希值来说,这种情况几乎不可能发生(因为这个值不会被简化,也不会被用作哈希表索引,尽管它的名字是这样的)
显式修复
因此,为了得到正确的代码,我们需要在之后做自己的检查,显式地检查散列到
already_seen
集合中的某个值是否等于之前使用该散列的值,理论上可能有多个这样的值,所以我们需要为每个外部散列记住多个值,也许可以用dict
代替already_seen
。希望这看起来并不令人满意。使用这种方法,我们基本上重新实现了集合和字典的核心逻辑-我们自己计算哈希,从内部存储器(
already_seen
)和中检索相应的值,然后手动检查是否相等(if log in ...
)。从另一个Angular 来看
我们之所以要做这一切--在我们自己的存储中寻找一个哈希值来表示原始的dict--是因为dict是不可哈希的,如果我们直接解决这个问题呢?
也就是说:与其简单地从碰巧是可哈希的原始数据中计算一个值,不如显式地将数据转换为可哈希形式,保留所有信息。换句话说,使用不同的类型来表示数据,而不是
dict
。由于我们所有的输入
dict
都有相同的键,所以很自然的事情就是将它们转换成用户定义类的属性。在Python 3.7及以上版本中,一个简单、自然和明确的方法是使用dataclass,如下所示:文档中没有很好地解释,但是使用
frozen=True
(主要目的是使示例不可变)也会生成__hash__
,根据需要考虑字段;使用slots=True
也会为类型avoiding memory overhead生成__slots__
。从这里开始,转换现有日志就很简单了:
我们可以使用
set
直接进行重复数据消除:或者,使用
dict
保留顺序(在3.7及更高版本中):当然,还有其他的选择,最简单的是从
.values
生成一个tuple
--假设每个日志dict都有其键 * 以相同的顺序 *(同样,假设Python 3.7及更高版本,其中键 * 有 * 一个顺序),这保留了所有 * 有用 * 的信息--.keys
只是为了方便。我们可以使用collections.namedtuple
:这比
dataclass
方法简单,但不太明确(并且没有提供一种优雅的方式来记录字段类型)。v1l68za42#
你有一些可行的答案,但我认为你可能把事情弄得过于复杂了。下面是我会在你的原始代码上做的快速修复。
(“12345”、无、“苹果”)
(“1234”、无、“橙色”)
ncgqoxb03#
1.你不能仅仅根据散列来判断"已经看到"。散列比实际数据小,但可能会有冲突,这是一种权衡。使用散列对日志进行分组,然后检查是否相等。
1.仍然会有碰撞。