Подтвердить что ты не робот

Как реализовать ленивое setdefault?

Одно незначительное раздражение с 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 и ответом Павла Аносова. В то время как на этом я пошел вперед и реализовал ленивую версию, так как основная идея по сути то же самое.

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 выше в d.setdefault теперь является вызываемым, а не вызовом функции.

Когда второй аргумент LazyDict.get или LazyDict.setdefault не является вызываемым, они ведут себя так же, как и соответствующие методы dict.

Если вы хотите передать вызываемое значение как значение по умолчанию (т.е. Не предназначено для вызова), или если вызываемый вызываемый требует аргументов, добавьте 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()))
4b9b3361

Ответ 1

Нет, оценка аргументов происходит до вызова. Вы можете реализовать функцию setdefault -like, которая принимает вызов в качестве второго аргумента и вызывает его только в том случае, если это необходимо.

Ответ 2

Это также можно выполнить с помощью defaultdict. Он создается с помощью вызываемого, который затем вызывается, когда к нему обращается несуществующий элемент.

from collections import defaultdict

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

defaultdict с 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

Для получения дополнительной информации см. Модуль коллекций.

Ответ 3

Вы можете сделать это в одном слое с помощью тройного оператора:

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

Если вы уверены, что cache никогда не будет хранить значения фальшивки, вы можете немного упростить его:

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

Ответ 4

Кажется, нет ни одного лайнера, который не требует дополнительного класса или дополнительного поиска. Для справки, вот простой (даже не лаконичный) способ достижения этого без любого из них.

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