如何记忆 **kwargs?
Posted
技术标签:
【中文标题】如何记忆 **kwargs?【英文标题】:How to memoize **kwargs? 【发布时间】:2011-09-18 11:26:12 【问题描述】:我还没有看到一种既定的方法来记忆一个接受关键字参数的函数,即某种类型的东西
def f(*args, **kwargs)
因为通常记忆器有一个dict
来缓存给定输入参数集的结果,而kwargs
是一个dict
,因此是不可散列的。我已经尝试过,在here 的讨论之后,使用
(args, frozenset(kwargs.items()))
作为缓存 dict
的键,但这仅适用于 kwargs
中的值是可散列的。此外,正如下面的答案所指出的,frozenset
不是有序数据结构。因此,此解决方案可能更安全:
(args, tuple(sorted(kwargs.items())))
但它仍然无法处理不可散列的元素。我见过的另一种方法是在缓存键中使用kwargs
的string
表示:
(args, str(sorted(kwargs.items())))
我看到的唯一缺点是散列可能很长的字符串的开销。据我所见,结果应该是正确的。谁能发现后一种方法的任何问题?下面的答案之一指出,这假设了 __str__
或 __repr__
函数对于关键字参数的值的某些行为。这似乎是一场表演。
是否有另一种更成熟的方式来实现记忆化,可以处理**kwargs
和不可散列的参数值?
【问题讨论】:
【参考方案1】:这里:
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)作为参数传递,它就可以工作。 我没有解决方案,但 collections.lru_cache() 实现可能有。 请参阅此处的 _make_key() 函数: http://code.activestate.com/recipes/578078/
【讨论】:
【参考方案2】:跟EMS说的差不多,不过最好的办法是:
key = cPickle.dumps((*args, **kwargs))
我一直在对装饰器的记忆进行大量研究和测试,这是迄今为止我发现的最好的方法。
【讨论】:
【参考方案3】:key = pickle.dumps( (args, sorted(kwargs.items()), -1 )
呢?
这似乎是一种比 str() 或 repr() 更稳健的方法。
【讨论】:
【参考方案4】: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
编辑: Ignacio 说:“虽然您不能在任意 dicts 上使用 sorted(),但 kwargs 将具有 str 键。”这是完全正确的。因此,这不是键的问题,但如果您(或不太可能的代表)以某种方式依赖排序,则可能需要牢记值。
关于使用str
:
大多数数据会很好地工作,但对手(例如在安全漏洞环境中)可能会制造冲突。介意你并不容易,因为大多数默认的repr
s 使用了很多好的分组和转义。事实上,我找不到这样的碰撞。但如果第三方草率或不完整的repr
实现,则可能会出现这种情况。
还要考虑以下几点:如果您要存储 ((<map object at 0x1377d50>,), frozenset(...))
和 ((<list_iterator object at 0x1377dd0>,<list_iterator object at 0x1377dd0>), frozenset(...))
之类的键,则只需调用相同的项目,您的缓存就会无限增长。 (您也许可以使用正则表达式来解决这个问题......)并且尝试使用生成器会弄乱您正在使用的函数的语义。如果您希望记住 is
样式的相等而不是 ==
样式的相等,这可能是理想的行为。
在解释器中执行类似str(1:object())
之类的操作,每次都会在内存中的相同位置返回一个对象!我认为这是工作中的垃圾收集器。这将是灾难性的,因为如果您碰巧正在散列 <some object at 0x???????>
并且您稍后碰巧在相同的内存位置创建了一个相同类型的对象(由于垃圾收集),您将从 memoized 函数中得到不正确的结果。如前所述,一种可能非常棘手的解决方法是使用正则表达式检测此类对象。
【讨论】:
+1 很好的一点,第二种方法是受数据的__str__
或__repr__
方法的支配。
您有没有感觉frozenset(kwargs.items())
的排序对于同一平台上的同一kwargs
是否具有确定性?
虽然你不能在任意dicts上使用sorted()
,但kwargs会有str键。
@juanchopanza: set
或 frozenset
没有排序
确实如此,所以这可能排除了第一个选项。但也许是 tuple
排序后的 kwargs.items()
...【参考方案5】:
dicts 可以按任意顺序排列,因此不能保证后者会起作用。使用sorted(kwargs.items())
先按键排序。
【讨论】:
+1 非常好!我会纠正这个问题,因为这不是重点。 顺便说一句,这会使使用frozenset的第一种方法无效吗? 我不知道frozenset
的合同细节,但我相信它可以。
Memoization 通常使用 args 作为带有结果的 dict 的键,hash(sorted('key': 'arg'.items()))
失败但 hash(frozenset('key': 'arg'.items()))
有效。以上是关于如何记忆 **kwargs?的主要内容,如果未能解决你的问题,请参考以下文章
在 Django REST Framework 中,如何在 html 模板中访问 kwargs 上下文? view.kwargs.foo 对我不起作用