Python memoising / decorador de propriedade de pesquisa adiada

109

Recentemente, examinei uma base de código existente contendo muitas classes em que os atributos de instância refletem os valores armazenados em um banco de dados. Refatorei muitos desses atributos para que suas pesquisas de banco de dados fossem adiadas, ou seja, não ser inicializado no construtor, mas apenas na primeira leitura. Esses atributos não mudam durante o tempo de vida da instância, mas são um verdadeiro gargalo para calcular aquela primeira vez e só são realmente acessados ​​para casos especiais. Portanto, eles também podem ser armazenados em cache depois de serem recuperados do banco de dados (isso, portanto, se encaixa na definição de memoisation, onde a entrada é simplesmente "sem entrada").

Eu me pego digitando o seguinte snippet de código repetidamente para vários atributos em várias classes:

class testA(object):

  def __init__(self):
    self._a = None
    self._b = None

  @property
  def a(self):
    if self._a is None:
      # Calculate the attribute now
      self._a = 7
    return self._a

  @property
  def b(self):
    #etc

Já existe um decorador para fazer isso em Python que simplesmente não conheço? Ou existe uma maneira razoavelmente simples de definir um decorador que faça isso?

Estou trabalhando em Python 2.5, mas as respostas 2.6 ainda podem ser interessantes se forem significativamente diferentes.

Nota

Essa pergunta foi feita antes que o Python incluísse muitos decoradores prontos para isso. Eu atualizei apenas para corrigir a terminologia.

detidamente
fonte
Estou usando o Python 2.7 e não vejo nada sobre decoradores prontos para isso. Você pode fornecer um link para os decoradores prontos mencionados na pergunta?
Bamcclur
@Bamcclur desculpe, costumava haver outros comentários detalhando-os, não sei por que eles foram excluídos. O único que posso encontrar agora é um Python 3 um: functools.lru_cache().
detly
Não tenho certeza se há recursos integrados (pelo menos Python 2.7), mas há a propriedade
guyarad
@guyarad Eu não vi esse comentário até agora. Essa é uma biblioteca fantástica! Poste isso como uma resposta para que eu possa votar a favor.
detly

Respostas:

12

Para todos os tipos de grandes utilitários, estou usando boltons .

Como parte dessa biblioteca, você armazenou propriedade em cache :

from boltons.cacheutils import cachedproperty

class Foo(object):
    def __init__(self):
        self.value = 4

    @cachedproperty
    def cached_prop(self):
        self.value += 1
        return self.value


f = Foo()
print(f.value)  # initial value
print(f.cached_prop)  # cached property is calculated
f.value = 1
print(f.cached_prop)  # same value for the cached property - it isn't calculated again
print(f.value)  # the backing value is different (it's essentially unrelated value)
Guyarad
fonte
124

Aqui está um exemplo de implementação de um decorador de propriedade preguiçoso:

import functools

def lazyprop(fn):
    attr_name = '_lazy_' + fn.__name__

    @property
    @functools.wraps(fn)
    def _lazyprop(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fn(self))
        return getattr(self, attr_name)

    return _lazyprop


class Test(object):

    @lazyprop
    def a(self):
        print 'generating "a"'
        return range(5)

Sessão interativa:

>>> t = Test()
>>> t.__dict__
{}
>>> t.a
generating "a"
[0, 1, 2, 3, 4]
>>> t.__dict__
{'_lazy_a': [0, 1, 2, 3, 4]}
>>> t.a
[0, 1, 2, 3, 4]
Mike Boers
fonte
1
Alguém pode recomendar um nome apropriado para a função interna? Eu sou tão ruim em nomear coisas de manhã ...
Mike Boers
2
Eu geralmente nomeio a função interna da mesma forma que a função externa com um sublinhado precedente. Portanto, "_lazyprop" - segue a filosofia de "uso interno apenas" do pep 8.
passouhil
1
Isso funciona muito bem :) Não sei por que nunca me ocorreu usar um decorador em uma função aninhada como essa também.
detly
4
dado o protocolo do descritor sem dados, este é muito mais lento e menos elegante do que a resposta abaixo usando__get__
Ronny
1
Dica: coloque um @wraps(fn)abaixo @propertypara não perder suas strings de doc etc. ( wrapsvem de functools)
letmaik
111

Eu escrevi este para mim mesmo ... Para ser usado para propriedades preguiçosas verdadeiras calculadas de uma só vez . Gosto porque evita colar atributos extras nos objetos e, uma vez ativado, não perde tempo verificando a presença de atributos, etc .:

import functools

class lazy_property(object):
    '''
    meant to be used for lazy evaluation of an object attribute.
    property should represent non-mutable data, as it replaces itself.
    '''

    def __init__(self, fget):
        self.fget = fget

        # copy the getter function's docstring and other attributes
        functools.update_wrapper(self, fget)

    def __get__(self, obj, cls):
        if obj is None:
            return self

        value = self.fget(obj)
        setattr(obj, self.fget.__name__, value)
        return value


class Test(object):

    @lazy_property
    def results(self):
        calcs = 1  # Do a lot of calculation here
        return calcs

Observação: a lazy_propertyclasse é um descritor sem dados , o que significa que é somente leitura. Adicionar um __set__método impede que ele funcione corretamente.

Ciclone
fonte
9
Demorou um pouco para entender, mas é uma resposta absolutamente impressionante. Gosto de como a própria função é substituída pelo valor que ela calcula.
Paul Etherton
2
Para a posteridade: outras versões deste foram propostas em outras respostas desde (ref 1 e 2 ). Parece que este é um popular em estruturas da web Python (existem derivados em Pyramid e Werkzeug).
André Caron
1
Obrigado por notar que Werkzeug tem werkzeug.utils.cached_property: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira
3
Eu descobri que esse método é 7,6 vezes mais rápido do que a resposta selecionada. (2,45 µs / 322 ns) Ver notebook ipython
Dave Butler
1
NB: isso não impede a atribuição à fgetforma como o @propertyfaz. Para garantir a imutabilidade / idempotência, você precisa adicionar um __set__()método que levanta AttributeError('can\'t set attribute')(ou qualquer exceção / mensagem adequada a você, mas é isso que propertyaumenta). Infelizmente, isso vem com um impacto de desempenho de uma fração de microssegundo, porque __get__()será chamado em cada acesso, em vez de extrair o valor de fget do dict no segundo acesso e subseqüentes. Vale a pena na minha opinião manter a imutabilidade / idempotência, que é fundamental para meus casos de uso, mas YMMV.
scanny
4

Aqui está uma função que recebe um argumento de tempo limite opcional, na __call__você também pode copiar o __name__, __doc__, __module__de namespace de func:

import time

class Lazyproperty(object):

    def __init__(self, timeout=None):
        self.timeout = timeout
        self._cache = {}

    def __call__(self, func):
        self.func = func
        return self

    def __get__(self, obj, objcls):
        if obj not in self._cache or \
          (self.timeout and time.time() - self._cache[key][1] > self.timeout):
            self._cache[obj] = (self.func(obj), time.time())
        return self._cache[obj]

ex:

class Foo(object):

    @Lazyproperty(10)
    def bar(self):
        print('calculating')
        return 'bar'

>>> x = Foo()
>>> print(x.bar)
calculating
bar
>>> print(x.bar)
bar
...(waiting 10 seconds)...
>>> print(x.bar)
calculating
bar
gnr
fonte
3

propertyé uma classe. Um descritor para ser exato. Basta derivar dele e implementar o comportamento desejado.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')
Ignacio Vazquez-Abrams
fonte
3

O que você realmente quer é o decorador reify(vinculado à fonte!) Da Pyramid:

Use como um decorador de método de classe. Ele opera quase exatamente como o @propertydecorador Python , mas coloca o resultado do método que ele decora no dict da instância após a primeira chamada, substituindo efetivamente a função que ele decora por uma variável de instância. É, no jargão do Python, um descritor sem dados. A seguir está um exemplo e seu uso:

>>> from pyramid.decorator import reify

>>> class Foo(object):
...     @reify
...     def jammy(self):
...         print('jammy called')
...         return 1

>>> f = Foo()
>>> v = f.jammy
jammy called
>>> print(v)
1
>>> f.jammy
1
>>> # jammy func not called the second time; it replaced itself with 1
>>> # Note: reassignment is possible
>>> f.jammy = 2
>>> f.jammy
2
Antti Haapala
fonte
1
Legal, faz exatamente o que eu precisava ... embora Pyramid possa ser uma grande dependência para um decorador:)
aproximadamente
@detly A implementação do decorador é simples e você mesmo pode implementá-la, sem a necessidade de pyramiddependência.
Peter Wood
Portanto, o link diz "fonte vinculada": D
Antti Haapala
@AnttiHaapala Notei, mas achei melhor destacar que é simples de implementar para quem não segue o link.
Peter Wood,
1

Até o momento, há uma mistura de termos e / ou confusão de conceitos em questão e nas respostas.

A avaliação lenta significa apenas que algo é avaliado em tempo de execução no último momento possível quando um valor é necessário. O padrão@property decorador faz exatamente isso.(*) A função decorada é avaliada apenas e toda vez que você precisa do valor daquela propriedade. (veja o artigo da wikipedia sobre avaliação preguiçosa)

(*) Na verdade, uma avaliação verdadeiramente preguiçosa (compare, por exemplo, haskell) é muito difícil de ser obtida em python (e resulta em um código que está longe de ser idiomático).

Memoização é o termo correto para o que quem pergunta parece estar procurando. Funções puras que não dependem de efeitos colaterais para avaliação de valor de retorno podem ser memoized com segurança e há na verdade um decorador em functools, @functools.lru_cache portanto, não há necessidade de escrever seus próprios decoradores, a menos que você precise de um comportamento especializado.

Jason Herbburn
fonte
Usei o termo "preguiçoso" porque na implementação original, o membro foi calculado / recuperado de um banco de dados no momento da inicialização do objeto e quero adiar esse cálculo até que a propriedade seja realmente usada em um modelo. Isso me pareceu corresponder à definição de preguiça. Concordo que, como minha pergunta já pressupõe uma solução usando @property, "preguiçoso" não faz muito sentido nesse ponto. (Eu também pensei no memoisation como um mapa de entradas para saídas em cache e, como essas propriedades têm apenas uma entrada, nada, um mapa parecia mais complexo do que o necessário.)
aproximadamente
Observe que todos os decoradores que as pessoas sugeriram como soluções "fora da caixa" também não existiam quando eu perguntei isso.
detly
Eu concordo com Jason, esta é uma questão sobre cache / memoização e não avaliação preguiçosa.
poindexter
@poindexter - Caching não chega cobri-lo; ele não distingue procurar o valor no tempo de inicialização do objeto e armazená-lo em cache de procurar o valor e armazená-lo em cache quando a propriedade é acessada (que é o principal recurso aqui). Como devo chamá-lo? Decorador "Cache-after-first-use"?
detly
@detly Memoize. Você deveria chamá-lo de Memoize. en.wikipedia.org/wiki/Memoization
poindexter
0

Você pode fazer isso de maneira fácil e agradável criando uma classe a partir da propriedade nativa do Python:

class cached_property(property):
    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__, None)
        if value is None:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value

Podemos usar esta classe de propriedade como uma propriedade de classe regular (também é compatível com a atribuição de item, como você pode ver)

class SampleClass():
    @cached_property
    def cached_property(self):
        print('I am calculating value')
        return 'My calculated value'


c = SampleClass()
print(c.cached_property)
print(c.cached_property)
c.cached_property = 2
print(c.cached_property)
print(c.cached_property)

Valor calculado apenas na primeira vez e depois disso usamos nosso valor salvo

Resultado:

I am calculating value
My calculated value
My calculated value
2
2
Rezakamalifard
fonte