I use a decorator to extend memoization via lru_cache to methods of objects which aren't themselves hashable (following stackoverflow.com/questions/33672412/python-functools-lru-cache-with-class-methods-release-object). This memoization works fine with python 3.6 but shows unexpected behavior on python 3.7.
Observed behavior: If the memoized method is called with keyword arguments, memoization works fine on both the python versions. If it's called without keyword arg syntax, it works on 3.6 but not on 3.7.
==> What could cause the different behavior?
The code sample below shows a minimal example which reproduces the behavior.
test_memoization_kwarg_call
passes for both python 3.6 and 3.7.
test_memoization_arg_call
passes for python 3.6 but fails for 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)
Note that, weirdly, the line assert obj.randint(1) == obj.randint(1)
does not lead to a test failure in test_memoization_kwarg_call
when used in python 3.6 but fails for python 3.7 inside test_memoization_arg_call
.
Python versions: 3.6.8 and 3.7.3, respectively.
Further info
user2357112 suggested to inspect import dis; dis.dis(test_memoization_arg_call)
.
On python 3.6 this gives
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
On python 3.7 this gives
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
the difference being that on 3.6 the call to the cached randint
method yields LOAD_ATTR, LOAD_CONST, CALL_FUNCTION
while on 3.7 it is yields LOAD_METHOD, LOAD_CONST, CALL_METHOD
. This may explain the difference in behavior but I do not understand the internals of CPython (?) to understand it. Any ideas?