Есть ли декоратор для простого кэширования возвращаемых значений функции?


158

Учтите следующее:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

Я новичок, но я думаю, что кэширование может быть преобразовано в декоратор. Только я такого не нашел;)

PS реальный расчет не зависит от изменчивых значений


Там может быть декоратор, который имеет такую ​​возможность, но вы еще не полностью указали, что вы хотите. Какой вид кэширования вы используете? И как будет указано значение? Из вашего кода я предполагаю, что вы действительно запрашиваете кэшированное свойство только для чтения.
Дэвид Бергер

Есть памятные декораторы, которые выполняют то, что вы называете «кешированием»; они, как правило, работают с функциями как таковыми (независимо от того, должны ли они стать методами или нет), результаты которых зависят от их аргументов (не от изменяемых вещей, таких как self! -), и поэтому хранят отдельный меморандум.
Алекс Мартелли

Ответы:


206

Начиная с Python 3.2 есть встроенный декоратор:

@functools.lru_cache(maxsize=100, typed=False)

Декоратор, чтобы обернуть функцию запоминающимся вызовом, который сохраняет до максимума самые последние вызовы. Это может сэкономить время, когда дорогая или связанная с вводом / выводом функция периодически вызывается с одинаковыми аргументами.

Пример кэша LRU для вычисления чисел Фибоначчи :

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> print([fib(n) for n in range(16)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> print(fib.cache_info())
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

Если вы застряли с Python 2.x, вот список других совместимых библиотек памятки:



Бэкпорт теперь можно найти здесь: pypi.python.org/pypi/backports.functools_lru_cache
Фредерик Норд

@gerrit в теории работает в общем случае с хешируемыми объектами - хотя некоторые хешируемые объекты равны, только если они являются одним и тем же объектом (например, пользовательские объекты без явной функции __hash __ ()).
Джонатан

1
@ Джонатан Это работает, но неправильно. Если я передаю изменяемый, изменяемый аргумент и изменяю значение объекта после первого вызова функции, второй вызов вернет измененный, а не исходный объект. Это почти наверняка не то, что хочет пользователь. Для того, чтобы он работал с изменяемыми аргументами, потребовалось lru_cacheбы сделать копию любого результата, который он кэширует, и в реализации такая копия не создается functools.lru_cache. Это также может создать проблемы с памятью, которые трудно обнаружить при использовании для кэширования большого объекта.
геррит

@gerrit Не могли бы вы продолжить здесь: stackoverflow.com/questions/44583381/… ? Я не совсем последовал вашему примеру.
Джонатан

28

Похоже, вы не запрашиваете универсальный декоратор памятки (т. Е. Вас не интересует общий случай, когда вы хотите кэшировать возвращаемые значения для разных значений аргумента). То есть вы хотели бы иметь это:

x = obj.name  # expensive
y = obj.name  # cheap

в то время как универсальный декоратор напоминания даст вам следующее:

x = obj.name()  # expensive
y = obj.name()  # cheap

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

[Обновление: декоратор памятки на основе классов, на который я ссылался и который цитировал здесь ранее, не работает для методов. Я заменил его функцией декоратора.] Если вы хотите использовать универсальный декоратор для запоминания, вот простой:

def memoize(function):
  memo = {}
  def wrapper(*args):
    if args in memo:
      return memo[args]
    else:
      rv = function(*args)
      memo[args] = rv
      return rv
  return wrapper

Пример использования:

@memoize
def fibonacci(n):
  if n < 2: return n
  return fibonacci(n - 1) + fibonacci(n - 2)

Другой декоратор памятки с ограничением размера кэша можно найти здесь .


Ни один из декораторов, упомянутых во всех ответах, не работает для методов! Вероятно, потому что они основаны на классе. Только одно я прошло? Другие работают нормально, но хранить значения в функциях довольно сложно.
Тобиас

2
Я думаю, что вы можете столкнуться с проблемой, если args не хэшируемый.
неизвестно

1
@ Неизвестно Да, первый декоратор, который я здесь цитировал, ограничен хэшируемыми типами. Тот, что в ActiveState (с ограничением размера кэша), собирает аргументы в (хешируемую) строку, которая, конечно, более дорогая, но более общая.
Натан Китчен

@vanity Спасибо за указание на ограничения декораторов на основе классов. Я пересмотрел свой ответ, чтобы показать функцию декоратора, которая работает для методов (на самом деле я тестировал эту).
Натан Китчен

1
@SiminJie Декоратор вызывается только один раз, и возвращаемая им упакованная функция та же самая, что используется для всех различных вызовов fibonacci. Эта функция всегда использует один и тот же memoсловарь.
Натан Китчен

22
class memorize(dict):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        result = self[key] = self.func(*key)
        return result

Пример использования:

>>> @memorize
... def foo(a, b):
...     return a * b
>>> foo(2, 4)
8
>>> foo
{(2, 4): 8}
>>> foo('hi', 3)
'hihihi'
>>> foo
{(2, 4): 8, ('hi', 3): 'hihihi'}

Странный! Как это работает? Это не похоже на другие декораторы, которые я видел.
PascalVKooten

1
Это решение возвращает TypeError, если используются аргументы с ключевыми словами, например, foo (3, b = 5)
kadee

1
Проблема решения заключается в том, что у него нет ограничения памяти. Что касается названных аргументов, вы можете просто добавить их в __call__ и __ пропущенных__ как ** nargs
Леонид Медников

16

Python 3.8 functools.cached_propertyдекоратор

https://docs.python.org/dev/library/functools.html#functools.cached_property

cached_propertyот Werkzeug упоминается по адресу: https://stackoverflow.com/a/5295190/895245, но предположительно производная версия будет объединена в 3.8, что является удивительным.

Этот декоратор может рассматриваться как кеширование @propertyили очиститель, @functools.lru_cacheкогда у вас нет никаких аргументов.

Документы говорят:

@functools.cached_property(func)

Преобразуйте метод класса в свойство, значение которого вычисляется один раз, а затем кэшируется как обычный атрибут в течение срока службы экземпляра. Аналогично свойству (), с добавлением кэширования. Полезно для дорогих вычисляемых свойств экземпляров, которые в противном случае эффективно неизменяемы.

Пример:

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

Новое в версии 3.8.

Примечание. Этот декоратор требует, чтобы атрибут dict в каждом экземпляре был изменяемым отображением. Это означает, что он не будет работать с некоторыми типами, такими как метаклассы (поскольку атрибуты dict в экземплярах типов являются прокси-серверами только для чтения для пространства имен класса), и те, которые задают слоты, не включая dict в качестве одного из определенных слотов (как такие классы не предоставлять атрибут dict вообще).


10

Werkzeug имеет cached_propertyдекоратор ( документы , источник )


Да. Это стоит отличить от общего случая запоминания, поскольку стандартное запоминание не работает, если класс не является хэшируемым.
Джеймсон Куинн


9

Я кодировал этот простой класс декоратора для кэширования ответов функций. Я нахожу это ОЧЕНЬ полезным для моих проектов:

from datetime import datetime, timedelta 

class cached(object):
    def __init__(self, *args, **kwargs):
        self.cached_function_responses = {}
        self.default_max_age = kwargs.get("default_cache_max_age", timedelta(seconds=0))

    def __call__(self, func):
        def inner(*args, **kwargs):
            max_age = kwargs.get('max_age', self.default_max_age)
            if not max_age or func not in self.cached_function_responses or (datetime.now() - self.cached_function_responses[func]['fetch_time'] > max_age):
                if 'max_age' in kwargs: del kwargs['max_age']
                res = func(*args, **kwargs)
                self.cached_function_responses[func] = {'data': res, 'fetch_time': datetime.now()}
            return self.cached_function_responses[func]['data']
        return inner

Использование просто:

import time

@cached
def myfunc(a):
    print "in func"
    return (a, datetime.now())

@cached(default_max_age = timedelta(seconds=6))
def cacheable_test(a):
    print "in cacheable test: "
    return (a, datetime.now())


print cacheable_test(1,max_age=timedelta(seconds=5))
print cacheable_test(2,max_age=timedelta(seconds=5))
time.sleep(7)
print cacheable_test(3,max_age=timedelta(seconds=5))

1
Ваша первая @cachedпропущенная скобка. Иначе он будет возвращать только cachedобъект на месте , myfuncи когда называют myfunc()то innerвсегда будет возвращено в качестве возвращаемого значения
Markus Meskanen

6

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Я автор Kids.cache .

Вы должны проверить kids.cache, он предоставляет @cacheдекоратор, который работает на Python 2 и Python 3. Никаких зависимостей, ~ 100 строк кода. Это очень просто использовать, например, с учетом вашего кода, вы можете использовать его следующим образом:

pip install kids.cache

затем

from kids.cache import cache
...
class MyClass(object):
    ...
    @cache            # <-- That's all you need to do
    @property
    def name(self):
        return 1 + 1  # supposedly expensive calculation

Или вы можете поставить @cacheдекоратор после @property(тот же результат).

Использование кеша для свойства называется отложенной оценкой и kids.cacheможет сделать гораздо больше (он работает с функцией с любыми аргументами, свойствами, любым типом методов и даже классами ...). Для продвинутых пользователей, kids.cacheподдержка, cachetoolsкоторая предоставляет модные хранилища кеша для python 2 и python 3 (LRU, LFU, TTL, RR cache).

ВАЖНОЕ ПРИМЕЧАНИЕ : хранилище кеша по умолчанию kids.cache- это стандартный dict, который не рекомендуется для долго работающей программы с разными запросами, поскольку это приведет к постоянно растущему хранилищу кеширования. Для этого вы можете подключить другие хранилища кеша, например, @cache(use=cachetools.LRUCache(maxsize=2))для украшения вашей функции / свойства / класса / метода ...)


Этот модуль, по-видимому, приводит к медленному времени импорта на python 2 ~ 0,9 с (см .: pastebin.com/raw/aA1ZBE9Z ). Я подозреваю, что это связано с этой строкой github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (см. Точки входа в setuptools). Я создаю проблему для этого.
Att Righ

Вот проблема для вышеупомянутого github.com/0k/kids.cache/issues/9 .
Att Righ

Это приведет к утечке памяти.
Тимоти Чжан

@vaab создать экземпляр cиз MyClass, и проверить его objgraph.show_backrefs([c], max_depth=10), есть ссылка цепь от объекта класса MyClassк c. То есть cникогда не будет выпущен, пока MyClassне будет выпущен.
Тимоти Чжан

@TimothyZhang, вы приглашены и можете добавить свои проблемы в github.com/0k/kids.cache/issues/10 . Stackoverflow - это не то место, где нужно правильно обсудить это. И необходимы дальнейшие разъяснения. Спасибо за ваш отзыв.
Вааб


4

Существует fastcache , который представляет собой «реализацию Python 3 на языке functools.lru_cache на C. Обеспечивает ускорение в 10–30 раз по сравнению со стандартной библиотекой».

То же, что выбранный ответ , только другой импорт:

from fastcache import lru_cache
@lru_cache(maxsize=128, typed=False)
def f(a, b):
    pass

Кроме того, он устанавливается в Anaconda , в отличие от functools, который необходимо установить .


1
functoolsявляется частью стандартной библиотеки, ссылка, которую вы разместили на случайную ветку git или что-то еще ...
cz


3

Если вы используете Django Framework, у него есть такое свойство для кэширования представления или ответа использования API, @cache_page(time)а также могут быть и другие параметры.

Пример:

@cache_page(60 * 15, cache="special_cache")
def my_view(request):
    ...

Более подробную информацию можно найти здесь .


2

Наряду с примером Memoize я нашел следующие пакеты Python:

  • кашпо ; Позволяет настроить ttl и \ или количество вызовов для кэшируемых функций; Также можно использовать зашифрованный файловый кеш ...
  • percache

1

Я реализовал что-то вроде этого, используя pickle для персистентности и используя sha1 для коротких, почти наверняка уникальных идентификаторов. По сути, кэш хэшировал код функции и историю аргументов для получения sha1, а затем искал файл с этим sha1 в имени. Если он существует, он открывает его и возвращает результат; в противном случае он вызывает функцию и сохраняет результат (при необходимости, сохранение только в том случае, если для его обработки потребовалось определенное время).

Тем не менее, я клянусь, я нашел существующий модуль, который сделал это, и попал сюда, пытаясь найти этот модуль ... Самое близкое, что я могу найти, это то, что выглядит примерно так: http: //chase-seibert.github. И.О. / блог / 2011/11/23 / pythondjango-диск на основе кэширования-decorator.html

Единственная проблема, с которой я сталкиваюсь, это то, что она не будет хорошо работать для больших входов, поскольку она хэширует str (arg), что не является уникальным для гигантских массивов.

Было бы хорошо, если бы существовал протокол unique_hash (), в котором класс возвращал бы безопасный хэш своего содержимого. Я в основном вручную реализовал это для типов, о которых я заботился.



1

Если вы используете Django и хотите кэшировать представления, см . Ответ Нихила Кумара .


Но если вы хотите кэшировать ЛЮБОЙ результат функции, вы можете использовать django-cache-utils .

Он использует кеши Django и предоставляет простой в использовании cachedдекоратор:

from cache_utils.decorators import cached

@cached(60)
def foo(x, y=0):
    print 'foo is called'
    return x+y

1

@lru_cache не идеален со значениями функций по умолчанию

мой memдекоратор:

import inspect


def get_default_args(f):
    signature = inspect.signature(f)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }


def full_kwargs(f, kwargs):
    res = dict(get_default_args(f))
    res.update(kwargs)
    return res


def mem(func):
    cache = dict()

    def wrapper(*args, **kwargs):
        kwargs = full_kwargs(func, kwargs)
        key = list(args)
        key.extend(kwargs.values())
        key = hash(tuple(key))
        if key in cache:
            return cache[key]
        else:
            res = func(*args, **kwargs)
            cache[key] = res
            return res
    return wrapper

и код для тестирования:

from time import sleep


@mem
def count(a, *x, z=10):
    sleep(2)
    x = list(x)
    x.append(z)
    x.append(a)
    return sum(x)


def main():
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5, z=6))
    print(count(1,2,3,4,5, z=6))
    print(count(1))
    print(count(1, z=10))


if __name__ == '__main__':
    main()

результат - только 3 раза со сном

но с @lru_cacheэтим будет 4 раза, потому что это:

print(count(1))
print(count(1, z=10))

будет рассчитан дважды (плохо работает со значениями по умолчанию)

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.