python 字典:get 与 setdefault

81 投票
8 回答
66779 浏览
提问于 2025-04-17 02:19

这两个表达式看起来是等价的。哪一个更好呢?

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 个回答

24

对于那些还在努力理解这两个术语的人,我来告诉你们 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 秒

所以这两种方法之间的时间差异非常大。

27

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不完全合适的情况下。尽量避免使用你最初的两个变体。

42

你给出的两个例子做的事情是一样的,但这并不意味着 getsetdefault 也一样。

这两者的区别主要在于,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

所以在这个目的上,setdefaultget 快大约 10%。

get 方法能做的事情比 setdefault 少。你可以用它来避免在键不存在时出现 KeyError(如果这种情况经常发生),即使你不想设置这个键。

想了解这两种方法的更多信息,可以查看 'setdefault' 字典方法的使用案例dict.get() 方法返回一个指针

关于 setdefault 的讨论得出的结论是,大多数情况下,你应该使用 defaultdict。而关于 get 的讨论则认为它比较慢,通常情况下,使用双重查找、使用 defaultdict 或处理错误会更好(这取决于字典的大小和你的使用场景)。

撰写回答