dict.setdefault
的一个小烦恼是它总是评估第二个参数(当然,当给出时),即使第一个参数已经是字典中的键。
例如:
import random
def noisy_default():
ret = random.randint(0, 10000000)
print 'noisy_default: returning %d' % ret
return ret
d = dict()
print d.setdefault(1, noisy_default())
print d.setdefault(1, noisy_default())
这会产生如下所示的输出:
noisy_default: returning 4063267
4063267
noisy_default: returning 628989
4063267
正如最后一行所确认的,第二次执行
noisy_default
是不必要的,因为此时键 1
已经存在于 d
中(值为 4063267
)。
是否可以实现
dict
的子类,其 setdefault
方法延迟计算其第二个参数?
编辑:
下面是受 BrenBarn 的评论和 Pavel Anossov 的答案启发的实现。与此同时,我也实现了 get 的惰性版本,因为基本思想本质上是相同的。
class LazyDict(dict):
def get(self, key, thunk=None):
return (self[key] if key in self else
thunk() if callable(thunk) else
thunk)
def setdefault(self, key, thunk=None):
return (self[key] if key in self else
dict.setdefault(self, key,
thunk() if callable(thunk) else
thunk))
现在,片段
d = LazyDict()
print d.setdefault(1, noisy_default)
print d.setdefault(1, noisy_default)
产生这样的输出:
noisy_default: returning 5025427
5025427
5025427
请注意,上面
d.setdefault
的第二个参数现在是可调用的,而不是函数调用。
当
LazyDict.get
或 LazyDict.setdefault
的第二个参数不是可调用时,它们的行为方式与相应的 dict
方法相同。
如果想要传递可调用对象作为默认值本身(即,not意味着被调用),或者如果要调用的可调用对象需要参数,请在适当的参数前面添加
lambda:
。例如:
d1.setdefault('div', lambda: div_callback)
d2.setdefault('foo', lambda: bar('frobozz'))
那些不喜欢覆盖
get
和 setdefault
的想法,和/或由此产生的需要测试可调用性等的人,可以使用此版本:
class LazyButHonestDict(dict):
def lazyget(self, key, thunk=lambda: None):
return self[key] if key in self else thunk()
def lazysetdefault(self, key, thunk=lambda: None):
return (self[key] if key in self else
self.setdefault(key, thunk()))
这也可以通过
defaultdict
来完成。它使用可调用实例化,然后在访问不存在的元素时调用该可调用。
from collections import defaultdict
d = defaultdict(noisy_default)
d[1] # noise
d[1] # no noise
使用
defaultdict
需要注意的是,可调用函数没有参数,因此您无法像使用 dict.setdefault
那样从键中派生默认值。这可以通过在子类中覆盖 __missing__
来缓解:
from collections import defaultdict
class defaultdict2(defaultdict):
def __missing__(self, key):
value = self.default_factory(key)
self[key] = value
return value
def noisy_default_with_key(key):
print key
return key + 1
d = defaultdict2(noisy_default_with_key)
d[1] # prints 1, sets 2, returns 2
d[1] # does not print anything, does not set anything, returns 2
有关更多信息,请参阅集合模块。
您可以使用三元运算符在一行中完成此操作:
value = cache[key] if key in cache else cache.setdefault(key, func(key))
如果您确定
cache
永远不会存储虚假值,您可以稍微简化一下:
value = cache.get(key) or cache.setdefault(key, func(key))
不,参数的评估发生在调用之前。您可以实现一个类似
setdefault
的函数,该函数将可调用对象作为第二个参数,并且仅在需要时才调用它。
似乎没有任何一行语句不需要额外的类或额外的查找。作为记录,这里有一个简单(甚至不简洁)的方法来实现这一目标,而无需它们中的任何一个。
try:
value = dct[key]
except KeyError:
value = noisy_default()
dct[key] = value
return value
对于 Python 3.8+,我能提供的最好的就是一个函数。
from typing import MutableMapping, Callable, TypeVar
K = TypeVar('K')
V = TypeVar('V')
MISSING = object()
def setdefault_lazy(d: MutableMapping[K ,V], key: K, func: Callable[[], V]) -> V:
if (value := d.get(key, MISSING)) is MISSING:
d[key] = value = func()
return value
使用案例:
d = dict()
print(setdefault_lazy(d, 1, noisy_default))
print(setdefault_lazy(d, 1, noisy_default))
您可以创建自己的
dict
类来使用此函数,如下所示:
class MyDict(dict):
setdefault_lazy = setdefault_lazy
d = MyDict(name="John")
print(d.setdefault_lazy('age', noisy_default))
print(d.setdefault_lazy('age', noisy_default))