记住在Python 3.6上有效但在3.7.3上无效的方法

时间:2019-04-24 15:51:22

标签: python python-3.x python-3.6 python-3.7

我使用装饰器将通过lru_cache的备注扩展到本身不是可哈希的对象的方法(在stackoverflow.com/questions/33672412/python-functools-lru-cache-with-class-methods-release-object之后)。 此备忘录可在python 3.6上正常运行,但在python 3.7上显示出意外的行为。

观察到的行为: 如果使用关键字参数调用备注方法,则备注在两个python版本上均能正常工作。如果在不使用关键字arg语法的情况下调用它,那么它将在3.6上有效,但在3.7上无效。

==>什么会导致不同的行为?

下面的代码示例显示了一个重现此行为的最小示例。

test_memoization_kwarg_call适用于python 3.6和3.7。 test_memoization_arg_call适用于python 3.6,但适用于3.7。

import random
import weakref
from functools import lru_cache


def memoize_method(func):
    # From stackoverflow.com/questions/33672412/python-functools-lru-cache-with-class-methods-release-object
    def wrapped_func(self, *args, **kwargs):
        self_weak = weakref.ref(self)

        @lru_cache()
        def cached_method(*args_, **kwargs_):
            return func(self_weak(), *args_, **kwargs_)

        setattr(self, func.__name__, cached_method)
        print(args)
        print(kwargs)
        return cached_method(*args, **kwargs)

    return wrapped_func


class MyClass:
    @memoize_method
    def randint(self, param):
        return random.randint(0, int(1E9))


def test_memoization_kwarg_call():
    obj = MyClass()
    assert obj.randint(param=1) == obj.randint(param=1)
    assert obj.randint(1) == obj.randint(1)


def test_memoization_arg_call():
    obj = MyClass()
    assert obj.randint(1) == obj.randint(1)

请注意,奇怪的是,行assert obj.randint(1) == obj.randint(1)在python 3.6中使用时不会导致test_memoization_kwarg_call中的测试失败,但在test_memoization_arg_call内部的python 3.7中失败。

Python版本:分别为3.6.8和3.7.3。

其他信息

user2357112建议检查import dis; dis.dis(test_memoization_arg_call)。 在python 3.6上,这给出了

 36           0 LOAD_GLOBAL              0 (MyClass)
              2 CALL_FUNCTION            0
              4 STORE_FAST               0 (obj)

 37           6 LOAD_FAST                0 (obj)
              8 LOAD_ATTR                1 (randint)
             10 LOAD_CONST               1 (1)
             12 CALL_FUNCTION            1
             14 LOAD_FAST                0 (obj)
             16 LOAD_ATTR                1 (randint)
             18 LOAD_CONST               1 (1)
             20 CALL_FUNCTION            1
             22 COMPARE_OP               2 (==)
             24 POP_JUMP_IF_TRUE        30
             26 LOAD_GLOBAL              2 (AssertionError)
             28 RAISE_VARARGS            1
        >>   30 LOAD_CONST               0 (None)
             32 RETURN_VALUE

在python 3.7上,这给出了

 36           0 LOAD_GLOBAL              0 (MyClass)
              2 CALL_FUNCTION            0
              4 STORE_FAST               0 (obj)

 37           6 LOAD_FAST                0 (obj)
              8 LOAD_METHOD              1 (randint)
             10 LOAD_CONST               1 (1)
             12 CALL_METHOD              1
             14 LOAD_FAST                0 (obj)
             16 LOAD_METHOD              1 (randint)
             18 LOAD_CONST               1 (1)
             20 CALL_METHOD              1
             22 COMPARE_OP               2 (==)
             24 POP_JUMP_IF_TRUE        30
             26 LOAD_GLOBAL              2 (AssertionError)
             28 RAISE_VARARGS            1
        >>   30 LOAD_CONST               0 (None)
             32 RETURN_VALUE

区别在于,在3.6上调用缓存的randint方法会产生LOAD_ATTR, LOAD_CONST, CALL_FUNCTION,而在3.7上会产生LOAD_METHOD, LOAD_CONST, CALL_METHOD。这也许可以解释行为上的差异,但是我不了解CPython(?)的内部知识。有什么想法吗?

3 个答案:

答案 0 :(得分:4)

这是特定于Python 3.7.3次要版本的错误。它在Python 3.7.2中不存在,并且在Python 3.7.4或3.8.0中不应该存在。它被归档为Python issue 36650

在C级别,没有关键字参数的调用和带有空**kwargs字典的调用的处理方式有所不同。取决于函数实现方式的详细信息,该函数可能会收到NULL的kwarg,而不是空的kwargs字典。 functools.lru_cache的C加速器对待带有NULL kwargs的调用与带有空kwargs dict的调用的处理方式不同,从而导致您在此处看到的错误。

使用您正在使用的方法缓存配方时,由于{{1},无论是否使用任何关键字参数,对方法的第一次调用将始终将空的kwargs字典传递给C级LRU包装器。 return cached_method(*args, **kwargs)中的}。后续呼叫可能会通过wrapped_func格言,因为它们不再通过NULL。这就是为什么您无法使用wrapped_func重现该错误的原因; first 调用不必传递任何关键字参数。

答案 1 :(得分:2)

关于这个问题,我有一个更简单的解决方案:

pip install methodtools

然后

import random
from methodtools import lru_cache


class MyClass:
    @lru_cache()
    def randint(self, param):
        return random.randint(0, int(1E9))


def test_memoization_kwarg_call():
    obj = MyClass()
    assert obj.randint(param=1) == obj.randint(param=1)
    assert obj.randint(1) == obj.randint(1)

很抱歉,这不是“为什么”的答案,但是如果您也对解决问题感兴趣。已通过3.7.3进行了测试。

答案 2 :(得分:1)

我以前从未对python这么说过,但是老实说这看起来像个bug。我不知道为什么会这样,因为所有这些东西都在底层C中。

但这是我所看到的,试图窥视黑匣子:

我在代码中添加了一些简单的打印方法:

def memoize_method(func):
    # From stackoverflow.com/questions/33672412/python-functools-lru-cache-with-class-methods-release-object
    def wrapped_func(self, *args, **kwargs):
        self_weak = weakref.ref(self)

        print('wrapping func')
        @lru_cache()
        def cached_method(*args_, **kwargs_):
            print('in cached_method', args_, kwargs_, id(cached_method))
            return func(self_weak(), *args_, **kwargs_)

        setattr(self, func.__name__, cached_method)
        return cached_method(*args, **kwargs)

    return wrapped_func

然后我像这样测试了功能:

def test_memoization_arg_call():
    obj = MyClass()
    for _ in range(5):
        print(id(obj.randint), obj.randint(1), obj.randint.cache_info(), id(obj.randint))
    print()
    for _ in range(5):
        print(id(obj.randint), obj.randint(2), obj.randint.cache_info(), id(obj.randint))

这是输出:

==================================
wrapping func
in cached_method (1,) {} 4525448992
4521585800 668415661 CacheInfo(hits=0, misses=1, maxsize=128, currsize=1) 4525448992
in cached_method (1,) {} 4525448992
4525448992 920166498 CacheInfo(hits=0, misses=2, maxsize=128, currsize=2) 4525448992
4525448992 920166498 CacheInfo(hits=1, misses=2, maxsize=128, currsize=2) 4525448992
4525448992 920166498 CacheInfo(hits=2, misses=2, maxsize=128, currsize=2) 4525448992
4525448992 920166498 CacheInfo(hits=3, misses=2, maxsize=128, currsize=2) 4525448992

in cached_method (2,) {} 4525448992
4525448992 690871031 CacheInfo(hits=3, misses=3, maxsize=128, currsize=3) 4525448992
4525448992 690871031 CacheInfo(hits=4, misses=3, maxsize=128, currsize=3) 4525448992
4525448992 690871031 CacheInfo(hits=5, misses=3, maxsize=128, currsize=3) 4525448992
4525448992 690871031 CacheInfo(hits=6, misses=3, maxsize=128, currsize=3) 4525448992
4525448992 690871031 CacheInfo(hits=7, misses=3, maxsize=128, currsize=3) 4525448992

有趣的是,它似乎误缓存了第一个位置args调用。 kwargs不会发生这种情况,如果您先调用kwargs调用,它也不会错误地缓存该请求或随后的任何pos args调用(无论出于何种原因,这意味着您的kwargs测试都在工作)。重要的内容是:

==================================
wrapping func
in cached_method (1,) {} 4525448992
4521585800 668415661 CacheInfo(hits=0, misses=1, maxsize=128, currsize=1) 4525448992
in cached_method (1,) {} 4525448992
4525448992 920166498 CacheInfo(hits=0, misses=2, maxsize=128, currsize=2) 4525448992
4525448992 920166498 CacheInfo(hits=1, misses=2, maxsize=128, currsize=2) 4525448992

您可以看到我在ID为cached_method的函数4525448992中使用相同的args / kwargs两次,但是它没有缓存。它甚至在CacheInfo中显示未命中(首先,缓存为空。其次,由于某种原因找不到(1,))。全部用C语言编写,所以我不知道如何解决...

我想最好的答案是使用另一种lru_cache方法,并等待开发人员修复此处发生的一切。

编辑:顺便说一句,很好的问题。