'setdefault'字典方法的使用案例

241

在Python 2.5中增加了collections.defaultdict,极大地减少了使用dictsetdefault方法的需要。本问题是为了我们共同学习而提出的:

  1. setdefault在今天的Python 2.6 / 2.7中仍有哪些用途?
  2. collections.defaultdict取代了哪些流行的setdefault用例?
18个回答

262
你可以说 defaultdict 对于在填充字典之前设置默认值很有用,而setdefault则对于在填充之时或之后设置默认值很有用。
可能最常见的使用案例是:分组条目(在未排序数据中,否则使用itertools.groupby)。
# really verbose
new = {}
for (key, value) in data:
    if key in new:
        new[key].append( value )
    else:
        new[key] = [value]


# easy with setdefault
new = {}
for (key, value) in data:
    group = new.setdefault(key, []) # key might exist already
    group.append( value )


# even simpler with defaultdict 
from collections import defaultdict
new = defaultdict(list)
for (key, value) in data:
    new[key].append( value ) # all keys have a default already
有时候在创建字典后,你希望确保特定的键存在。但是defaultdict在这种情况下不起作用,因为它仅在显式访问时创建键。假设你正在使用类似HTTP的协议并有许多头信息 -- 一些是可选的,但你想要默认值。
headers = parse_headers( msg ) # parse the message, get a dict
# now add all the optional headers
for headername, defaultvalue in optional_headers:
    headers.setdefault( headername, defaultvalue )

1
事实上,我认为这是使用defaultdict替换的主要用例。你能举个例子来说明第一段中的意思吗? - Eli Bendersky
2
Muhammad Alkarouri:首先要做的是复制字典,然后覆盖其中一些项。我也经常这样做,我想这实际上是大多数人更喜欢的习语,而不是“setdefault”。另一方面,如果不是所有的“defaultvalues”都相等(即有些是“0”,有些是“[]”),那么“defaultdict”就行不通了。 - Jochen Ritzel
2
@YHC4k,是的。这就是为什么我使用了headers = dict(optional_headers)。对于默认值不全相等的情况,这样做是可行的。最终结果与先获取HTTP头然后设置未获取的默认值相同。如果您已经有了optional_headers,那么这种方法非常实用。尝试我的两步代码并将其与您的进行比较,您会明白我的意思。 - Muhammad Alkarouri
33
或者只需执行new.setdefault(key, []).append(value)。该语句的作用是,如果字典new中已存在键key,则将值value添加到该键对应的列表中;如果不存在,则创建一个新的键值对,键为key,值为包含value的列表。 - fmalina
3
最佳答案竟然归结为defaultdictsetdefault更好(那么现在还有哪些用例呢?)我觉得很奇怪。此外,在我看来,ChainMap可以更好地处理http的示例。 - YvesgereY
显示剩余4条评论

37

我通常使用setdefault来处理关键字参数字典,例如在这个函数中:

def notify(self, level, *pargs, **kwargs):
    kwargs.setdefault("persist", level >= DANGER)
    self.__defcon.set(level, **kwargs)
    try:
        kwargs.setdefault("name", self.client.player_entity().name)
    except pytibia.PlayerEntityNotFound:
        pass
    return _notify(level, *pargs, **kwargs)

它非常适合调整围绕接受关键字参数的函数的包装器中的参数。


19

defaultdict非常适合默认值是静态的情况,比如新列表,但如果默认值是动态的,则不太适用。

例如,我需要一个字典将字符串映射到唯一的整数。 defaultdict(int)将始终使用0作为默认值。同样,defaultdict(intGen())始终生成1。

相反,我使用了一个普通的字典:

nextID = intGen()
myDict = {}
for lots of complicated stuff:
    #stuff that generates unpredictable, possibly already seen str
    strID = myDict.setdefault(myStr, nextID())
请注意,dict.get(key, nextID()) 不足以满足我的要求,因为我需要稍后能够引用这些值。 intGen 是我创建的一个小类,它自动递增一个整数并返回其值。
class intGen:
    def __init__(self):
        self.i = 0

    def __call__(self):
        self.i += 1
    return self.i

如果有人知道使用 defaultdict 来完成这个任务的方法,我很愿意看到。


如果想使用(子类)defaultdict 的方式来实现,请参阅此问题:https://dev59.com/lHE85IYBdhLWcg3wKwOE - weronika
10
你可以用itertools.count().next替换intGen - Antimony
11
每次调用myDict.setdefault()时,nextID()的值都将递增,即使返回的值未被用作strID。这种情况似乎有些浪费,并且说明了我对setdefault()的一些不喜欢之处——即它总是评估其default参数,无论它是否实际被使用。 - martineau
1
你可以使用 defaultdict 来实现:myDict = defaultdict(lambda: nextID())。然后,在循环中使用 strID = myDict[myStr] - musiphil
如果有人知道如何使用defaultdict来完成这个任务,我很想看看。 --> http://ideone.com/psOZ5M - moooeeeep
6
为了获得您所描述的defaultdict行为,为什么不直接使用myDict = defaultdict(nextID) - forty_two

19

像大多数回答所述,setdefaultdefaultdict可以让您在键不存在时设置默认值。但是,我想指出关于setdefault用例的一个小注意点。当Python解释器执行setdefault时,即使键存在于字典中,它也会始终评估函数的第二个参数。例如:

In: d = {1:5, 2:6}

In: d
Out: {1: 5, 2: 6}

In: d.setdefault(2, 0)
Out: 6

In: d.setdefault(2, print('test'))
test
Out: 6

正如您所看到的,即使字典中已经存在2,print 也会被执行。这在您计划使用 setdefault 进行优化(例如用于记忆化)时变得非常重要。如果将递归函数调用作为 setdefault 的第二个参数添加,它不会产生任何性能提升,因为 Python 将始终以递归方式调用该函数。

既然提到了记忆化,如果您考虑使用记忆化增强函数,则更好的选择是使用 functools.lru_cache 装饰器。lru_cache 更好地处理递归函数的缓存需求。


12

就像穆罕默德所说,有时只需要设置默认值的情况。一个很好的例子是一个数据结构首先被填充,然后进行查询。

考虑一棵 trie 树。在添加单词时,如果需要但不存在子节点,则必须创建以扩展 trie 树。在查询单词是否存在时,缺少的子节点表示该单词不存在,不应该创建它。

defaultdict 无法实现这一点。相反,必须使用带有 get 和 setdefault 方法的常规 dict。


11

当我想在 OrderedDict 中使用默认值时,我会使用 setdefault()。Python并没有一个标准的集合同时做到这两点, 但是有方法可以实现这样一个集合。


5
从理论上讲,setdefault 在有时需要设置默认值和有时不需要设置默认值的情况下仍然很方便。但在实际生活中,我还没有遇到这样的用例。
但是,在标准库(Python 2.6,_threadinglocal.py)中出现了一个有趣的用例:
>>> mydata = local()
>>> mydata.__dict__
{'number': 42}
>>> mydata.__dict__.setdefault('widgets', [])
[]
>>> mydata.widgets
[]

我认为使用__dict__.setdefault是一个相当有用的情况。
编辑:事实上,这是标准库中唯一的例子,而且它在注释中。因此,可能没有足够的例子来证明存在setdefault的必要性。不过,以下是解释:
对象将其属性存储在__dict__属性中。恰好在创建对象后,__dict__属性是可写的。它也是一个字典,而不是一个defaultdict。一般情况下,对象作为defaultdict具有__dict__是不明智的,因为这将使每个对象都具有所有合法的标识符作为属性。因此,除非被认为没有用处,我无法预见任何Python对象的更改会摆脱__dict__.setdefault,除非完全删除它。

1
你能详细说明一下,_dict.setdefault 有什么特别的用处吗? - Eli Bendersky
1
@Eli:我认为重点在于__dict__实现上是一个dict,而不是一个defaultdict - Katriel
1
好的。我不介意 setdefault 在 Python 中保留,但现在它几乎没什么用,这很奇怪。 - Eli Bendersky
@Eli:我同意。如果它本来就不存在,那么今天引入它的理由不足够充分。但是既然已经存在,考虑到所有使用它的代码,要主张将其删除是困难的。 - Muhammad Alkarouri
1
将其归类为防御式编程。setdefault 明确表示您正在通过可能存在或不存在的键分配给字典,并且如果它不存在,则希望创建具有默认值的字典:例如 d.setdefault(key,[]).append(value)。在程序的其他地方,您执行 alist=d[k],其中 k 是计算出来的,如果 k 不在 d 中,则希望抛出异常(这可能需要使用 defaultdict 的 assert k in d 或甚至 if not ( k in d): raise KeyError)。 - nigel222

4
我重新编写了被接受的答案,并将其简化为新手易懂的形式。
#break it down and understand it intuitively.
new = {}
for (key, value) in data:
    if key not in new:
        new[key] = [] # this is core of setdefault equals to new.setdefault(key, [])
        new[key].append(value)
    else:
        new[key].append(value)


# easy with setdefault
new = {}
for (key, value) in data:
    group = new.setdefault(key, []) # it is new[key] = []
    group.append(value)



# even simpler with defaultdict
new = defaultdict(list)
for (key, value) in data:
    new[key].append(value) # all keys have a default value of empty list []

此外,我将这些方法分类为参考:
dict_methods_11 = {
            'views':['keys', 'values', 'items'],
            'add':['update','setdefault'],
            'remove':['pop', 'popitem','clear'],
            'retrieve':['get',],
            'copy':['copy','fromkeys'],}

dict_methods_11 = { 'views':['keys', 'values', 'items'], 'add':['update','setdefault'], 'remove':['pop', 'popitem','clear'], 'retrieve':['get'], 'copy':['copy','fromkeys'] }我已经编辑了两个额外的逗号。 - Ali Hassan

4
defaultdict 相较于 dictdict.setdefault) 的一个缺陷是,每次给定一个不存在的键时(例如使用 ==print),defaultdict 对象都会创建一个新项。此外,defaultdict 类通常比 dict 类更少见,因此在序列化方面更加困难。

另外,我认为不应该通过函数或方法来改变对象,这样做是不合适的。


它不必每次都创建一个新对象。你可以很容易地使用defaultdict(lambda l=[]: l)来代替。 - Artyer
13
永远不要像@Artyer建议的那样做——可变默认值会咬你。 - Brandon Humpert
避免在打印和类似情况下创建对象的方法是使用get而不是[...]。调用get不会导致默认对象的创建。 - undefined

3
以下是一些setdefault的示例,以展示其有用性:
"""
d = {}
# To add a key->value pair, do the following:
d.setdefault(key, []).append(value)

# To retrieve a list of the values for a key
list_of_values = d[key]

# To remove a key->value pair is still easy, if
# you don't mind leaving empty lists behind when
# the last value for a given key is removed:
d[key].remove(value)

# Despite the empty lists, it's still possible to 
# test for the existance of values easily:
if d.has_key(key) and d[key]:
    pass # d has some values for key

# Note: Each value can exist multiple times!
"""
e = {}
print e
e.setdefault('Cars', []).append('Toyota')
print e
e.setdefault('Motorcycles', []).append('Yamaha')
print e
e.setdefault('Airplanes', []).append('Boeing')
print e
e.setdefault('Cars', []).append('Honda')
print e
e.setdefault('Cars', []).append('BMW')
print e
e.setdefault('Cars', []).append('Toyota')
print e

# NOTE: now e['Cars'] == ['Toyota', 'Honda', 'BMW', 'Toyota']
e['Cars'].remove('Toyota')
print e
# NOTE: it's still true that ('Toyota' in e['Cars'])

很高兴看到一个使用setdefault返回值的例子。这是一种更简单的使用方式。 - Martlark

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