defaultdict与字典元素初始化的区别

4
我正在尝试优化一个脚本的性能,该脚本为给定的每个单词在词典中查找相似单词。
每个唯一的单词都将被分割成字母 n-gram,并且对于每个 n-gram,词典将返回包含相同字母 n-gram 的单词列表。然后将此列表中的每个单词作为键添加到一个字典中,并将其值增加一。这样就可以得到具有相应频率分数的相似单词的字典。
word_dict = {}
get = word_dict.get
for letter_n_gram in word:
    for entry in lexicon[n_gram]:
        word_dict[entry] = get(entry, 0) + 1

这个实现方式是可行的,但是通过将dict替换为collections.defaultdict可以加快脚本运行速度。

word_dd = defaultdict(int)
for letter_n_gram in word:
    for entry in lexicon[n_gram]:
        word_dd[entry] += 1

没有其他代码被改变。

我原以为这两个代码片段(最重要的是添加分数)应该以完全相同的方式工作,即如果键存在,则将其值增加1,如果不存在,则创建键并将值设置为1。

然而,在运行新代码后,一些键的值为0,我认为这在逻辑上是不可能的。

我的逻辑或对defaultdict功能的了解是否有误?如果不是,那么如何将word_dd中的任何值设置为0?

编辑:我也非常确定脚本的其他部分不会影响这些结果,因为我立即使用以下方法测试字典:

for item in word_dd.iteritems():
    if item[1] == 0:
        print "Found zero value element"
        break

哪些键的值为0?你确定这些键已经在字典中了吗? - thefourtheye
2
你如何测试值?任何键访问都将创建该键;因此,word_dd ['nonesuch']不会分配,但为您创建值。 - Martijn Pieters
测试问题中添加的值 - Deutherius
1
您对 defaultdict 的理解似乎很好:您发布的代码不可能出现 0 in word_dd.values() 为True的情形。您确定您在发布的两个代码片段之间没有涉及到 word_dd 的任何代码吗?此外,只有当默认值计算代价昂贵时,默认字典才会比 dict.get/dict.setdefault 快得多 - 常量整数明显不是。在这里考虑使用它的原因是它使您的代码更加 简单,而不是更快。 - lvc
3个回答

6

当您访问 defaultdict 中的键时,如果不存在,则会自动创建。由于我们使用 int 作为默认工厂函数,它会创建键并赋予默认值 0。

from collections import defaultdict
d = defaultdict(int)
print d["a"]
# 0
print d
# defaultdict(<type 'int'>, {'a': 0})

因此,在访问密钥之前,您应该确保它存在于defaultdict实例中,就像这样:
print "a" in d
# False

1
这就是 defaultdict 优化的全部意义。它消除了其他情况下 get(entry, 0) 的开销。 - Two-Bit Alchemist
修改了我的问题,请重新阐述。 - Deutherius
@Deutherius 如果 1 在 ddict 中不存在,它将被创建并使用默认值 0。我在答案中解释了这种行为,请查看。 - thefourtheye
我原本以为 word_dict.iteritems() 会返回一个字典中现有项的迭代器 - 在我的测试循环中,item 是一个元组 (key, value),因此 1 是一个索引,而不是一个字典查询。 - Deutherius
@Deutherius 我又错了,非常抱歉。你能否提供一个样本数据集来重现这个问题? - thefourtheye
显示剩余2条评论

6

任何访问键的项目都将实现其值:

>>> from collections import defaultdict
>>> d = defaultdict(int)
>>> d['foo']
0

使用包含关系来测试存在性,如下所示:
>>> 'bar' in d
False
>>> 'foo' in d
True

如果您正在计算n-gram,您可能需要查看collections.Counter():

from collections import Counter

word_counter = Counter()
for letter_n_gram in word:
    word_counter.update(lexicon[n_gram])

在这里Counter.update()将更新lexicon[n_gram]表达式返回的所有条目的计数。

defaultdict(int)类似,Counter()对象会自动实现值,默认为整数0


根据我对thefourtheye的最新回答,我认为我没有错误地测试零值的存在。word_dd.iteritems()不应该创建任何元素,AFAIK。 我一定会查看collections.Counter,谢谢。 - Deutherius
@Deutherius:不,.iteritems()不会。然而,你在问题中发布的代码也不会。 - Martijn Pieters
2
@Deutherius: 你能够在字典中拥有0值的唯一方式是通过访问键(即在字典中键尚未定义的任何地方使用dictionary[key])或直接赋值为0(通过赋值、增量赋值或.update()方法)。 - Martijn Pieters

0

哎呀,我终于发现了代码的错误。

由于我的输入集中有许多具有相同测试单词的连续n-gram,因此我只会为每个唯一的测试单词创建类似单词的字典一次。

然后将该字典用于其他目的,其中键被多次测试。如果该字典是collections.defaultdict且默认工厂未设置为None,则当然可能会创建零值元素。

然而,在每个主循环中都进行了零值元素的测试-因此找到了在上一个循环中创建的零值元素。

将测试代码缩进到适当的部分后,结果正如预期-在创建之后没有零值元素。

我为我的问题错误和不完整的构造向大家道歉-其他人无法找到错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接