Запоминание метода, работающего на Python 3.6, но не на 3.7.3

Я использую декоратор для расширения запоминания через lru_cache на методы объектов, которые сами по себе не могут быть хэшируемыми (следуя qaru.site/info/313334/...). Эта памятка прекрасно работает с питоном 3.6, но демонстрирует неожиданное поведение на питоне 3.7.

Наблюдаемое поведение: если метод memoized вызывается с аргументами ключевого слова, то меморизация отлично работает в обеих версиях 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/info/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) не приводит к сбою теста в test_memoization_kwarg_call при использовании в python 3.6, но завершается неудачей для python 3.7 внутри test_memoization_arg_call.

Версии Python: 3.6.8 и 3.7.3 соответственно.

Дальнейшая информация

user2357112 предложил проверить import dis; dis.dis(test_memoization_arg_call) import dis; dis.dis(test_memoization_arg_call). На питоне 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

На питоне 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 (?), Чтобы понять это. Есть идеи?

Ответ 1

Это ошибка, в частности, в минорном выпуске Python 3.7.3. Его не было в Python 3.7.2, и он не должен присутствовать в Python 3.7.4 или 3.8.0. Он был подан как выпуск Python 36650.

На уровне C вызовы без аргументов ключевого слова и вызовы с пустым dict **kwargs обрабатываются по-разному. В зависимости от деталей того, как реализована функция, функция может получить NULL для kwargs вместо пустого dict kwargs. Ускоритель C для functools.lru_cache обрабатывает вызовы с NULL kwargs по-другому, чем вызовы с пустым dicw, что приводит к ошибке, которую вы видите здесь.

В используемом вами рецепте кэширования метода первый вызов метода всегда будет передавать пустой аргумент kwargs оболочке LRU уровня C, независимо от того, использовались ли какие-либо ключевые аргументы, из-за return cached_method(*args, **kwargs) в wrapped_func. Последующие вызовы могут проходить диктовку NULL kwargs, потому что они больше не проходят через wrapped_func. Вот почему вы не можете воспроизвести ошибку с test_memoization_kwarg_call; первый вызов не должен передавать аргументы без ключевых слов.

Ответ 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.

Ответ 3

Можете ли вы предоставить некоторые сведения о том, почему именно вы выбрали вышеуказанную форму напоминания? Как я вижу это


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

сам достигает запоминания randint, который вы определили.

Ответ 4

я никогда не говорил этого о питоне, но это, честно говоря, похоже на ошибку. Я понятия не имею, почему это происходит, потому что все эти вещи находятся в основе C.

но вот что я вижу, пытаясь заглянуть в черный ящик:

я добавил простую печать в ваш код:

def memoize_method(func):
    # From stackoverflow.com/info/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

Интересно, что кажется, что он неправильно кэширует первый вызов позиционных аргументов. это не происходит с 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

Вы можете видеть, что я нахожусь в функции cached_method с идентификатором 4525448992 дважды с точно 4525448992 же аргументами /kwargs, но это не кэширование. он даже показывает сами промахи в CacheInfo (во-первых, кеш пуст). во-вторых, он не может найти (1,) по какой-то причине). что все в C, так что я не знаю, как это исправить...

Я думаю, что лучший ответ - использовать другой метод lru_cache и ждать, пока разработчики исправят все, что здесь происходит.

редактировать: кстати, отличный вопрос.