Python 2.5中增加的collections.defaultdict极大地减少了对dict的setdefault方法的需求。这个问题是为了我们的集体教育:
在今天的Python 2.6/2.7中,setdefault在哪些方面仍然有用? setdefault的哪些流行用例被collections.defaultdict所取代?
Python 2.5中增加的collections.defaultdict极大地减少了对dict的setdefault方法的需求。这个问题是为了我们的集体教育:
在今天的Python 2.6/2.7中,setdefault在哪些方面仍然有用? setdefault的哪些流行用例被collections.defaultdict所取代?
当前回答
我刚刚偶然发现了一个非常重要的用例: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成员。
其他回答
我通常使用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)
它非常适合在包装器中围绕接受关键字参数的函数调整参数。
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())
注意这个词典。get(key, nextID())是不够的,因为我需要能够在以后引用这些值。
intGen是我构建的一个小类,它自动递增int并返回它的值:
class intGen:
def __init__(self):
self.i = 0
def __call__(self):
self.i += 1
return self.i
如果有人有办法做到这一点与defaultdict,我很乐意看到它。
我经常使用setdefault,在字典中设置默认值(!!)通常是os。环境字典:
# Set the venv dir if it isn't already overridden:
os.environ.setdefault('VENV_DIR', '/my/default/path')
简单点说,是这样的:
# Set the venv dir if it isn't already overridden:
if 'VENV_DIR' not in os.environ:
os.environ['VENV_DIR'] = '/my/default/path')
值得注意的是,你也可以使用结果变量:
venv_dir = os.environ.setdefault('VENV_DIR', '/my/default/path')
但与违约之前相比,这已经不那么必要了。
[编辑]大错特错!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))
在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