Decoratore proprietà Python memoising / ricerca differita


109

Recentemente ho esaminato una base di codice esistente contenente molte classi in cui gli attributi di istanza riflettono i valori memorizzati in un database. Ho riformattato molti di questi attributi per differire le ricerche nel database, ad es. non essere inizializzato nel costruttore ma solo alla prima lettura. Questi attributi non cambiano nel corso della durata dell'istanza, ma rappresentano un vero collo di bottiglia per calcolare quella prima volta e vi si accede realmente solo per casi speciali. Quindi possono anche essere memorizzati nella cache dopo che sono stati recuperati dal database (questo quindi si adatta alla definizione di memoisation in cui l'input è semplicemente "no input").

Mi ritrovo a digitare il seguente frammento di codice più e più volte per vari attributi in varie classi:

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

Esiste già un decoratore per eseguire questa operazione in Python di cui semplicemente non sono a conoscenza? Oppure esiste un modo ragionevolmente semplice per definire un decoratore che fa questo?

Sto lavorando con Python 2.5, ma le risposte 2.6 potrebbero comunque essere interessanti se sono significativamente diverse.

Nota

Questa domanda è stata posta prima che Python includesse molti decoratori già pronti per questo. L'ho aggiornato solo per correggere la terminologia.


Sto usando Python 2.7 e non vedo nulla sui decoratori già pronti per questo. Potete fornire un collegamento ai decoratori già pronti menzionati nella domanda?
Bamcclur

@Bamcclur mi dispiace, c'erano altri commenti che li dettagliavano, non so perché sono stati cancellati. L'unico che posso trovare in questo momento è un Python 3 uno: functools.lru_cache().
detly

Non sono sicuro che ci siano built-in (almeno Python 2.7), ma c'è la proprietà cache
guyarad

@guyarad Non ho visto questo commento fino ad ora. Questa è una libreria fantastica! Pubblicala come risposta così posso votarla positivamente.
detly

Risposte:


12

Per tutti i tipi di grandi utilità sto usando i boltons .

Come parte di quella libreria hai proprietà memorizzate nella 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)

124

Ecco un esempio di implementazione di un decoratore di proprietà pigro:

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)

Sessione interattiva:

>>> 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]

1
Qualcuno può consigliare un nome appropriato per la funzione interna? Sono così bravo a nominare le cose al mattino ...
Mike Boers

2
Di solito chiamo la funzione interna come la funzione esterna con un trattino basso precedente. Quindi "_lazyprop" - segue la filosofia del "solo uso interno" di pep 8.
Speso il

1
Funziona alla grande :) Non so perché non mi è mai venuto in mente di usare un decoratore anche su una funzione annidata come quella.
detly

4
dato il protocollo del descrittore non di dati, questo è molto più lento e meno elegante della risposta sotto usando__get__
Ronny

1
Suggerimento: inserisci un @wraps(fn)sotto @propertyper non perdere le stringhe dei documenti ecc. ( wrapsViene da functools)
letmaik

111

L'ho scritto per me stesso ... Da utilizzare per vere proprietà pigre calcolate una tantum . Mi piace perché evita di incollare attributi extra sugli oggetti e una volta attivato non perde tempo a controllare la presenza degli attributi, ecc .:

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

Nota: la lazy_propertyclasse è un descrittore non di dati , il che significa che è di sola lettura. L'aggiunta di un __set__metodo ne impedirebbe il corretto funzionamento.


9
Ci è voluto un po 'di tempo per capire, ma è una risposta assolutamente sbalorditiva. Mi piace come la funzione stessa viene sostituita dal valore che calcola.
Paul Etherton,

2
Per i posteri: altre versioni di questo sono state proposte in altre risposte da allora (rif. 1 e 2 ). Sembra che questo sia popolare nei framework web Python (esistono derivati ​​in Pyramid e Werkzeug).
André Caron

1
Grazie per aver notato che Werkzeug ha werkzeug.utils.cached_property: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira

3
Ho trovato questo metodo 7,6 volte più veloce della risposta selezionata. (2.45 µs / 322 ns) Vedi ipython notebook
Dave Butler,

1
NB: questo non impedisce l'assegnazione alla fgetvia @propertyfa. Per garantire l'immutabilità / idempotenza, è necessario aggiungere un __set__()metodo che solleva AttributeError('can\'t set attribute')(o qualunque eccezione / messaggio ti si addica, ma questo è ciò che propertysolleva). Questo sfortunatamente ha un impatto sulle prestazioni di una frazione di microsecondo perché __get__()verrà chiamato a ogni accesso piuttosto che estrarre il valore fget da dict al secondo e successivo accesso. Ne vale la pena, a mio parere, mantenere l'immutabilità / idempotenza, che è la chiave per i miei casi d'uso, ma YMMV.
scanny

4

Ecco un richiamabile che prende un argomento timeout facoltativo, nel __call__si potrebbe anche copiare il __name__, __doc__, __module__dal namespace di 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

3

propertyè una classe. Un descrittore per essere esatti. Basta derivarne e attuare il comportamento desiderato.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')

3

Quello che vuoi veramente è il decoratore reify(fonte collegata!) Di Pyramid:

Utilizzare come decoratore di metodi di classe. Funziona quasi esattamente come il @propertydecoratore Python , ma inserisce il risultato del metodo che decora nel dict di istanza dopo la prima chiamata, sostituendo efficacemente la funzione che decora con una variabile di istanza. È, nel gergo di Python, un descrittore non di dati. Quello che segue è un esempio e il suo utilizzo:

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

1
Bello, fa esattamente quello di cui avevo bisogno ... anche se Pyramid potrebbe essere una grande dipendenza per un decoratore:)
detly

@detly L'implementazione del decoratore è semplice e potresti implementarla da solo, senza bisogno della pyramiddipendenza.
Peter Wood,

Quindi il link dice "fonte collegata": D
Antti Haapala

@AnttiHaapala l'ho notato, ma ho pensato di evidenziare che è semplice da implementare per coloro che non seguono il link.
Peter Wood,

1

Finora c'è un miscuglio di termini e / o confusione di concetti sia in questione che nelle risposte.

Valutazione pigra significa semplicemente che qualcosa viene valutato in fase di esecuzione nell'ultimo momento possibile in cui è necessario un valore. Il @propertydecoratore standard fa proprio questo. (*) La funzione decorata viene valutata solo e ogni volta che serve il valore di quella proprietà. (vedi l'articolo di wikipedia sulla valutazione pigra)

(*) In realtà una vera valutazione pigra (confronta ad esempio haskell) è molto difficile da ottenere in python (e risulta in un codice tutt'altro che idiomatico).

Memoizzazione è il termine corretto per ciò che il richiedente sembra cercare. Le funzioni pure che non dipendono dagli effetti collaterali per la valutazione del valore di ritorno possono essere memorizzate in modo sicuro e in realtà esiste un decoratore in functools, @functools.lru_cache quindi non è necessario scrivere decoratori propri a meno che non sia necessario un comportamento specializzato.


Ho usato il termine "pigro" perché nell'implementazione originale, il membro era calcolato / recuperato da un DB al momento dell'inizializzazione dell'oggetto e desidero rimandare tale calcolo fino a quando la proprietà non è stata effettivamente utilizzata in un modello. Questo mi sembrava corrispondere alla definizione di pigrizia. Sono d'accordo che, poiché la mia domanda presuppone già una soluzione utilizzando @property, "pigro" non ha molto senso a quel punto. (Ho anche pensato alla memoisation come una mappa di input per output memorizzati nella cache, e poiché queste proprietà hanno un solo input, niente, una mappa sembrava più complessa del necessario.)
Detly

Si noti che tutti i decoratori che le persone hanno suggerito come soluzioni "fuori dagli schemi" non esistevano nemmeno quando ho chiesto questo.
detly

Sono d'accordo con Jason, questa è una domanda sulla memorizzazione nella cache / memoizzazione non sulla valutazione pigra.
poindexter

@poindexter - La memorizzazione nella cache non lo copre completamente ; non distingue la ricerca del valore al momento dell'inizializzazione dell'oggetto e la memorizzazione nella cache dalla ricerca del valore verso l'alto e la memorizzazione nella cache quando si accede alla proprietà (che è la caratteristica chiave qui). Come dovrei chiamarlo? Decoratore "Cache-after-first-use"?
detly

@detly Memoize. Dovresti chiamarlo Memoize. en.wikipedia.org/wiki/Memoization
poindexter

0

Puoi farlo facilmente e facilmente costruendo una classe dalla proprietà nativa di 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

Possiamo usare questa classe di proprietà come una normale proprietà di classe (supporta anche l'assegnazione di oggetti come puoi vedere)

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)

Valore calcolato solo la prima volta, dopodiché abbiamo utilizzato il nostro valore salvato

Produzione:

I am calculating value
My calculated value
My calculated value
2
2
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.