Как memoize ** kwargs?
Я не видел установленного способа memoize функции, которая принимает аргументы ключевого слова, то есть что-то типа
def f(*args, **kwargs)
поскольку, как правило, memoizer имеет dict
для кэширования результатов для заданного набора входных параметров, а kwargs
является dict
и, следовательно, недоступен. Я пробовал, после обсуждения здесь, используя
(args, frozenset(kwargs.items()))
в качестве ключа к кешу dict
, но это работает только в том случае, если значения в kwargs
являются хешируемыми. Кроме того, как указано в ответах ниже, frozenset
не является упорядоченной структурой данных. Поэтому это решение может быть более безопасным:
(args, tuple(sorted(kwargs.items())))
Но он все еще не может справиться с не-хэшируемыми элементами. Другой подход, который я видел, - использовать string
представление kwargs
в кеше:
(args, str(sorted(kwargs.items())))
Единственный недостаток, который я вижу с этим, - накладные расходы на хэширование потенциально очень длинной строки. Насколько я вижу, результаты должны быть правильными. Может ли кто-нибудь выявить какие-либо проблемы с последним подходом? Один из приведенных ниже ответов указывает, что это предполагает определенное поведение функций __str__
или __repr__
для значений аргументов ключевого слова. Это похоже на шоу-стоппер.
Есть ли еще один, более простой способ достижения memoization, который может справиться с **kwargs
и значениями un-hashable?
Ответы
Ответ 1
key = (args, frozenset(kwargs.items())
Это "лучший", который вы можете сделать, не делая предположений о своих данных.
Однако представляется возможным захотеть выполнить замещение в словарях (хотя это немного необычно), вы можете сделать особый случай, если захотите. Например, вы можете рекурсивно применять frozenset(---.items())
при копировании словарей.
Если вы выполняете sorted
, вы можете оказаться в плохой ситуации, когда у вас есть неупорядоченные ключи. Например, "Подмножество и сравнения сравнений не обобщаются на полную функцию упорядочения. Например, любые два непересекающихся множества не равны и не являются подмножествами друг друга, поэтому все последующие возвращают False: ab. Соответственно, не реализуйте метод cmp().
>>> sorted([frozenset({1,2}), frozenset({1,3})])
[frozenset({1, 2}), frozenset({1, 3})]
>>> sorted([frozenset({1,3}), frozenset({1,2})]) # THE SAME
[frozenset({1, 3}), frozenset({1, 2})] # DIFFERENT SORT RESULT
# sorted(stuff) != sorted(reversed(stuff)), if not strictly totally ordered
edit: Игнасио говорит: "Пока вы не можете использовать sorted() для произвольных dicts, kwargs будет иметь str-ключи". Это совершенно правильно. Таким образом, это не проблема для ключей, хотя, возможно, что-то нужно помнить о значениях, если вы (или маловероятные репрезентации) полагаетесь на сортировку как-то.
Относительно использования str
:
В этом случае большинство данные будут работать хорошо, но для противника может возникнуть конфликт (например, в контексте уязвимости безопасности). Вам нелегко думать, потому что большинство стандартных repr
используют множество хороших группировок и побегов. На самом деле я не смог найти такое столкновение. Но это возможно с неряшливыми сторонними или неполными реализациями repr
.
Также обратите внимание на следующее: если вы храните ключи типа ((<map object at 0x1377d50>,), frozenset(...))
и ((<list_iterator object at 0x1377dd0>,<list_iterator object at 0x1377dd0>), frozenset(...))
, ваш кеш будет неограниченно расти, просто вызывая одни и те же элементы. (Возможно, вы можете обойти эту проблему с помощью регулярного выражения...) И попытка использовать генераторы испортит семантику функции, которую вы используете. Это может быть желательным поведением, но если вы хотите memoize на is
-стилевое равенство, а не ==
-стилевое равенство.
Кроме того, что-то вроде str({1:object()})
в интерпретаторе будет возвращать объект в том же месте в памяти каждый раз! Я думаю, что это сборщик мусора на работе. Это было бы катастрофой, потому что, если вам посчастливилось хешировать <some object at 0x???????>
, и позже вы создадите объект того же типа в том же месте памяти (из-за сбора мусора), вы получите неверные результаты из memoized функции. Как уже упоминалось, одним из возможных способов взлома является обнаружение таких объектов с помощью регулярного выражения.
Ответ 2
dicts может быть в произвольном порядке, поэтому нет гарантии, что последний будет работать. Используйте sorted(kwargs.items())
, чтобы сначала отсортировать его по ключевым словам.
Ответ 3
Это похоже на то, что сказал EMS, но лучший способ:
key = cPickle.dumps((*args, **kwargs))
Я проводил много исследований и тестов для запоминания с декораторами, и это лучший метод, который я нашел до сих пор.
Ответ 4
Как насчет key = pickle.dumps( (args, sorted(kwargs.items()), -1 )
?
Это, по-видимому, более надежный подход, чем str() или repr().
Ответ 5
Здесь:
from functools import wraps
def memoize(fun):
"""A simple memoize decorator for functions supporting positional args."""
@wraps(fun)
def wrapper(*args, **kwargs):
key = (args, frozenset(sorted(kwargs.items())))
try:
return cache[key]
except KeyError:
ret = cache[key] = fun(*args, **kwargs)
return ret
cache = {}
return wrapper
Тесты:
import unittest
class TestMemoize(unittest.TestCase):
def test_it(self):
@memoize
def foo(*args, **kwargs):
"foo docstring"
calls.append(None)
return (args, kwargs)
calls = []
# no args
for x in range(2):
ret = foo()
expected = ((), {})
self.assertEqual(ret, expected)
self.assertEqual(len(calls), 1)
# with args
for x in range(2):
ret = foo(1)
expected = ((1, ), {})
self.assertEqual(ret, expected)
self.assertEqual(len(calls), 2)
# with args + kwargs
for x in range(2):
ret = foo(1, bar=2)
expected = ((1, ), {'bar': 2})
self.assertEqual(ret, expected)
self.assertEqual(len(calls), 3)
self.assertEqual(foo.__doc__, "foo docstring")
unittest.main()
Это работает до тех пор, пока вы не передадите в качестве аргумента нераспадаемый тип (например, dict).
У меня нет для этого решения, но у реализации collection.lru_cache() может быть.
См. Здесь функцию _make_key():
http://code.activestate.com/recipes/578078/