python 字典:get 与 setdefault
这两个表达式看起来是等价的。哪一个更好呢?
data = [('a', 1), ('b', 1), ('b', 2)]
d1 = {}
d2 = {}
for key, val in data:
# variant 1)
d1[key] = d1.get(key, []) + [val]
# variant 2)
d2.setdefault(key, []).append(val)
结果是一样的,但哪种写法更好,或者说更符合Python的风格呢?
我个人觉得第二种写法比较难理解,因为我觉得setdefault这个东西很难掌握。如果我理解得没错,它会在字典里查找“key”的值,如果找不到,就会把“[]”放进去,然后返回这个值或者“[]”的引用,并把“val”添加到这个引用里。虽然这样做很流畅,但对我来说一点也不直观。
我觉得第一种写法更容易理解(如果有,就获取“key”的值,如果没有,就获取“[]”,然后把它和由[val]组成的列表合并,最后把结果放到“key”里)。不过,虽然这种写法更容易理解,我担心它的性能会差一些,因为涉及到创建列表。另外一个缺点是“d1”在表达式中出现了两次,这样容易出错。可能用get方法会有更好的实现,但我现在想不起来。
我猜第二种写法虽然对新手来说比较难理解,但速度更快,因此更好。大家怎么看?
8 个回答
对于那些还在努力理解这两个术语的人,我来告诉你们 get()
和 setdefault()
方法之间的基本区别。
场景-1
root = {}
root.setdefault('A', [])
print(root)
场景-2
root = {}
root.get('A', [])
print(root)
在场景-1中,输出结果是 {'A': []}
,而在场景-2中,输出结果是 {}
。
所以,setdefault()
会在字典中设置缺失的键,而 get()
只是给你一个默认值,但并不会修改字典。
现在我们来看看这个有什么用处——假设你在字典中查找一个元素,它的值是一个列表,如果找到了,你想修改这个列表;如果没找到,就创建一个新键并用这个列表。
使用 setdefault()
def fn1(dic, key, lst):
dic.setdefault(key, []).extend(lst)
使用 get()
def fn2(dic, key, lst):
dic[key] = dic.get(key, []) + (lst) #Explicit assigning happening here
现在让我们来看看时间消耗——
dic = {}
%%timeit -n 10000 -r 4
fn1(dic, 'A', [1,2,3])
耗时 288 纳秒
dic = {}
%%timeit -n 10000 -r 4
fn2(dic, 'A', [1,2,3])
耗时 128 秒
所以这两种方法之间的时间差异非常大。
agf的答案其实没有进行公平的比较。在以下代码之后:
print timeit("d[0] = d.get(0, []) + [1]", "d = {1: []}", number = 10000)
d[0]
里有一个包含10,000个项目的列表,而在以下代码之后:
print timeit("d.setdefault(0, []) + [1]", "d = {1: []}", number = 10000)
d[0]
只是一个空列表[]
。也就是说,d.setdefault
这个版本并没有改变存储在d
中的列表。实际上,代码应该是:
print timeit("d.setdefault(0, []).append(1)", "d = {1: []}", number = 10000)
而且这个版本的速度比错误的setdefault
示例要快。
这里的区别在于,当你使用连接操作添加元素时,每次都会复制整个列表(当你有10,000个元素时,这个开销就开始变得明显了)。使用append
方法时,列表的更新是平均O(1)的,也就是说,时间基本上是固定的。
最后,还有两个在原问题中没有考虑的选项:defaultdict
或者简单地检查字典是否已经包含这个键。
所以,假设d3, d4 = defaultdict(list), {}
# variant 1 (0.39)
d1[key] = d1.get(key, []) + [val]
# variant 2 (0.003)
d2.setdefault(key, []).append(val)
# variant 3 (0.0017)
d3[key].append(val)
# variant 4 (0.002)
if key in d4:
d4[key].append(val)
else:
d4[key] = [val]
第一种变体是最慢的,因为它每次都复制列表,第二种变体是第二慢的,第三种变体是最快的,但如果你需要使用早于2.5版本的Python就不适用,第四种变体的速度稍微慢于第三种。
我建议如果可以的话使用第三种变体,第四种变体可以作为偶尔需要的替代方案,特别是在defaultdict
不完全合适的情况下。尽量避免使用你最初的两个变体。
你给出的两个例子做的事情是一样的,但这并不意味着 get
和 setdefault
也一样。
这两者的区别主要在于,d[key]
每次都要手动设置为列表,而 setdefault
只在 d[key]
还没有设置的时候自动将其设置为列表。
为了让这两种方法尽量相似,我运行了
from timeit import timeit
print timeit("c = d.get(0, []); c.extend([1]); d[0] = c", "d = {1: []}", number = 1000000)
print timeit("c = d.get(1, []); c.extend([1]); d[0] = c", "d = {1: []}", number = 1000000)
print timeit("d.setdefault(0, []).extend([1])", "d = {1: []}", number = 1000000)
print timeit("d.setdefault(1, []).extend([1])", "d = {1: []}", number = 1000000)
得到了
0.794723378711
0.811882272256
0.724429205999
0.722129751973
所以在这个目的上,setdefault
比 get
快大约 10%。
get
方法能做的事情比 setdefault
少。你可以用它来避免在键不存在时出现 KeyError
(如果这种情况经常发生),即使你不想设置这个键。
想了解这两种方法的更多信息,可以查看 'setdefault' 字典方法的使用案例 和 dict.get() 方法返回一个指针。
关于 setdefault
的讨论得出的结论是,大多数情况下,你应该使用 defaultdict
。而关于 get
的讨论则认为它比较慢,通常情况下,使用双重查找、使用 defaultdict
或处理错误会更好(这取决于字典的大小和你的使用场景)。