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

jas*_*onh 13 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)
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

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

use*_*ica 4

这是 Python 3.7.3 次要版本中特有的错误。它在 Python 3.7.2 中不存在,也不应该在 Python 3.7.4 或 3.8.0 中存在。它被归档为Python 问题 36650

在 C 级别,不带关键字参数的调用和带空**kwargs字典的调用的处理方式有所不同。根据函数实现方式的细节,函数可能会接收NULLkwargs 而不是空的 kwargs 字典。使用 kwargs 处理的调用的C 加速器functools.lru_cacheNULL使用空 kwargs 字典的调用不同,导致了您在此处看到的错误。

使用您正在使用的方法缓存配方,对方法的第一次调用将始终将空的 kwargs 字典传递给 C 级 LRU 包装器,无论是否使用任何关键字参数,因为return cached_method(*args, **kwargs)in wrapped_func。后续调用可能会传递NULLkwargs 字典,因为它们不再经过wrapped_func. 这就是为什么您无法使用 ; 重现该错误的原因test_memoization_kwarg_call。第一次调用不必传递任何关键字参数。