Python中具有相同键的散列字典

piv4azn7  于 2023-01-01  发布在  Python
关注(0)|答案(3)|浏览(121)
    • 背景:**

我有一组键相同但值不同的日志。键保证是str,值可以是strNone
例如:

logs = [
 {'id': '1234', 'error': None, 'fruit': 'orange'},
 {'id': '12345', 'error': None, 'fruit': 'apple'}
]

有时候这些日志是重复的,相同的键和值对于字典来说是相同的。

{'id': '1234', 'error': None, 'fruit': 'orange'}
{'id': '1234', 'error': None, 'fruit': 'orange'}

我正在按如下方式处理这些问题:

already_seen = set()
for log in logs:
    log_hash = compute_hash(log)
    if log_hash in already_seen:
        continue
    already_seen.add(log_hash)

最初,我使用了以下哈希函数:

def compute_hash(log_dict: dict):
    return hash(log_dict.values())

但是这个 * 对于不同的字典 * 给出了相同的哈希值,所以我把这个函数改为:

def compute_hash(log_dict: dict):
    return hash(frozenset(log_dict.values()))

现在它工作正常(据我所注意到的)。

    • 问题**:

1.为什么最初的方法不起作用?

  • 当前方法是否正确?正确性:
  • 具有相同值的dict的相同哈希值。
  • 具有不同值的字典的不同散列值。
  • 有没有更好或更简单的方法来解决这个问题?

示例:

dict_1 = {
    "date": "2022-12-30 18:04:35.488",
    "tenant": "hz",
    "action": "REFRESH_KUBE_WORKLOAD",
    "code": "SERVER_SENT_REQ_TO_AGENT",
    "message_id": "ae62aa57-b155-452f-b7bd-740c18110aa7",
    "error": None,
}
dict_2 = {
    "date": "2022-12-30 18:04:35.479",
    "tenant": "hz",
    "action": "REFRESH_KUBE_WORKLOAD",
    "code": "SERVER_FAIL",
    "message_id": "5b0eb8d9-9c6a-4c6d-bd27-b03ac0b865e8",
    "error": " Timeout",
}

print(hash(dict_1.values()) == hash(dict_2.values()))
a9wyjsp7

a9wyjsp71#

在Python 3.x中,dict上的.values返回dict_values的一个示例(名称没有在内置函数中定义,但是示例是这样标识自己的):

>>> dv = {1:2, 3:4}.values()
>>> dv
dict_values([2, 4])

此类旨在作为现有基础对象上的视图,并且不定义其自己的__hash__方法。object是直接超类,因此对__hash__的调用回退到object.__hash__

>>> dv.__class__.__bases__
(<class 'object'>,)
>>> dv.__class__.__hash__
<slot wrapper '__hash__' of 'object' objects>

事实上,dict_values不能合理地实现__hash__,因为它是(间接)可变的-作为视图,它依赖于底层的dict:

>>> d = {1:2}
>>> dv = d.values()
>>> d[3] = 4
>>> dv
dict_values([2, 4])

由于没有一种明显的通用方法来散列任何对象,同时还考虑其实际属性,因此默认方法 * 不 * 考虑属性,而是简单地基于对象标识。例如,在我的平台上,结果如下所示:

Python 3.8.10 (default, Nov 14 2022, 12:59:47) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> dv = {1:2, 3:4}.values()
>>> bin(id(dv))
'0b11111110101110011010010110000001010101011110000'
>>> bin(hash(dv))
'0b1111111010111001101001011000000101010101111'

换句话说:

>>> hash(dv) == id(dv) // 16
True

因此,如果原始代码中的compute_hash被临时对象重复调用,它不会给出有用的结果--结果不依赖于对象的内容,并且通常是相同的,因为循环中的临时(即直接GCd)对象通常会在同一个内存位置结束。
另一方面,frozenset是为了长期存储一些不可变的数据,因此,定义一个__hash__是很重要和有用的,它做到了:

>>> f = frozenset(dv)
>>> bin(id(f))
'0b11111110101110011010001011101000110001011100000'
>>> bin(hash(f))
'0b101111010001101001001111100001000001100111011101101100000110001'

我们可以在GitHub上看到frozenset.__hash__的参考实现。(毕竟,它是一个不可变的对象,所以除了节省内存之外,没有理由重复计算),否则它会迭代哈希表,将条目的子计算异或在一起,并做一些额外的修正。所以我们可以确信这将是所讨论的任务的正确散列值。
很难想象能更简单地解决这个问题,因为对不工作代码的唯一更改是单一类型强制,但是,如果所有日志条目都有相同的键集,那么将其硬编码到哈希中可能更有意义:

def hash_log_entry(entry):
    return hash(entry['id']) ^ hash(entry['error']) ^ hash(entry['fruit'])

然而,现有的守则在技术上并不正确;毕竟,散列值只包含有限数量的位,而我们可以散列任意大的输入集,因此,完全不同的集可能被错误地注册为already_seen(尽管这是极不可能的)。更重要的是,如果我们的散列只关心log.values,而不考虑顺序,那么当然它会将相同的散列值给予另一个log,其中相同的一组值被分配给不同的键例如{'id': '1234', 'error': None, 'fruit': 'orange'}{'id': '1234', 'error': 'orange', 'fruit': None}
为了解决这个问题,一种方法是使用dict,以便跟踪原始值,并根据匹配的哈希值对其进行分组(因为我们现在实际上需要考虑冲突):

from collections import defaultdict

# We can't use a set to remember dicts with the same custom hash -
# that's the problem we were trying to work around in the first place!
already_seen = defaultdict(list)
for log in logs:
    log_hash = compute_hash(log)
    if log in already_seen.get(log_hash, ()):
        continue
    already_seen[log_hash].append(log)

但这实际上是重复了dict应该为我们实现的检查逻辑。
另一种方法是使用我们自己的日志条目类型:

from dataclasses import dataclass
from typing import Optional

# It's not very well explained in the documentation, but
# when the type is declared to be immutable using `frozen=True`,
# it will have a __hash__ generated as well.
@dataclass(frozen=True)
class LogEntry:
    id: str
    error: Optional[str]
    fruit: str

# convert existing logs
logs = [LogEntry(**d) for d in logs]

# Now, since the type is hashable, we can directly deduplicate with a set:
set(logs)
wqsoz72f

wqsoz72f2#

你有一些可行的答案,但我认为你可能把事情弄得过于复杂了。下面是我会在你的原始代码上做的快速修复。

logs = [
    {'id': '1234', 'error': None, 'fruit': 'orange'},
    {'id': '1234', 'error': None, 'fruit': 'orange'},
    {'id': '12345', 'error': None, 'fruit': 'apple'}, 
]

def get_values(log: dict):
    return tuple(log.values())

unique_logs = set(map(get_values, logs))
for log in unique_logs:
    print(log)

(“12345”、无、“苹果”)
(“1234”、无、“橙色”)

hmmo2u0o

hmmo2u0o3#

1.你不能仅仅根据散列来判断“已经看到”。散列比实际数据小,但可能会有冲突,这是一种权衡。使用散列对日志进行分组,然后检查是否相等。
1.仍然会有碰撞。

  1. dict已经通过hash对键进行分组并检查是否相等,不需要重新发明这个。您的日志是字典,因为它们是可变的,所以无法散列。一个简单的方法是使用json.dumps()将dict转换为字符串。或者找到类似于frozenset但针对dict的方法。
already_seen = set()
for log in logs:
    log_hash = json.dumps(log, sort_keys=True)
    if log_hash in already_seen:
        continue
    already_seen.add(log_hash)

相关问题