Чи є декоратор, щоб просто кешувати функцію повернення значень?


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, ось список інших сумісних бібліотек пам’яті:




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

1
@Jonathan Це працює, але неправильно. Якщо я передаю хешируемый, змінний аргумент і змінюю значення об'єкта після першого виклику функції, другий виклик поверне змінений, а не оригінальний об'єкт. Це майже точно не те, що хоче користувач. Щоб вона працювала на змінних аргументах, потрібно було б 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
Я думаю, що ви можете зіткнутися з проблемою, якщо аргументи не є хешируемими.
Невідомо

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

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

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, завжди буде повернуто як значення повернення
Маркус Месканен

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 кеш).

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


Здається, цей модуль призводить до уповільнення часу імпорту на python 2 ~ 0,9s (див.: Pastebin.com/raw/aA1ZBE9Z ). Я підозрюю, що це пов’язано з цим рядком github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (cf пункти входу setuptools). Я створюю проблему для цього.
Att Righ

Ось проблема для вищевказаного github.com/0k/kids.cache/isissue/9 .
Att Righ

Це призведе до витоку пам'яті.
Тімоті Чжан

@vaab створити екземпляр cз MyClass, і перевірити його objgraph.show_backrefs([c], max_depth=10), є посилання ланцюг від об'єкта класу MyClassдо c. Тобто, cніколи не буде звільнений, поки не MyClassбуде звільнений.
Тимофій Чжан

@TimothyZhang вас запрошують і висловлюєте свої занепокоєння в github.com/0k/kids.cache/isissue/10 . Stackoverflow не є правильним місцем для належного обговорення цього питання. І потрібні подальші роз’яснення. Спасибі за ваш відгук.
вааб


4

Існує швидкий кеш , який є "C реалізацією Python 3 functools.lru_cache. Забезпечує швидкість 10-30x над стандартною бібліотекою."

Те саме, що обрана відповідь , просто інший імпорт:

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

Поряд із Прикладом пам'яті я знайшов такі пакети python:

  • кешпі ; Це дозволяє встановити ttl і \ або кількість викликів для кешованих функцій; Також можна використовувати зашифрований кешований файловий кеш ...
  • кашлюк

1

Я реалізував щось подібне, використовуючи соління для стійкості та використовуючи sha1 для коротких майже напевно унікальних ідентифікаторів. В основному кеш хеширував код функції та історію аргументів, щоб отримати sha1, тоді шукав файл з цим sha1 в імені. Якщо він існував, він відкрив його і повернув результат; якщо ні, то він викликає функцію та зберігає результат (за бажанням, лише якщо зберегти певну кількість часу).

Це сказало, я клянусь, що знайшов існуючий модуль, який це зробив, і опинився тут, намагаючись знайти цей модуль ... Найближчим, що я можу знайти, це такий, який виглядає правильно: http: //chase-seibert.github. io / blog / 2011/11/23 / pythondjango-disk-based-caching-decorator.html

Єдина проблема, яку я бачу в цьому, це те, що він не буде добре працювати з великими входами, оскільки він хеш str (arg), який не є унікальним для гігантських масивів.

Було б добре, якби існував унікальний протокол__sh (), який мав клас повернути захищений хеш його вмісту. Я в основному вручну реалізував це для тих типів, про які я піклувався.



1

Якщо ви використовуєте Джанго і хочете кешувати перегляди, дивіться відповідь Нікіла Кумара .


Але якщо ви хочете кешувати будь-які результати функцій, ви можете використовувати 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.