【问题标题】:Memoization recipe that allows non-hashable arguments允许不可散列参数的记忆配方
【发布时间】:2015-06-18 05:55:35
【问题描述】:

常见的记忆配方(如thisthese)使用dict 来存储缓存,因此要求函数参数是可散列的。

我希望函数可以使用尽可能多的不同参数类型,当然包括dictsetlist。实现这一目标的最佳方法是什么?

我正在考虑的一种方法是将所有不可散列的参数包装到它们的可散列子类中(即,定义一个 dict 的子类来定义它自己的 __hash__ 函数)。

另外,我正在考虑创建一个 dict 的子类,它依赖于与 hash 不同的哈希函数(定义一个递归地在容器上工作的全局 my_hash 函数并不难),并使用它子类来存储缓存。但我认为没有一种简单的方法可以做到这一点。

编辑:

我想我会尝试我建议的the solution 用于python 容器的一般散列。有了这个,我应该能够将(*args, **kwargs) 的元组包装到自动哈希类中,并使用常规记忆。

【问题讨论】:

  • 获取被调用函数本身然后将​​其用作哈希?将需要一些自省并检查调用堆栈......但我认为应该是可行的。 (不过我不会在凌晨 2:30 尝试:p)
  • @JonClements 很有趣,我可以试试。但是我不会有同样的问题吗,因为内省会返回不可散列的对象?
  • @JonClements:这不会破坏记忆的全部意义吗?
  • 无论你怎么做,你要么必须深度复制输入,要么你必须要求传递给你的 memoized 函数的任何东西在它进入后都不会发生突变备忘录。我怀疑让 memoization 装饰器的用户完成将函数及其参数放入哈希友好形式的工作总体上比尝试接受不可哈希的参数要好。
  • 不知道怎么没看到这个问题has already been asked

标签: python python-3.x hash memoization


【解决方案1】:

dict/list/set/etc 是有原因的。不可散列,而是它们是可变的

这是它们的不可变对应物存在的主要原因之一(frozendict/frozenset/tuple)。 (嗯,元组并不完全是一个冻结列表,但实际上它可以达到目的)。

因此,为了您的目的,请使用不可变的替代方案。

这里快速演示了为什么不应该散列可变对象。请记住,散列需要a==b ==> hash(a)==hash(b)

@memoize
def f(x): ...

d1 = {'a':5}
d2 = {'a':99}
res1 = f(d1)
res2 = f(d2)
d1['a'] = 99
res3 = f(d1)  # what should be returned? not well defined...

【讨论】:

  • 哈希不可变值只会成为问题,IMO,当您修改已经存储在基于哈希的数据结构中的对象时。例如,d = {d1, d2},然后修改d1d1 最终会进入错误的存储桶,本质上会破坏集合的数据结构。那,AFAIK,是可变值在 python 中不可散列的唯一原因。但是在您的示例(和我的用例)中,似乎没有问题。当然,更改后的f(d1) 应该返回与f(d2) 相同的结果——不管有没有记忆。并且记忆应该正确识别d1的当前值等于d2
  • @max 正确。实现这一点的唯一方法是在将密钥存储在缓存中之前“冻结”密钥,这是通过使用例如完成的。一个冻结的字典。
  • 好吧,如果你真的想确保没有人弄乱钥匙,你是对的。但在某些情况下,相信自己和/或其他开发人员要小心是可以的,然后您就可以使用和散列字典了。
【解决方案2】:

方法一(拆分键和值)

这是基于字典只是压缩键和值的想法。 有了这个想法,我们可以将类似的东西做成一个字典来存储键(函数参数)和值(函数的返回值)。

由于它使用list.index,所以不确定它会有多慢。也许zipping 会更快?

class memoize:
    def __init__(self, func):
        self.func = func
        self.known_keys = []
        self.known_values = []

    def __call__(self, *args, **kwargs):
        key = (args, kwargs)

        if key in self.known_keys:
            i = self.known_keys.index(key)
            return self.known_values[i]
        else:
            value = self.func(*args, **kwargs)
            self.known_keys.append(key)
            self.known_values.append(value)

            return value

有效!:

>>> @memoize
... def whatever(unhashable):
...     print(*unhashable) # Just to know when called for this example
...     return 12345
...
>>> whatever([1, 2, 3, 4])
1 2 3 4
12345
>>> whatever([1, 2, 3, 4])
12345
>>> whatever({"a": "b", "c": "d"})
a c
12345
>>> whatever({"a": "b", "c": "d"})
12345

方法 2(假哈希)

class memoize:
    def __init__(self, func):
        self.func = func
        self.known = {}

    def __call__(self, *args, **kwargs):
        key = give_fake_hash((args, kwargs))

        try:
            return self.known[key]
        except KeyError:
            value = self.func(*args, **kwargs)
            self.known[key] = value
            return value

def give_fake_hash(obj):
    cls = type(obj)
    name = "Hashable" + cls.__name__

    def fake_hash(self):
        return hash(repr(self))

    t = type(name, (cls, ), {"__hash__": fake_hash})

    return t(obj)

方法 2.5(用于 dicts)

import operator

class memoize:
    def __init__(self, func):
        self.func = func
        self.known = {}

    def __call__(self, *args, **kwargs):
        key = give_fake_hash((args, kwargs))

        try:
            return self.known[key]
        except KeyError:
            value = self.func(*args, **kwargs)
            self.known[key] = value
            return value

def fake_hash(self):
    return hash(repr(self))

class HashableTuple(tuple):
    __hash__ = fake_hash

class RereprDict(dict):
    def __repr__(self):
        try:
            self._cached_repr
        except AttributeError:
            self._cached_repr = repr(sorted(self.items(), key=operator.itemgetter(0)))
        finally:
            return self._cached_repr

    __hash__ = fake_hash

def fix_args(args):
    for elem in args:
        if isinstance(elem, dict):
            elem = RereprDict(elem)
        yield elem

def give_fake_hash(tup):
    args, kwargs = tup

    args = tuple(fix_args(args))
    kwargs = RereprDict(kwargs)

    return HashableTuple((args, kwargs))

【讨论】:

  • 是的,它会起作用,但由于只有在性能成为问题时才使用记忆,所以我更愿意保留 O(1) 和高度优化的 python dict 的好处。
  • 看方法2保持dicts的速度。
  • fake_hash 无法在字典中正常工作,因为评估为 == 的两个字典可能使用 repr 以不同的顺序显示它们的内容,因此最终得到不同的哈希值。
  • 好吧,见方法 2.5。在这一点上,它可能甚至不值得记忆。
  • 即使使用repr(sorted(self.items())),也不能保证哈希是有效的,因为集合字典(例如)仍然可以以任意顺序打印值......看起来像这样这是一个非常烦人的问题,我希望python为此提供了一些内置支持。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-07
  • 1970-01-01
  • 2017-10-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多