如何实现一个懒惰的setdefault?

时间:2013-07-08 17:50:41

标签: python lazy-evaluation

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())

这产生如下的ouptut:

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.getLazyDict.setdefault的第二个参数不可调用时,它们的行为方式与相应的dict方法相同。

如果想要传递一个callable作为默认值本身(即 not 意味着被调用),或者如果要调用的callable需要参数,则将lambda:添加到适当的论点。 E.g:

d1.setdefault('div', lambda: div_callback)

d2.setdefault('foo', lambda: bar('frobozz'))

那些不喜欢覆盖getsetdefault的想法,和/或由此产生的测试可训练性等的需求,可以使用此版本:

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()))

4 个答案:

答案 0 :(得分:17)

这也可以通过defaultdict完成。它用一个callable实例化,然后在访问一个不存在的元素时调用它。

from collections import defaultdict

d = defaultdict(noisy_default)
d[1] # noise
d[1] # no noise

defaultdict的警告是,callable没有参数,因此您无法使用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

有关详细信息,请参阅collections模块。

答案 1 :(得分:9)

不,在调用之前发生了对参数的评估。你可以实现一个类似setdefault的函数,它将一个可调用的函数作为它的第二个参数,并且仅在需要时调用它。

答案 2 :(得分:5)

您可以使用三元运算符在单行中执行此操作:

value = cache[key] if key in cache else cache.setdefault(key, func(key))

如果你确定cache永远不会存储假值,你可以稍微简化一下:

value = cache.get(key) or cache.setdefault(key, func(key))

答案 3 :(得分:0)

似乎没有一种不需要额外的类或额外的查找的代码。记录下来,这是在没有任何一种情况下实现这一目标的简单(甚至不简洁)的方法。

try:
    value = dct[key]
except KeyError:
    value = noisy_default()
    dct[key] = value
return value