我在Python 2.6和3.2中遇到了令我惊讶的行为:
>>> xs = dict.fromkeys(range(2), [])
>>> xs
{0: [], 1: []}
>>> xs[0].append(1)
>>> xs
{0: [1], 1: [1]}
然而,3.2中的dict
理解显示出更礼貌的举止:
>>> xs = {i:[] for i in range(2)}
>>> xs
{0: [], 1: []}
>>> xs[0].append(1)
>>> xs
{0: [1], 1: []}
>>>
为什么fromkeys
表现得那样?
答案 0 :(得分:19)
您的Python 2.6示例等同于以下内容,这可能有助于澄清:
>>> a = []
>>> xs = dict.fromkeys(range(2), a)
结果字典中的每个条目都将引用同一个对象。正如您所见,通过每个字典条目可以看到变异该对象的效果,因为它是一个对象。
>>> xs[0] is a and xs[1] is a
True
使用dict理解,或者如果你坚持使用Python 2.6或更早版本且你没有字典理解,你可以通过dict()
使用生成器表达式获得dict理解行为:
xs = dict((i, []) for i in range(2))
答案 1 :(得分:5)
在第一个版本中,您使用相同的空列表对象作为两个键的值,因此如果您更改了一个,则也会更改另一个。
看看这个:
>>> empty = []
>>> d = dict.fromkeys(range(2), empty)
>>> d
{0: [], 1: []}
>>> empty.append(1) # same as d[0].append(1) because d[0] references empty!
>>> d
{0: [1], 1: [1]}
在第二个版本中,在dict理解的每次迭代中都会创建一个 new 空列表对象,因此它们彼此独立。
至于“为什么”fromkeys()
就是这样的 - 好吧,如果不能那样工作就会令人惊讶。 fromkeys(iterable, value)
使用来自 iterable 的键构造一个新的dict,它们都具有值value
。如果该值是一个可变对象,并且您更改了该对象,那么您还可以合理地预期会发生什么?
答案 2 :(得分:1)
要回答实际提出的问题:fromkeys
之所以这样,是因为没有其他合理的选择。让fromkeys
决定您的论点是否可变并每次都创建新副本是不合理的(甚至是不可能的)。在某些情况下,这是没有意义的,而在其他情况下,这是不可能的。
因此,您传入的第二个参数只是一个引用,因此被复制。在Python中分配[]
的意思是“对新列表的单个引用”,而不是“每次访问此变量时都创建新列表”。替代方法是传入一个生成新实例的函数,该函数决定了理解为您提供的功能。
以下是用于创建可变容器的多个实际副本的一些选项:
正如您在问题中提到的,dict理解使您可以为每个元素执行任意语句:
d = {k: [] for k in range(2)}
这里重要的是,这等效于将分配k = []
放入for
循环中。每次迭代都会创建一个新列表并将其分配给一个值。
使用@Andrew Clark建议的dict
构造函数的形式:
d = dict((k, []) for k in range(2))
这将创建一个生成器,该生成器在执行时再次为每个键值对分配一个新列表。
使用collections.defaultdict
代替常规的dict
:
d = collections.defaultdict(list)
此选项与其他选项略有不同。每次您访问尚不存在的键时,defaultdict
都会调用list
,而不是预先创建新的列表引用。您可以在那里随意添加键,有时这会非常方便:
for k in range(2):
d[k].append(42)
由于您已经设置了新元素的工厂,因此实际上其行为将与您在原问题中fromkeys
预期的完全一样。
访问潜在的新密钥时,请使用dict.setdefault
。这样做的作用类似于defaultdict
,但是在一定程度上它具有受控制的优势,因为只有您要创建新键的访问才能真正创建它们:
d = {}
for k in range(2):
d.setdefault(k, []).append(42)
缺点是每次调用该函数时都会创建一个新的空列表对象,即使它从未分配给值。这不是一个大问题,但是如果您经常调用它和/或您的容器不如list
那么简单,它可能会加起来。