Python 2.5中增加的collections.defaultdict极大地减少了对dict的setdefault方法的需求。这个问题是为了我们的集体教育:

在今天的Python 2.6/2.7中,setdefault在哪些方面仍然有用? setdefault的哪些流行用例被collections.defaultdict所取代?


当前回答

[编辑]大错特错!setdefault总是会触发long_computation,因为Python很急切。

扩展塔特尔的答案。对我来说,最好的用例是缓存机制。而不是:

if x not in memo:
   memo[x]=long_computation(x)
return memo[x]

这需要3行和2到3次查找,我很乐意这样写:

return memo.setdefault(x, long_computation(x))

其他回答

你可以说defaultdict用于在填充dict之前设置默认值,而setdefault用于在填充dict期间或之后设置默认值。

可能是最常见的用例:对项进行分组(在未排序的数据中,否则使用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-ish——有些是可选的,但你想要它们的默认值:

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 )

从理论上讲,如果您有时想设置默认值,有时不想设置默认值,那么setdefault仍然很方便。在现实生活中,我还没有遇到过这样的用例。

然而,一个有趣的用例来自标准库(Python 2.6, _threadinglocal.py):

>>> mydata = local()
>>> mydata.__dict__
{'number': 42}
>>> mydata.__dict__.setdefault('widgets', [])
[]
>>> mydata.widgets
[]

我会说使用__dict__。Setdefault是一个非常有用的例子。

编辑:碰巧,这是标准库中唯一的示例,并且它在注释中。因此,它可能不足以证明setdefault的存在。不过,这里有一个解释:

Objects store their attributes in the __dict__ attribute. As it happens, the __dict__ attribute is writeable at any time after the object creation. It is also a dictionary not a defaultdict. It is not sensible for objects in the general case to have __dict__ as a defaultdict because that would make each object having all legal identifiers as attributes. So I can't foresee any change to Python objects getting rid of __dict__.setdefault, apart from deleting it altogether if it was deemed not useful.

我刚刚偶然发现了一个非常重要的用例:dict.setdefault()对于只想要单个规范对象(而不是恰好相等的多个对象)的多线程代码非常有用。

例如,Python 3.6.0中的(Int)标志Enum有一个错误:如果多个线程在竞争一个复合(Int)标志成员,最终可能会有多个:

from enum import IntFlag, auto
import threading

class TestFlag(IntFlag):
    one = auto()
    two = auto()
    three = auto()
    four = auto()
    five = auto()
    six = auto()
    seven = auto()
    eight = auto()

    def __eq__(self, other):
        return self is other

    def __hash__(self):
        return hash(self.value)

seen = set()

class cycle_enum(threading.Thread):
    def run(self):
        for i in range(256):
            seen.add(TestFlag(i))

threads = []
for i in range(8):
    threads.append(cycle_enum())

for t in threads:
    t.start()

for t in threads:
    t.join()

len(seen)
# 272  (should be 256)

解决方案是使用setdefault()作为保存计算的组合成员的最后一步——如果已经保存了另一个成员,则使用它而不是新成员,从而保证唯一的Enum成员。

正如大多数答案,state setdefault或defaultdict将允许您在键不存在时设置默认值。然而,我想指出一个关于setdefault用例的小警告。当Python解释器执行时,setdefaultit将始终计算函数的第二个参数,即使该键存在于字典中。例如:

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可以更好地处理递归函数的缓存需求。

在CPython中setdefault的另一个用例是,它在所有情况下都是原子的,而defaultdict将不是原子的,如果你使用从lambda创建的默认值。

cache = {}

def get_user_roles(user_id):
    if user_id in cache:
        return cache[user_id]['roles']

    cache.setdefault(user_id, {'lock': threading.Lock()})

    with cache[user_id]['lock']:
        roles = query_roles_from_database(user_id)
        cache[user_id]['roles'] = roles

如果两个线程执行缓存。同时设置default,它们中只有一个能够创建默认值。

如果你使用defaultdict:

cache = defaultdict(lambda: {'lock': threading.Lock()}

这将导致竞态条件。在我上面的例子中,第一个线程可以创建一个默认锁,第二个线程可以创建另一个默认锁,然后每个线程可以锁定自己的默认锁,而不是每个线程试图锁定单个锁的预期结果。


从概念上讲,setdefault的基本行为是这样的(如果你使用空列表、空dict、int或其他不是用户python代码(如lambda)的默认值,defaultdict也会这样表现):

gil = threading.Lock()

def setdefault(dict, key, value_func):
    with gil:
        if key not in dict:
            return
       
        value = value_func()

        dict[key] = value

从概念上讲,defaultdict的基本行为是这样的(只有在使用lambda这样的python代码时-如果使用空列表则不是这样):

gil = threading.Lock()

def __setitem__(dict, key, value_func):
    with gil:
        if key not in dict:
            return

    value = value_func()

    with gil:
        dict[key] = value