在我的电脑上,有一个文件/etc/dictionaries-common/words 包含大量英文单词:
>>> with open("/etc/dictionaries-common/words") as f:
... words = [line.strip() for line in f]
...
>>> "python" in words
True
>>> "BDFL" in words
False
让我们创建一个字典来存储所有这些单词的长度:
>>> word_lengths = {w: len(w) for w in words}
>>> word_lengths["parrot"]
6
而且,只是为了好玩,我们将对原始单词列表进行洗牌:
>>> from random import shuffle
>>> shuffle(words)
>>> words[:5]
["Willie's", 'Araceli', 'accessed', 'engagingly', 'hobnobs']
嗯,hobnobs。无论如何......现在我们已经与words 搞混了一点,我们变得有点偏执(可能出于同样的原因,我们渴望 hobnobs),我们想要检查所有单词我们的word_lengths 字典在我们混合之后仍然在words 中:
>>> all(w in words for w in word_lengths)
True
嗯,我们到了那里,但在我的机器上花了三分钟多的时间——至少足够多吃几块美味的饼干了。仔细想想,原因很明显:我们有...
>>> len(words)
99171
... 将近十万个单词要检查,对于字典中的每一个单词,Python 都必须搜索我们混合的单词列表,直到找到匹配项。它不必总是检查整个列表,但平均而言,每次将是五万个单词(或列表的一半),总共 50,000 × 100,000 = 5,000,000,000 次测试。即使在这个神奇的技术时代,50 亿也是很多。
为了绝对确定(我通常不那么偏执;通常我只是困),让我们反过来检查一下,确保 words 中的所有内容仍在 word_lengths 中:
>>> all(w in word_lengths for w in words)
True
嘿,什么?这次是,就像,十分之一秒!是什么赋予了?你吓到我了,伙计……嘿,我的饼干呢?我刚才有,我很确定。
与可以按任何旧顺序排列的列表不同(因此确保其中有一些项目意味着依次检查每个项目直到找到它),字典更有效一些。派对上可能没那么有趣,但是,嘿,让它负责音乐,你知道吗?
字典无情高效的秘诀在于,对于每个项目,字典都会根据其内容计算键的哈希值(实际上只是一个整数),并使用该哈希值将项目存储在内存中的特定位置。然后,当您查找该项目时,它会再次计算密钥内容的哈希值,对自己说“好的,"python",哈希值是7036520087640895475 ...是的,我知道我必须把它放在哪里,然后”,然后直接到正确的内存位置找到它。所以这一次,它只需要进行十万次检查,而不是五十亿次。
这有点像将所有 CD 整齐地按字母顺序放在架子上,而不是从它们的盒子里随意堆放在扬声器顶部。字典知道它在哪里,我告诉你。
但要让字典保持一致,这是有代价的。还记得我说过字典会根据项目的内容计算散列吗?那么,如果内容发生变化会发生什么?对于不成问题的不可变对象——它们的内容不能改变——但是可变对象,根据定义,可以改变它们的内容,当它们改变时,它们的哈希值(如果他们甚至有一个)也会改变。这很酷,显然,不是每个人都想被放在一个盒子里,我明白了,但是如果哈希值发生了变化,字典就无法确定它把东西放在哪里了。
就好像 Joy Division 把他们的名字改成了 New Order,而现在你不知道你把 Blue Monday 的 12 英寸混音放在哪里了。这行不通。
所以,字典有一个规则:如果你想成为一把钥匙,不要去改变。