Как я могу написать декоратор Python для кэширования?

Я пытаюсь написать декоратор python для memoize. У меня есть несколько вопросов.

  1. Как @memoize преобразуется в функцию call класса memoize?
  2. Почему init ожидает аргумент.
  3. Где хранится кеш? Это связано с каждой функцией или это глобальная переменная? т. е. Будет ли два объекта кеша, если я использую @memoize для нескольких функций.

..

class memoize:
    def __init__(self):
        self.cache = {}

    def __call__(self, function):
        def wrapper(*args, **kwargs):
            key = str(function.__name__) + str(args) + str(kwargs)
            if key in cache:
                return cache[key]
            else:
                value = function(*args, **kwargs)
                cache[key] = value
                return value
        return wrapper

@memoize
def fib(n):
    if n in (0, 1):
        return 1
    else:
        return fib(n-1) + fib(n-2)

for i in range(0, 10):
    print(fib(i))

Я получаю ошибку компиляции.

Traceback (most recent call last):
  File "memoize.py", line 17, in <module>
    @memoize
TypeError: __init__() takes exactly 1 argument (2 given)

person user1159517    schedule 26.01.2015    source источник
comment
Вы действительно хотите написать это с нуля?   -  person Dair    schedule 26.01.2015
comment
Да. Я изучаю декораторы Python   -  person user1159517    schedule 26.01.2015
comment
Хорошо, просто перепроверил, так как есть декоратор lru_cache.   -  person Dair    schedule 26.01.2015


Ответы (1)


  1. Вы должны помнить, что @decorator — это просто синтаксический сахар для func = decorator(func). Итак, вот разница:

(1)

@decorator 
def func():
     ...

такой же, как

func = decorator(func)  # Just call of __init__
func(...)               # Call of decorator.__call__

но (2)

@decorator(some_param)
def func():
     ...

похож на

# Call of __init__ plus call of __call__
func = decorator(some_param)(func)  
# Call of closure returned by decorator.__call__
func(...)   
  1. Вы реализовали декоратор, принимающий аргументы для синтаксиса (2), но не предоставляете их при их использовании, как в примере (1). Поэтому __init__ жалуясь, он получает func в качестве второго аргумента.

  2. Вы должны написать self.cache в замыкании wrapper, чтобы wrapper ссылался на соответствующий объект decorator. Написание только cache приведет к поиску глобальной переменной и, следовательно, к ошибке.

UPD: я изменил ваш код на подход (1):

class memoize:
    def __init__(self, function):
        self.cache = {}
        self.function = function

    def __call__(self, *args, **kwargs):        
        key = str(args) + str(kwargs)
        if key in self.cache:
            return self.cache[key]

        value = self.function(*args, **kwargs)
        self.cache[key] = value
        return value

@memoize
def fib(n):
    if n in (0, 1):
        return 1
    else:
        return fib(n-1) + fib(n-2)

for i in range(0, 10):
    print(fib(i))

print(fib.cache)
person myaut    schedule 26.01.2015
comment
Если вы видите этот пример, кэш wiki.python.org/moin/PythonDecoratorLibrary#Memoize является переменной-членом класса memoize. Никаких оберток вокруг - person user1159517; 26.01.2015
comment
Я изменил ваш подход к (1) и опубликовал код, но предлагаю вам внимательно прочитать мое объяснение. - person myaut; 26.01.2015
comment
Хорошо. Как теперь добавить ttl? Я хотел бы использовать @memoize(ttl=10). Я попытался добавить параметр в функцию init в классе и снова получил ошибку - person user1159517; 26.01.2015
comment
Хе-хе, я ожидал, что ты спросишь об этом. Затем вам нужно изменить код с подхода (1) на (2). Используйте исходный код (но измените cache -> self.cache). Смотрите мой ответ на ваш второй вопрос. - person myaut; 26.01.2015
comment
Так вот тут я запутался. Тогда я должен помнить два типа синтаксиса декоратора. 1. Для memoize без указания каких-либо аргументов. 2. Для запоминания с ttl. - person user1159517; 26.01.2015
comment
Это чрезвычайно полезный декоратор Python для многих приложений. Спасибо, что поделился! - person sffc; 24.07.2015