Переназначение атрибута функции делает его "недоступным"

У меня есть простой маленький декоратор, который кэширует результаты вызовов функций в dict как атрибут функции.

from decorator import decorator
def _dynamic_programming(f, *args, **kwargs):
    try:
        f.cache[args]
    except KeyError:
        f.cache[args] = f(*args, **kwargs)
    return f.cache[args]

def dynamic_programming(f):
    f.cache = {}
    return decorator(_dynamic_programming, f)

Теперь я хочу добавить возможность очистки кэша. Так что я меняю dynamic_programming() функционировать так:

def dynamic_programming(f):
    f.cache = {}
    def clear():
        f.cache = {}
    f.clear = clear
    return decorator(_dynamic_programming, f)

Теперь давайте предположим, что я использую эту маленькую вещь для реализации числовой функции Фибоначчи:

@dynamic_programming
def fib(n):
    if n <= 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

>>> fib(4)
5
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}

Но теперь, когда я очищаю кеш, происходит нечто странное:

>>> fib.clear()
>>> fib.cache
{(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5}

Или (с новым ядром Python) сделайте все наоборот:

>>> fib.clear()
>>> fib(4)
5
>>> fib.cache
{}

Почему кеш почему-то не "доступен" после первого доступа к нему, т.е. не меняется при вызове clear() после звонка или звонка после clear()?

(Кстати. Я знаю решение, чтобы правильно очистить кеш: вызов f.cache.clear() вместо назначения {} чтобы он работал как положено. Меня просто интересует причина, по которой не удается назначить решение.)

2 ответа

Решение

Проблема с decorator модуль. Если вы добавите некоторые print заявления вашему декоратору:

from decorator import decorator
def _dynamic_programming(f, *args, **kwargs):
    print "Inside decorator", id(f.cache)
    try:
        f.cache[args]
    except KeyError:
        f.cache[args] = f(*args, **kwargs)
    return f.cache[args]

def dynamic_programming(f):
    f.cache = {}
    print "Original cache", id(f.cache)
    def clear():
        f.cache = {}
        print "New cache", id(f.cache)
    f.clear = clear
    return decorator(_dynamic_programming, f)

@dynamic_programming
def fib(n):
    if n <= 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

print fib(4)
print id(fib.cache)
fib.clear()
print id(fib.cache)
print fib(10)
print id(fib.cache)

Выводит (пропущены повторяющиеся строки):

Original cache 139877501744024
Inside decorator 139877501744024
5
139877501744024
New cache 139877501802208
139877501744024
Inside decorator 139877501802208
89
139877501744024

Как видите, cache Внутри декоратор меняется в соответствии с функцией очистки. Тем не менее cache доступ из __main__ не меняется. Печать cache снаружи и внутри декоратор выдает более четкую картинку (опять же, дубликаты пропускаются):

Inside decorator {}
Inside decorator {(1,): 1}
Inside decorator {(2,): 2, (0,): 1, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
5
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
Inside decorator {}
Inside decorator {(1,): 1}
Inside decorator {(2,): 2, (0,): 1, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1}
Inside decorator {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
Inside decorator {(0,): 1, (1,): 1, (2,): 2, (8,): 34, (3,): 3, (9,): 55, (4,): 5, (5,): 8, (6,): 13, (7,): 21}
89
Outside {(2,): 2, (0,): 1, (3,): 3, (1,): 1, (4,): 5}

Как видите, внутренние изменения не отражаются снаружи. Проблема в том, что внутри decorator В модуле есть строка (внутри класса, который использовался для создания декоратора):

self.dict = func.__dict__.copy()

А потом позже:

func.__dict__ = getattr(self, 'dict', {})

Так что в основном __dict__ снаружи отличается от __dict__ на внутренней. Это означает, что:

  • __dict__ копируется (не ссылается) декоратором
  • Когда cache меняется, это меняет изнутри __dict__а не снаружи __dict__
  • Следовательно cache используется _dynamic_programming очищен, но вы не можете видеть это снаружи, как декоратор __dict__ все еще указывает на старое cache (как вы можете видеть выше, как внутри cache обновления, а снаружи cache остается такой же)

Итак, подведем итог, это проблема с decorator модуль.

Поэтому ответ @matsjoyce очень интересный и всесторонний, и я знаю, что у вас уже есть решение, но мне всегда было немного яснее написать свои собственные декораторы:

def dynamic_programming(f):
    def wrapper(*args, **kwargs):
        try:
            return wrapper.cache[args]            
        except KeyError:
            res = wrapper.cache[args] = f(*args, **kwargs)
            return res
    wrapper.cache = {}
    wrapper.clear = wrapper.cache.clear
    return wrapper
Другие вопросы по тегам