Переназначение атрибута функции делает его "недоступным"
У меня есть простой маленький декоратор, который кэширует результаты вызовов функций в 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