如何在Python中初始化一个空列表字典?

126

我尝试以编程方式创建一个列表的字典,但无法单独访问字典键。每当我创建列表的字典并尝试追加到一个键时,所有键都会被更新。以下是一个非常简单的测试用例:

data = {}
data = data.fromkeys(range(2),[])
data[1].append('hello')
print data

实际结果:{0:['hello'],1:['hello']}

期望结果:{0:[],1:['hello']}

以下是有效的内容。

data = {0:[],1:[]}
data[1].append('hello')
print data

实际结果和期望结果:{0:[],1:['hello']}

为什么fromkeys方法没有按预期工作?


我认为你应该调用 list() 来创建一个新的列表。你可能想使用字典推导式而不是 fromkeys - SomethingSomething
7个回答

146
[]被作为第二个参数传递给dict.fromkeys()时,结果dict中的所有值都将是相同的list对象。 在Python 2.7或更高版本中,请改用dict comprehension:请参考这里
data = {k: [] for k in range(2)}

在Python的早期版本中,没有字典推导,但可以将列表推导式传递给dict构造函数:

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

在Python 2.4-2.6中,也可以将生成器表达式传递给 dict,并且可以省略周围的括号

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

122
尝试使用defaultdict代替:
from collections import defaultdict
data = defaultdict(list)
data[1].append('hello')

这样,键不需要提前初始化为空列表。相反,defaultdict() 对象在每次访问尚不存在的键时调用给定的工厂函数。因此,在此示例中,尝试访问 data[1] 会触发内部的 data[1] = list(),将该键作为其值赋予一个新的空列表。
使用 .fromkeys 的原始代码共享一个(可变)列表。同样,
alist = [1]
data = dict.fromkeys(range(2), alist)
alist.append(2)
print(data)

输出将为{0: [1, 2], 1: [1, 2]}。这在dict.fromkeys()文档中有说明:

所有的值都指向同一个实例,因此通常不会使用可变对象(如空列表)作为value

另一种选择是使用dict.setdefault()方法,该方法在首次检查键是否存在并设置默认值后检索键的值。然后可以在结果上调用.append

data = {}
data.setdefault(1, []).append('hello')

最后,要从一个已知键列表和给定的 "模板" 列表中创建字典(其中每个值应该以相同的元素开头,但是是不同的列表),请使用字典推导和 复制 初始列表:
alist = [1]
data = {key: alist[:] for key in range(2)}

在这里,alist[:] 创建了 alist 的浅拷贝,对每个值都进行了单独的处理。参见 如何克隆列表以避免赋值后意外更改? 了解更多复制列表的技巧。

45
你可以使用字典推导式:

你可以使用字典推导式:

>>> keys = ['a','b','c']
>>> value = [0, 0]
>>> {key: list(value) for key in keys}
    {'a': [0, 0], 'b': [0, 0], 'c': [0, 0]}

value[:]并不是很丑陋(除非你和Alex Martelli有相同的审美观),而且打字更少。在Python的最新版本中,现在有一个list.copy方法。就性能而言,对于小型列表(最多50或60个项目),切片是最快的,但对于较大的列表,list(value)实际上会稍微快一些。value.copy()似乎具有与list(value)类似的性能。这3种技术在大型列表上显著变慢:在我的旧32位机器上,发生在32k左右,根据您的CPU字长和缓存大小可能会有所不同。 - PM 2Ring

43

这个答案的目的是为任何被尝试使用可变默认值在dict中实例化fromkeys()而获得的结果所困扰的人解释此行为。

考虑:

#Python 3.4.3 (default, Nov 17 2016, 01:08:31) 

# start by validating that different variables pointing to an
# empty mutable are indeed different references.
>>> l1 = []
>>> l2 = []
>>> id(l1)
140150323815176
>>> id(l2)
140150324024968

所以对 l1 的任何更改都不会影响 l2,反之亦然。 目前为止,对于任何可变对象,包括 dict 都是如此。

# create a new dict from an iterable of keys
>>> dict1 = dict.fromkeys(['a', 'b', 'c'], [])
>>> dict1
{'c': [], 'b': [], 'a': []}

这可以是一个方便的函数。在这里,我们为每个键分配了一个默认值,这个默认值也恰好是一个空列表。

# the dict has its own id.
>>> id(dict1)
140150327601160

# but look at the ids of the values.
>>> id(dict1['a'])
140150323816328
>>> id(dict1['b'])
140150323816328
>>> id(dict1['c'])
140150323816328

实际上它们都在使用同一个引用! 对其中一个的更改会影响到所有,因为它们实际上是同一个对象!

>>> dict1['a'].append('apples')
>>> dict1
{'c': ['apples'], 'b': ['apples'], 'a': ['apples']}
>>> id(dict1['a'])
>>> 140150323816328
>>> id(dict1['b'])
140150323816328
>>> id(dict1['c'])
140150323816328

对许多人来说,这不是他们预期的!

现在我们尝试通过显式复制被用作默认值的列表来进行。

>>> empty_list = []
>>> id(empty_list)
140150324169864

现在创建一个字典,其内容为 empty_list 的复制品。

>>> dict2 = dict.fromkeys(['a', 'b', 'c'], empty_list[:])
>>> id(dict2)
140150323831432
>>> id(dict2['a'])
140150327184328
>>> id(dict2['b'])
140150327184328
>>> id(dict2['c'])
140150327184328
>>> dict2['a'].append('apples')
>>> dict2
{'c': ['apples'], 'b': ['apples'], 'a': ['apples']}

仍然没有成功! 我听到有人喊道,这是因为我使用了一个空列表!

>>> not_empty_list = [0]
>>> dict3 = dict.fromkeys(['a', 'b', 'c'], not_empty_list[:])
>>> dict3
{'c': [0], 'b': [0], 'a': [0]}
>>> dict3['a'].append('apples')
>>> dict3
{'c': [0, 'apples'], 'b': [0, 'apples'], 'a': [0, 'apples']}
< p > fromkeys() 的默认行为是将值分配为 None

>>> dict4 = dict.fromkeys(['a', 'b', 'c'])
>>> dict4
{'c': None, 'b': None, 'a': None}
>>> id(dict4['a'])
9901984
>>> id(dict4['b'])
9901984
>>> id(dict4['c'])
9901984

实际上,所有的值都是(也是唯一的)None。现在,让我们以无数种方式之一迭代遍历 dict 并更改其值。

>>> for k, _ in dict4.items():
...    dict4[k] = []

>>> dict4
{'c': [], 'b': [], 'a': []}

嗯,看起来和以前一样!

>>> id(dict4['a'])
140150318876488
>>> id(dict4['b'])
140150324122824
>>> id(dict4['c'])
140150294277576
>>> dict4['a'].append('apples')
>>> dict4
>>> {'c': [], 'b': [], 'a': ['apples']}

但它们确实是不同的[],在这种情况下这是预期结果。


8
所以我们需要迭代吗? - lucid_dreamer
2
我认为整个重点是不要迭代...那是捷径,否则我为什么需要这个函数呢? - Ricky Levi
当然你必须迭代。dict.fromkeys首先进行迭代。无论如何,这似乎是为了解释已经非常清楚的前两个答案而提供了大量细节。 - Karl Knechtel

10

你可以使用这个:

l = ['a', 'b', 'c']
d = dict((k, [0, 0]) for k in l)

9
您正在使用单个列表的引用填充字典,因此当您更新它时,更新将在所有引用中反映出来。请尝试使用字典推导式来代替。请参见在Python中使用列表推导式创建字典
d = {k : v for k in blah blah blah}

很好的建议,关于初始化字典值...谢谢 Cobie!我扩展了你的示例来重置现有字典 d 中的值。我按照以下方式执行: d = { k:0 for k in d } - John
这个答案中的 v 是什么? - Dr_Zaszuś

-4
你可以使用这个:
data[:1] = ['hello']

2
向提问者解释为什么这个方法有效可能会对他有所帮助。原始问题是询问为什么它不能按预期工作。 - william.taylor.09
@william.taylor.09 这为什么能够工作,显然吧? - Conner Dassen
OP问:“为什么fromkeys方法没有按预期工作?” - william.taylor.09
这个不行。data是一个字典,因此不能被切片,所以data[:1]已经无效了。 - Karl Knechtel

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