Запоминание, когда аргументы могут быть очень большими

Предположим, у меня есть ссылочно прозрачная функция. Это очень легко запомнить; например :

def memoize(obj): memo = {} @functools.wraps(obj) def memoizer(*args, **kwargs): combined_args = args + (kwd_mark,) + tuple(sorted(kwargs.items())) if combined_args not in memo: memo[combined_args] = obj(*args, **kwargs) return cache[combined_args] return memoizer @memoize def my_function(data, alpha, beta): # ... 

Теперь предположим, что аргумент data my_function огромен; скажем, это frozenset с миллионами элементов. В этом случае стоимость memoization является запретительной: каждый раз нам приходилось вычислять hash(data) как часть поиска словаря.

Я могу сделать словарь memo атрибутом для data вместо объекта внутри memoize decorator. Таким образом, я могу полностью пропустить аргумент data при выполнении поиска в кеше, так как вероятность того, что еще один огромный frozenset будет одинаковой, незначительна. Однако этот подход заканчивает загрязнение аргумента, переданного my_function . Хуже того, если у меня есть два или более больших аргумента, это не поможет вообще (я могу только прикрепить memo к одному аргументу).

Есть ли что-нибудь еще, что можно сделать?

Ну, вы можете использовать «хэш» там без страхов. Хеш фенизета не рассчитывается более одного раза Python – только когда он создан – проверьте тайминги:

 >>> timeit("frozenset(a)", "a=range(100)") 3.26825213432312 >>> timeit("hash(a)", "a=frozenset(range(100))") 0.08160710334777832 >>> timeit("(lambda x:x)(a)", "a=hash(frozenset(range(100)))") 0.1994171142578125 

Не забывайте, что встроенный «хэш» Python __hash__ метод __hash__ объекта, который имеет свое возвращаемое значение, определенное во время создания встроенных hasheable объектов. Выше вы можете видеть, что вызов функции лямбда-идентификатора более чем в два раза медленнее, чем вызов хеша (a) "

Итак, если все ваши аргументы доступны, просто добавьте их хэш при создании «shared_args» – иначе просто напишите его создание, чтобы вы использовали хэш для файлов frozenset (и, возможно, других), с условным.

Оказывается, встроенный __hash__ не так уж плох, так как он кэширует свое значение после первого вычисления. Настоящий удар производительности происходит от встроенного __eq__ , поскольку он не __eq__ на одинаковых объектах и ​​фактически проходит через полное сравнение каждый раз, что делает его очень дорогостоящим.

Один из подходов, о котором я думал, заключается в подклассе встроенного класса для всех больших аргументов:

 class MyFrozenSet(frozenset): __eq__ = lambda self, other : id(self) == id(other) __hash__ = lambda self : id(self) 

Таким образом, поиск в словарях будет мгновенным. Но равенство для нового класса будет нарушено.

Лучшее решение, вероятно, таково: только когда выполняется поиск словаря, большие аргументы могут быть обернуты внутри специального класса, который переопределяет __eq__ и __hash__ для возврата id() обернутого объекта id() . Очевидная реализация обертки немного раздражает, так как она требует копирования всех стандартных методов frozenset . Возможно, извлечение его из соответствующего класса ABC может облегчить его.