Existe um decorador para simplesmente armazenar em cache os valores de retorno da função?


157

Considere o seguinte:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

Sou novo, mas acho que o armazenamento em cache pode ser fatorado em um decorador. Só que eu não encontrei um igual;)

PS o cálculo real não depende de valores mutáveis


Pode haver um decorador por aí que tenha alguma capacidade como essa, mas você não especificou completamente o que deseja. Que tipo de back-end de cache você está usando? E como o valor será digitado? Estou assumindo pelo seu código que o que você realmente está pedindo é uma propriedade somente leitura em cache.
David Berger

Existem decoradores de memorização que executam o que você chama de "cache"; eles normalmente trabalham em funções como tais (quer se tornem métodos ou não) cujos resultados dependem de seus argumentos (não de coisas mutáveis, como eu! -) e, portanto, mantêm um memorando separado.
Alex Martelli

Respostas:


206

A partir do Python 3.2, existe um decorador embutido:

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

Decorador para agrupar uma função com uma chamada de memorização que economize até as chamadas mais recentes de tamanho máximo. Isso pode economizar tempo quando uma função cara ou ligada à E / S é chamada periodicamente com os mesmos argumentos.

Exemplo de um cache LRU para calcular números de Fibonacci :

@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)

Se você está preso ao Python 2.x, aqui está uma lista de outras bibliotecas de memoização compatíveis:



o backport agora pode ser encontrado aqui: pypi.python.org/pypi/backports.functools_lru_cache
Frederick Nord

@gerrit, em teoria, funciona para objetos hashable em geral - embora alguns objetos hashable sejam iguais apenas se forem o mesmo objeto (como objetos definidos pelo usuário sem uma função __hash __ () explícita).
Jonathan

1
@ Jonathan Ele funciona, mas de forma errada. Se eu passar um argumento que pode ser lavado e mutável e alterar o valor do objeto após a primeira chamada da função, a segunda chamada retornará o objeto alterado, e não o original. Isso quase certamente não é o que o usuário deseja. Para que funcione com argumentos mutáveis, é necessário lru_cachefazer uma cópia de qualquer resultado em cache, e nenhuma cópia está sendo feita na functools.lru_cacheimplementação. Isso também correria o risco de criar problemas de memória difíceis de encontrar quando usados ​​para armazenar em cache um objeto grande.
gerrit

@gerrit Você gostaria de acompanhar aqui: stackoverflow.com/questions/44583381/… ? Não segui inteiramente o seu exemplo.
Jonathan

28

Parece que você não está solicitando um decorador de memorização de uso geral (ou seja, não está interessado no caso geral em que deseja armazenar em cache valores de retorno para diferentes valores de argumento). Ou seja, você gostaria de ter o seguinte:

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

enquanto um decorador de memorização de uso geral daria a você o seguinte:

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

Eu afirmo que a sintaxe da chamada de método tem um estilo melhor, porque sugere a possibilidade de computação cara, enquanto a sintaxe da propriedade sugere uma pesquisa rápida.

[Atualização: o decorador de memorização baseado em classe ao qual vinculei e citei aqui anteriormente não funciona para métodos. Substituí-lo por uma função decoradora.] Se você deseja usar um decorador de memorização de uso geral, aqui está uma simples:

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

Exemplo de uso:

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

Outro decorador de memorização com um limite no tamanho do cache pode ser encontrado aqui .


Nenhum dos decoradores mencionados em todas as respostas funciona para métodos! Provavelmente porque são baseados em classes. Apenas um eu é passado? Outros funcionam bem, mas é essencial armazenar valores em funções.
211 Tobias

2
Eu acho que você pode ter um problema se args não for lavável.
Desconhecido

1
@Desconhecido Sim, o primeiro decorador que citei aqui é limitado a tipos laváveis. O do ActiveState (com o limite de tamanho do cache) seleciona os argumentos em uma string (lavável), que é obviamente mais cara, mas mais geral.
Nathan Kitchen

@vanity Obrigado por apontar as limitações dos decoradores baseados em classe. Revisei minha resposta para mostrar uma função decoradora, que funciona para métodos (na verdade, testei essa).
Nathan Kitchen #

1
@SiminJie O decorador é chamado apenas uma vez e a função agrupada que ele retorna é a mesma usada para todas as chamadas diferentes fibonacci. Essa função sempre usa o mesmo memodicionário.
Nathan Kitchen

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

Usos da amostra:

>>> @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'}

Estranho! Como é que isso funciona? Não parece com outros decoradores que eu já vi.
PascalVKooten

1
Esta solução retorna um TypeError Se se utilizarem palavras-chave argumentos, por exemplo foo (3, b = 5)
KaDee

1
O problema da solução é que ela não tem limite de memória. Quanto aos argumentos nomeados, você pode simplesmente adicioná-los à __ call__ e __ missing__ como nargs **
Leonid Mednikov

16

functools.cached_propertyDecorador Python 3.8

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

cached_propertydo Werkzeug foi mencionado em: https://stackoverflow.com/a/5295190/895245, mas uma versão supostamente derivada será mesclada no 3.8, o que é incrível.

Esse decorador pode ser visto como armazenamento em cache @propertyou como um limpador @functools.lru_cachepara quando você não tiver argumentos.

Os documentos dizem:

@functools.cached_property(func)

Transforme um método de uma classe em uma propriedade cujo valor é calculado uma vez e, em seguida, armazenado em cache como um atributo normal durante a vida da instância. Semelhante à propriedade (), com a adição de armazenamento em cache. Útil para propriedades computadas caras de instâncias que são efetivamente imutáveis.

Exemplo:

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)

Novo na versão 3.8.

Nota Este decorador requer que o atributo dict em cada instância seja um mapeamento mutável. Isso significa que ele não funcionará com alguns tipos, como metaclasses (já que os atributos dict nas instâncias de tipo são proxies somente leitura para o espaço de nomes da classe) e aqueles que especificam slots sem incluir o dict como um dos slots definidos (como essas classes não forneça um atributo dict ).



9

Eu codifiquei essa classe simples de decorador para armazenar em cache as respostas das funções. Acho MUITO útil para meus projetos:

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

O uso é direto:

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
Seu primeiro @cachedestá faltando parênteses. Então ele só irá retornar o cachedobjeto no lugar do myfunce quando chamado como myfunc(), em seguida, innerserá sempre retornado como um valor de retorno
Markus Meskanen

6

AVISO LEGAL: Eu sou o autor de kids.cache .

Você deve verificar kids.cache, ele fornece um @cachedecorador que funciona no python 2 e python 3. Sem dependências, ~ 100 linhas de código. É muito simples de usar, por exemplo, com seu código em mente, você pode usá-lo assim:

pip install kids.cache

Então

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

Ou você pode colocar o @cachedecorador após o @property(mesmo resultado).

O uso de cache em uma propriedade é chamado de avaliação lenta , kids.cachepode fazer muito mais (funciona na função com argumentos, propriedades, qualquer tipo de método e até classe ...). Para usuários avançados, kids.cachesuporte cachetoolsque fornece armazenamentos de cache sofisticados para python 2 e python 3 (cache LRU, LFU, TTL, RR).

NOTA IMPORTANTE : o armazenamento em cache padrão de kids.cacheé um ditado padrão, que não é recomendado para programas de longa duração com consultas sempre diferentes, pois isso levaria a um armazenamento em cache cada vez maior. Para esse uso, você pode conectar outros repositórios de cache usando, por exemplo ( @cache(use=cachetools.LRUCache(maxsize=2))para decorar sua função / propriedade / classe / método ...)


Este módulo parece resultar em um tempo de importação lento no python 2 ~ 0.9s (consulte: pastebin.com/raw/aA1ZBE9Z ). Eu suspeito que isso se deve a esta linha github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (cf pontos de entrada de setuptools). Estou criando um problema para isso.
Att Righ

Aqui está um problema para o github.com/0k/kids.cache/issues/9 acima .
precisa

Isso levaria ao vazamento de memória.
Timothy Zhang

@vaab criar uma instância cde MyClass, e inspeccioná-la com objgraph.show_backrefs([c], max_depth=10), há uma cadeia de ref a partir do objecto de classe MyClassa c. Ou seja, cnunca seria lançado até o MyClasslançamento.
Timothy Zhang

@TimothyZhang, você está convidado e pode adicionar suas preocupações em github.com/0k/kids.cache/issues/10 . O Stackoverflow não é o lugar certo para uma discussão adequada sobre isso. E mais esclarecimentos são necessários. Obrigado pelo seu feedback.
vaab


4

Existe o fastcache , que é "A implementação em C do Python 3 functools.lru_cache. Fornece uma aceleração de 10 a 30x na biblioteca padrão".

O mesmo que a resposta escolhida , apenas importação diferente:

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

Além disso, ele é instalado no Anaconda , ao contrário das funções que precisam ser instaladas .


1
functoolsfaz parte da biblioteca padrão, o link que você postou é um garfo git aleatório ou algo mais ...
cz


3

Se você estiver usando o Django Framework, ele possui essa propriedade para armazenar em cache uma visualização ou resposta do uso da API @cache_page(time)e também pode haver outras opções.

Exemplo:

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

Mais detalhes podem ser encontrados aqui .


2

Junto com o exemplo Memoize , encontrei os seguintes pacotes python:

  • cachepy ; Permite configurar ttl e \ ou o número de chamadas para funções em cache; Além disso, pode-se usar cache baseado em arquivo criptografado ...
  • percache

1

Eu implementei algo assim, usando pickle para persistência e sha1 para identificações curtas, quase certamente únicas. Basicamente, o cache hash o código da função e o histórico dos argumentos para obter um sha1 e, em seguida, procurou um arquivo com esse sha1 no nome. Se existia, abriu e retornou o resultado; caso contrário, ele chama a função e salva o resultado (opcionalmente, salvando apenas se levar um certo tempo para processar).

Dito isso, eu juro que encontrei um módulo existente que fez isso e me encontro aqui tentando encontrá-lo ... O mais próximo que posso encontrar é esse, que parece certo: http: //chase-seibert.github. io / blog / 2011/11/23 / pythondjango-disk-based-caching-decorator.html

O único problema que vejo com isso é que não funcionaria bem para entradas grandes, pois hashes str (arg), que não é exclusivo para matrizes gigantes.

Seria bom se houvesse um protocolo unique_hash () que tivesse uma classe retornando um hash seguro de seu conteúdo. Basicamente, implementei isso manualmente para os tipos que me interessavam.



1

Se você estiver usando o Django e quiser armazenar em cache as visualizações, consulte a resposta de Nikhil Kumar .


Mas se você deseja armazenar em cache QUALQUER resultado de função, você pode usar django-cache-utils .

Ele reutiliza os caches do Django e fornece um cacheddecorador fácil de usar :

from cache_utils.decorators import cached

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

1

@lru_cache não é perfeito com valores de função padrão

meu memdecorador:

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

e código para teste:

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()

resultado - apenas 3 vezes com sono

mas com @lru_cacheisso será 4 vezes, porque isso:

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

será calculado duas vezes (mau trabalho com padrões)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.