Python字典:get与setdefault的区别

80

下面两个表达式对我来说似乎是等价的。哪一个更好?

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规范呢?
个人认为,版本2更难理解,因为对我而言setdefault非常棘手。如果我理解正确,它在字典中查找“key”的值,如果不可用,则将“[]”输入到字典中,返回对该值或“[]”的引用,并将“val”附加到该引用。虽然这显然很顺畅,但对我来说却一点也不直观。
在我看来,版本1更容易理解(如果可用,则获取“key”的值,如果不可用,则获取“[]”,然后与由[val]组成的列表连接,并将结果放置在“key”中)。但虽然更易于理解,我担心这个版本的性能较差,因为需要创建所有这些列表。另一个缺点是表达式中出现了两次“d1”,这是相当容易出错的。可能有一种更好的使用get的实现方式,但目前我还没有想到。
我的猜测是,尽管对于经验不足的人来说更难掌握,但版本2更快,因此更可取。您有什么看法?

这两个表达式在我看来似乎是等价的?怎么可能呢?它们执行不同的操作。既然它们执行不同的操作,你怎么能问哪一个更好呢? - S.Lott
1
他们会吗?也许我提供的示例过于简单,但对我来说它们似乎做的是相同的事情。至少当我执行上面的代码时,d1和d2得到了相同的结果。你有一个案例手头上这两个表达式做不同的事情吗? - Cerno
7
也许S.Lott的意思是第二种方法修改了一个已有的列表,而第一种方法通过连接创建了一个新的列表。在这种情况下,最终结果是相同的,但是这两个版本的实现方式不同。在不同的情况下,这两种方法可能会产生非常不同的效果。具体来说,如果列表的其他位置有另一个引用,版本2可能会产生副作用。 - senderle
2
@Cerno:这两个示例语句的行为是完全相同的。然而,这些语句中的表达式却是完全不同的,因为其中一个表达式具有副作用,而另一个表达式则没有。您是在询问问题中提到的表达式,还是询问整个语句(作为一个整体)--如评论中所述?赋值是一个语句setdefault()是一个表达式。我对您的问题和评论感到困惑。请明确指出您所讨论的内容。 - S.Lott
@senderle:但这意味着问题仍然存在。您指出版本2可能会产生副作用,而版本1中不存在。假设我不想使用可能发生的任何副作用,那么应该可以确定哪个版本在稳定性方面更可取。从您的评论中,我了解到它是版本1。但效率呢? - Cerno
8个回答

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或处理错误(具体取决于字典的大小和您的用例)。


5
请注意,您的setdefault版本实际上从未将构建的列表存储回字典中,因此您正在比较通过加法构建10,000个元素列表的代码和构建10,000个1元素列表并将其丢弃的代码。请参阅我的答案以获取更多信息。 - Duncan

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)

实际上,使用正确的 append 示例比错误的 setdefault 示例更快。

这里的区别在于,当你使用连接符进行追加时,整个列表每次都会被复制(一旦你有了10000个元素,这就开始变得可测量了)。使用 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]

在这四种方式中,第一种是最慢的,因为它每次都要复制列表,第二种是第二慢的,第三种是最快的,但如果你需要 Python 版本早于 2.5,则无法使用,第四种仅比第三种略慢。

我建议尽可能使用第三种,如果 defaultdict 不太适用的话,可以考虑使用第四种。避免使用你原来的两个变体。


5
你说得对,我的例子是错误的,但幸运的是它们并没有指向错误的方向。我稍微修改了它们。 - agf

23

对于那些仍然难以理解这两个术语的人,让我告诉你get()和setdefault()方法之间的基本区别:

情况-1

root = {}
root.setdefault('A', [])
print(root)

场景-2

root = {}
root.get('A', [])
print(root)
在第一种情况下,输出将为{'A': []},而在第二种情况下,输出为{}
因此,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秒

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


11

你可能想要查看 collections 模块中的 defaultdict。下面的代码等价于你提供的示例。

from collections import defaultdict

data = [('a', 1), ('b', 1), ('b', 2)]

d = defaultdict(list)

for k, v in data:
    d[k].append(v)

这里还有更多内容。


2
我知道这一点,但是我想知道哪个原始版本更好。或者你会说使用defaultdict总是最好的选择吗?我尽量避免额外的导入,因此通常使用传统的dict。但可能这是一个愚蠢的做法? - Cerno
1
getsetdefault都有其使用场景,而defaultdict无法涵盖所有情况。 - agf
7
"我尽可能地避免引入额外的导入。"请停止这样做。这是一种非常糟糕的策略,会导致程序不必要的复杂性。 - S.Lott
@Cerno,“defaultdict”自2.5版本以来就存在了,但现在很少有人停留在2.4版本。总的来说,我认为这是最好的答案——“defaultdict”使代码易读,并且它是用c实现的,因此一定很快。 - senderle
@Cerno 是的,那是打错了/脑抽了。我想说的是 defaultdict 而不是 dict.get。如果它适用于你的使用情况,它会是最快的。 - agf
显示剩余2条评论

10

1. 在这里用一个好的例子解释:
http://code.activestate.com/recipes/66516-add-an-entry-to-a-dictionary-unless-the-entry-is-a/

dict.setdefault的典型用法
somedict.setdefault(somekey,[]).append(somevalue)

dict.get的典型用法
theIndex[word] = 1 + theIndex.get(word,0)


2. 更多解释:http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html

dict.setdefault()相当于getset & get,或者说是如果必要则设置,然后获取。特别适用于字典键计算昂贵或打字较长的情况下,性能尤为高效。

dict.setdefault()的唯一问题在于默认值始终进行评估,无论是否需要。这只有在默认值很难计算时才会有影响。在这种情况下,请使用defaultdict。


3. 最后,官方文档显示区别:http://docs.python.org/2/library/stdtypes.html

get(key[, default])
如果key在字典中,则返回其值,否则返回默认值。如果未提供默认值,则默认为None,因此该方法永远不会引发KeyError异常。

setdefault(key[, default])
如果key在字典中,则返回其值。如果没有,则插入key并返回默认值。默认值默认为None。


2
dict.get 的逻辑是:
if key in a_dict:
    value = a_dict[key] 
else: 
    value = default_value

举个例子:
In [72]: a_dict = {'mapping':['dict', 'OrderedDict'], 'array':['list', 'tuple']}
In [73]: a_dict.get('string', ['str', 'bytes'])
Out[73]: ['str', 'bytes']
In [74]: a_dict.get('array', ['str', 'byets'])
Out[74]: ['list', 'tuple']
setdefault 的机制是:
    levels = ['master', 'manager', 'salesman', 'accountant', 'assistant']
    #group them by the leading letter
    group_by_leading_letter = {}
    # the logic expressed by obvious if condition
    for level in levels:
        leading_letter = level[0]
        if leading_letter not in group_by_leading_letter:
            group_by_leading_letter[leading_letter] = [level]
        else:
            group_by_leading_letter[leading_letter].append(word)
    In [80]: group_by_leading_letter
    Out[80]: {'a': ['accountant', 'assistant'], 'm': ['master', 'manager'], 's': ['salesman']}

setdefault字典方法就是为了达到这个目的。前面的for循环可以重写为:

In [87]: for level in levels:
    ...:     leading = level[0]
    ...:     group_by_leading_letter.setdefault(leading,[]).append(level)
Out[80]: {'a': ['accountant', 'assistant'], 'm': ['master', 'manager'], 's': ['salesman']}

这很简单,意味着非空列表追加一个元素或空列表追加一个元素。

defaultdict 使这甚至更容易。要创建一个 defaultdict,只需为字典中每个槽位传递一种类型或函数来生成默认值:

from collections import defualtdict
group_by_leading_letter = defaultdict(list)
for level in levels:
    group_by_leading_letter[level[0]].append(level)

由于每种方法的呈现方式截然不同,因此这个答案是误导性的。在您提供的示例中,“get”和“setdefault”之间在功能上没有区别,您只是否定了if语句。 - Matthew Muller

2
这个问题没有一个严格的答案。它们都能达到同样的目的。它们都可以用来处理键上的缺失值。我发现唯一的区别是,在使用setdefault()时,如果调用的键(如果以前不在字典中)会自动插入,而在使用get()时则不会。以下是一个示例:setdefault()
>>> myDict = {'A': 'GOD', 'B':'Is', 'C':'GOOD'} #(1)
>>> myDict.setdefault('C')  #(2)
'GOOD'
>>> myDict.setdefault('C','GREAT')  #(3)
'GOOD'
>>> myDict.setdefault('D','AWESOME') #(4)
'AWESOME'
>>> myDict #(5)
{'A': 'GOD', 'B': 'Is', 'C': 'GOOD', 'D': 'AWSOME'} 
>>> myDict.setdefault('E')
>>>

获取()
>>> myDict = {'a': 1, 'b': 2, 'c': 3}   #(1)
>>> myDict.get('a',0)   #(2)
1
>>> myDict.get('d',0)   #(3)
0
>>> myDict #(4)
{'a': 1, 'b': 2, 'c': 3}

这是我的结论:当涉及到默认值填充时,没有特定的答案可以说哪一个最好。唯一的区别是setdefault()会自动将任何带有默认值的新键添加到字典中,而get()则不会。如需更多信息,请点击 这里

1
In [1]: person_dict = {}

In [2]: person_dict['liqi'] = 'LiQi'

In [3]: person_dict.setdefault('liqi', 'Liqi')
Out[3]: 'LiQi'

In [4]: person_dict.setdefault('Kim', 'kim')
Out[4]: 'kim'

In [5]: person_dict
Out[5]: {'Kim': 'kim', 'liqi': 'LiQi'}

In [8]: person_dict.get('Dim', '')
Out[8]: ''

In [5]: person_dict
Out[5]: {'Kim': 'kim', 'liqi': 'LiQi'}

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