【问题标题】:Python function returns different result with memoizationPython函数通过记忆返回不同的结果
【发布时间】:2019-01-06 04:22:51
【问题描述】:

如果我使用记忆装饰器,我的 Python 寻路函数会返回不同的结果。它自己返回一个正确的值,但被记忆后,它返回一个错误的值。

我说的函数是这样的:

@functools.lru_cache(maxsize=None)
def recursiveTraversal(originIndex, target, steps):
    startingVertex = data[originIndex]

    if startingVertex["ID"] == target:
        if steps == 0:
            return Path(0, [])
        else:
            return None
    else:
        if steps == 0:
            return None
        else:
            potentialPaths = []
            for i in startingVertex["Edges"]:
                nextVertex = data[i]
                nextPath = recursiveTraversal(i, target, steps - 1)
                if nextPath == None:
                    continue
                nextPath.weight += int(nextVertex["Weight"])
                nextPath.vertices.append(i)

                potentialPaths.append(nextPath)
            if len(potentialPaths) > 0:
                minPath = min(potentialPaths, key=lambda x: x.weight)
                return minPath
            else:
                return None

一个完整的可运行示例can be found here。文件的上半部分是所有数据,代码在下。要重现这一点,只需注释掉第 15 行,并观察输出是否不同。

如何让memoized版本输出和普通版本一样的东西?

【问题讨论】:

  • 输入在哪里?
  • 你能给我们一个minimal reproducible example吗?首先,该输入应该是问题的一部分,而不是评论。更重要的是,如果你只是在我们给定的代码上运行recursiveTraversal(0, "1", 3),显然只会在data 上引发NameError
  • 另外,你说这里的数据根本没有改变……但那些nextPath 值不只是引用data 中的某个节点吗?一眼看去,我看不到任何地方可以返回其他任何东西。然后您立即通过更改其权重并附加另一个顶点来修改返回的任何内容。
  • nextVertex 是对data 的引用,但我实际上是在更改nextPath,它是一个Path 对象,而不是对data 的引用。
  • 我刚刚用一个可运行的示例更新了这个(包含dataPath 类)。

标签: python recursion optimization path-finding memoization


【解决方案1】:

问题是您正在修改recursiveTraversal 的返回值的属性。此函数返回Path 对象,您可以修改它们的属性weightvertices。因此,对于非缓存版本,每次使用 (x, y, z) 参数调用函数时,都会创建一个新的 Path(0, []) 对象,并且稍后会在 for 循环中修改其属性。但是对于每个(x, y, z) 调用,您都可以确保从一个新对象开始。现在对于缓存版本,缓存包装器不是通过一直向下递归树来提供新对象,而是为您提供先前创建的 Path 对象的实例(它已经修改了 weight 和 @987654333 @ 属性)并且这些被进一步修改(即这会修改缓存)。这可以从下面的例子中看出:

# Augment `Path` class with `__repr__`.
class Path:
    # Other methods go here.

    def __repr__(self):
        return '{}({}, {})'.format(self.__class__.__name__, repr(self.weight), repr(self.vertices))

data = [
    {'ID': '2', 'Weight': 1, 'Edges': [1]},
    {'ID': '1', 'Weight': 1, 'Edges': []}
]

print(recursiveTraversal(0, '1', 1))  # Prints "Path(1, [1])".
print(recursiveTraversal(1, '1', 0))  # Prints "Path(1, [1])".

检查函数recursiveTraversal 似乎对于steps=0 它应该返回Path(0, []),以防目标匹配。然而它返回Path(1, [1])。发生这种情况是因为之前对recursiveTraversal 的调用已经调用了recursiveTraversal(1, '1', 0) 并修改了结果的weightvertices 属性。在对recursiveTraversal(1, '1', 0) 执行第二次显式调用时,您将取回对该对象的缓存引用。

可能的解决方案

一种可能的解决方案是在进一步修改缓存对象之前创建它们的副本。这可以防止缓存被修改。

from copy import deepcopy

# Within `recursiveTraversal`:
# ...
nextPath = deepcopy(recursiveTraversal(i, target, steps - 1))
# ...

【讨论】:

  • 非常感谢,完全正确!对于遇到这种情况的任何其他人,似乎缓存存储 references 到返回值,而不是返回值本身。这就是我的问题的根源。
  • @Reubend 缓存准确地存储函数返回的内容。但是,在 Python 中,变量的概念与 C++ 中的示例不同。 This postthis post 会给你更多的洞察力。
猜你喜欢
  • 2022-06-17
  • 2021-02-07
  • 2020-07-28
  • 2014-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-17
  • 1970-01-01
相关资源
最近更新 更多