python - 在 python 3.6 上工作但不在 3.7.3 上工作的方法的记忆
问题描述
我使用装饰器通过 lru_cache 将 memoization 扩展到本身不可散列的对象的方法(遵循stackoverflow.com/questions/33672412/python-functools-lru-cache-with-class-methods-release-object)。这种记忆在 python 3.6 上运行良好,但在 python 3.7 上显示出意外的行为。
观察到的行为: 如果使用关键字参数调用 memoized 方法,则 memoization 在两个 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)
请注意,奇怪的是,该行在python 3.6assert obj.randint(1) == obj.randint(1)
中使用时不会导致测试失败,test_memoization_kwarg_call
但在 python 3.7 inside中会失败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
在 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
方法 yieldLOAD_ATTR, LOAD_CONST, CALL_FUNCTION
而在 3.7 上调用 yield LOAD_METHOD, LOAD_CONST, CALL_METHOD
。这可以解释行为上的差异,但我不了解 CPython 的内部结构(?)来理解它。有任何想法吗?
解决方案
这是 Python 3.7.3 次要版本中的一个错误。它不存在于 Python 3.7.2 中,也不应该存在于 Python 3.7.4 或 3.8.0 中。它被归档为Python 问题 36650。
在 C 级别,不带关键字参数的调用和带空**kwargs
dict 的调用的处理方式不同。根据函数实现方式的细节,函数可能会接收NULL
kwargs 而不是空的 kwargs dict。functools.lru_cache
使用 kwargs处理调用的 C 加速器NULL
与使用空 kwargs dict 的调用不同,导致您在此处看到的错误。
使用您正在使用的方法缓存配方,对方法的第一次调用将始终将一个空的 kwargs dict 传递给 C 级 LRU 包装器,无论是否使用了任何关键字参数,因为return cached_method(*args, **kwargs)
in wrapped_func
. 随后的调用可能会通过NULL
kwargs dict,因为它们不再通过wrapped_func
. 这就是为什么您无法使用test_memoization_kwarg_call
;重现该错误的原因。第一次调用必须不传递关键字参数。
推荐阅读
- c# - ASP.NET Core Worker 服务中的依赖注入
- java - Android Studio choreographer.java
- php - if 语句存在问题并与特定类别匹配
- c# - 为什么我不能将类型 'T' 转换为 int where T : System.Enum?
- mysql - 是更新 MySQL Atomic 中多行的单个查询吗?
- javascript - 如何根据 ReactJS 中的值过滤对象?
- security - 如何将 Auth Bearer 令牌传递给 Wapiti Web 应用程序漏洞扫描程序?
- reactjs - 试图在反应中嵌入另一个 Intranet 站点
- gitlab - Gitlab选项自动为问题创建分支/合并请求
- java - 如何使用 MyBatis BlobInputStreamTypeHandler / ClobReaderTypeHandler 流式传输选择请求的内容?