Python如何使用defaultdict创建字典的字典和列表

9
我如何使用defaultdict创建字典的字典列表?我遇到了以下错误。
>>> from collections import defaultdict
>>> a=defaultdict()
>>> a["testkey"]=None
>>> a
defaultdict(None, {'testkey': None})
>>> a["testkey"]["list"]=[]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object does not support item assignment

a["testkey"] 为 None,你不能像它是字典一样处理。 - Eugene Soldatov
2
请注意,即使您使用了 defaultdict,将其他内容分配给一个键(例如 a["testkey"] = None)也会替换默认值。 - jonrsharpe
谢谢@jonrsharpe,我会记下来的。 - file2cable
3个回答

21

这有点棘手。你可以像这样创建一个defaultdict的defaultdict:

defaultdict(lambda: defaultdict(list))

2
这是完全没有问题的(受欢迎的),但是由于我有一个关于尽可能避免“lambda”函数的病理学(它们经常被滥用,在总是有更好的选择的情况下,例如使用map/filter与带有内联lambda主体的listcomp/genexpr始终比使用lambda慢;在遇到此类滥用时,我会采取避免它们的措施),这种方法使用一个空的defaultdict(list)的绑定方法来避免lambda并将所有工作推到C层,这(微不足道,快5-10%)提高了运行时速度: defaultdict(defaultdict(list).copy) - ShadowRanger
1
嘿,好酷的想法!我也讨厌lambda。请随意将其编辑到答案中,或添加您自己的答案。 - wim
1
@wim: 当然可以。让我的强迫症发作吧。 :-) - ShadowRanger
1
+1 给你。也许你会喜欢我在这里提出的关于 Lambda 的问题(https://dev59.com/2WrWa4cB1Zd3GeqP-27b)。 - wim

12
略微比使用 lambda 更快:
defaultdict(defaultdict(list).copy)

这个与wim's answer的观察行为相同,但避免了使用lambda,而是使用在CPython中实现的绑定内置方法,这意味着默认值生成不需要执行任何Python字节代码或查找任何名称,并且运行速度更快。在CPython 3.5的微基准测试中,当一个键不存在于访问时所付出的代价看起来比以其他方式等效的lambda低约5-10%。
真正的原因是我讨厌lambda,因为人们滥用它是个坏主意(例如,使用lambda的map/filter总是比等效的listcomp/genexpr更冗长和更慢,但人们仍然以不可辨认的理由这样做),即使在这种情况下,这几乎毫无影响。

更新:自3.8版本开始,这种性能提升已经消失了,使用lambda在简单的微基准测试中(使用ipython),运行时间缩短了约3%(3.8版本)和约7%(3.9版本)。如果您想重现我的测试,请测试以下内容:

>>> from collections import defaultdict
>>> %%timeit dd = defaultdict(lambda: defaultdict(list)); o = object
... dd[o()]

>>> %%timeit dd = defaultdict(defaultdict(list).copy); o = object
... dd[o()]

在缓存中,o = object 可以最小化查找开销,并允许我们创建极其便宜的、保证唯一性的键来访问(强制自动创建 list),而不需要进行其他工作。请注意保留 HTML 标签。
3.8版本的性能改进很可能主要是由于引入了 LOAD_GLOBAL指令的opcode缓存, 从一个完整的dict查找(在内置函数中list有两个)转变为对dict上的版本标签进行快速检查,然后从缓存中廉价地加载,将成本降低约40%,这样做可以减少在lambda中查找defaultdictlist的成本。 3.9的改进可能(不确定)与CPython的内部移动以更优化和支持向量调用代码路径有关,代价是牺牲非向量调用代码路径(相对而言,defaultdict(list).copy路径使用更多),即使在这些改进之前,defaultdict(list).copy也存在一些低效率,而lambda则没有,提供了一些改进的余地。

你好,能展示一下你的基准测试结果吗?我无法重现那个速度提升。 - wim
@wim:这只是一个有意滥用“自动创建”的 ipython 微基准测试,正如我所说,即使是在这种情况下,收益也很小。只需要两行代码%%timeit -r5 dd = defaultdict(defaultdict(list).copy); k = itertools.count(10000).__next__,而“主体”仅为dd[k()]。在我的机器上,Linux x86-64、Python 3.5 上,每个循环的成本为 504 ns;使用 lambda: defaultdict(list) 代替则为559 ns。就像我所说,并不是很重要。我的主要动机(尽管我并不认为在这里使用 lambda 是不好的)是讨厌 lambda (嘿,它还更短!),而不是微小的优化。 :-) - ShadowRanger
你可能需要在回答中提及Python版本来说明所提到的性能增益。 - wim
尽管我很想喜欢这个答案,但性能提升似乎已经消失了。Lambda 表达式似乎始终比较快,大约快 10%(在 Linux 上使用 CPython 3.9)。 - wim
@wim:是的,可以确认。根据代码检查的猜测(有知识背景),变更如下:1)3.8添加了一种逐操作码缓存的方法,用于LOAD_GLOBAL,大幅降低了查找lambdadefaultdict/list成本;2)代码路径defaultdict.copy用于复制时基本上与lambda做相同的事情(比缓存的LOAD_GLOBAL略微便宜,但不是很多),但使用了一种C可变参数函数调用技术(PyObject_CallFunctionObjArgs),它更加通用(也更慢)比字节码解释器循环使用的优化路线。 - ShadowRanger
显示剩余2条评论

2
您可能需要像这样做。
>>> from collections import defaultdict
>>> a=defaultdict()
>>> a["testkey"]=None
>>> a["testkey"]=defaultdict(list)
>>> a["testkey"]["list"]=["a","b","c"]
>>> a
defaultdict(None, {'testkey': defaultdict(<type 'list'>, {'list': ['a', 'b', 'c']})})

3
可以,你可以向第一个defaultdict传递一个自定义函数。 - jonrsharpe
谢谢@jonrsharpe,但我在我的控制台上尝试了一下,它可以工作。这样做是错误的吗? - nohup
2
它可以工作,但是混合使用defaultdict和显式值设置有点尴尬。此外,a["testkey"]=None是完全多余的。 - jonrsharpe
我明白了。那么我猜在defaultdict()内部使用一个lambda函数可能是最好的方法? - nohup
感谢 @nohup 的回答,+1 让它工作起来了,但我更喜欢使用 lambda 函数的函数式方法。 - file2cable

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