使用fromkeys和可变对象创建字典。惊喜

时间:2011-11-17 21:39:59

标签: python

我在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表现得那样?

3 个答案:

答案 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中分配[]的意思是“对新列表的单个引用”,而不是“每次访问此变量时都创建新列表”。替代方法是传入一个生成新实例的函数,该函数决定了理解为您提供的功能。

以下是用于创建可变容器的多个实际副本的一些选项:

  1. 正如您在问题中提到的,dict理解使您可以为每个元素执行任意语句:

    d = {k: [] for k in range(2)}
    

    这里重要的是,这等效于将分配k = []放入for循环中。每次迭代都会创建一个新列表并将其分配给一个值。

  2. 使用@Andrew Clark建议的dict构造函数的形式:

    d = dict((k, []) for k in range(2))
    

    这将创建一个生成器,该生成器在执行时再次为每个键值对分配一个新列表。

  3. 使用collections.defaultdict代替常规的dict

    d = collections.defaultdict(list)
    

    此选项与其他选项略有不同。每次您访问尚不存在的键时,defaultdict都会调用list,而不是预先创建新的列表引用。您可以在那里随意添加键,有时这会非常方便:

    for k in range(2):
        d[k].append(42)
    

    由于您已经设置了新元素的工厂,因此实际上其行为将与您在原问题中fromkeys预期的完全一样。

  4. 访问潜在的新密钥时,请使用dict.setdefault。这样做的作用类似于defaultdict,但是在一定程度上它具有受控制的优势,因为只有您要创建新键的访问才能真正创建它们:

    d = {}
    for k in range(2):
        d.setdefault(k, []).append(42)
    

    缺点是每次调用该函数时都会创建一个新的空列表对象,即使它从未分配给值。这不是一个大问题,但是如果您经常调用它和/或您的容器不如list那么简单,它可能会加起来。