Python Memoising / Deferred Lookup Property Decorator

109

Kürzlich habe ich eine vorhandene Codebasis durchlaufen, die viele Klassen enthält, in denen Instanzattribute in einer Datenbank gespeicherte Werte widerspiegeln. Ich habe viele dieser Attribute überarbeitet, damit ihre Datenbanksuchen verschoben werden, d. H. nicht im Konstruktor initialisiert werden, sondern erst beim ersten Lesen. Diese Attribute ändern sich nicht über die Lebensdauer der Instanz, stellen jedoch einen echten Engpass bei der Berechnung des ersten Males dar und werden nur in besonderen Fällen wirklich aufgerufen. Daher können sie auch zwischengespeichert werden, nachdem sie aus der Datenbank abgerufen wurden (dies entspricht daher der Definition der Memoisierung, bei der die Eingabe einfach "keine Eingabe" ist).

Ich tippe immer wieder den folgenden Codeausschnitt für verschiedene Attribute in verschiedenen Klassen ein:

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

Gibt es in Python bereits einen Dekorateur, der dies einfach nicht kennt? Oder gibt es eine einigermaßen einfache Möglichkeit, einen Dekorateur zu definieren, der dies tut?

Ich arbeite unter Python 2.5, aber 2.6-Antworten könnten immer noch interessant sein, wenn sie sich erheblich unterscheiden.

Hinweis

Diese Frage wurde gestellt, bevor Python viele fertige Dekorateure dafür einbezog. Ich habe es nur aktualisiert, um die Terminologie zu korrigieren.

detly
quelle
Ich verwende Python 2.7 und sehe dafür nichts über vorgefertigte Dekorateure. Können Sie einen Link zu den fertigen Dekorateuren bereitstellen, die in der Frage erwähnt werden?
Bamcclur
@Bamcclur Entschuldigung, es gab andere Kommentare, die sie detailliert beschreiben, nicht sicher, warum sie gelöscht wurden. Das einzige, was ich momentan finden kann, ist ein Python 3 : functools.lru_cache().
Detly
Ich bin
mir
@guyarad Ich habe diesen Kommentar bis jetzt nicht gesehen. Das ist eine fantastische Bibliothek! Poste das als Antwort, damit ich es positiv bewerten kann.
Detly

Antworten:

12

Für alle Arten von großartigen Dienstprogrammen verwende ich Boltons .

Als Teil dieser Bibliothek haben Sie die Eigenschaft zwischengespeichert :

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

Hier ist eine Beispielimplementierung eines faulen Immobiliendekorateurs:

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)

Interaktive Sitzung:

>>> 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
quelle
1
Kann jemand einen passenden Namen für die innere Funktion empfehlen? Ich bin so schlecht darin, Dinge am Morgen zu benennen ...
Mike Boers
2
Normalerweise benenne ich die innere Funktion genauso wie die äußere Funktion mit einem vorangestellten Unterstrich. Also "_lazyprop" - folgt der "nur für den internen Gebrauch" -Philosophie von Pep 8.
verbrachte den
1
Das funktioniert super :) Ich weiß nicht, warum es mir nie in den Sinn gekommen ist, einen Dekorator auch für eine solche verschachtelte Funktion zu verwenden.
Detly
4
In Anbetracht des Nicht-Daten-Deskriptor-Protokolls ist dieses viel langsamer und weniger elegant als die folgende Antwort mit__get__
Ronny
1
Tipp: @wraps(fn)@propertywrapsfunctools
Geben
111

Ich habe dieses für mich selbst geschrieben ... Für echte einmal berechnete faule Eigenschaften. Ich mag es, weil es vermeidet, zusätzliche Attribute auf Objekte zu kleben, und wenn es einmal aktiviert ist, verschwendet es keine Zeit damit, nach Attributpräsenz zu suchen usw.:

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

Hinweis: Die lazy_propertyKlasse ist ein Nicht-Daten-Deskriptor , dh sie ist schreibgeschützt. Das Hinzufügen einer __set__Methode würde verhindern, dass sie ordnungsgemäß funktioniert.

Zyklon
quelle
9
Das Verständnis hat eine Weile gedauert, ist aber eine absolut erstaunliche Antwort. Mir gefällt, wie die Funktion selbst durch den berechneten Wert ersetzt wird.
Paul Etherton
2
Für die Nachwelt: Andere Versionen davon wurden seitdem in anderen Antworten vorgeschlagen (Lit. 1 und 2 ). Dies scheint in Python-Webframeworks sehr beliebt zu sein (Ableitungen gibt es in Pyramid und Werkzeug).
André Caron
1
Vielen Dank für die Feststellung, dass Werkzeug werkzeug.utils.cached_property hat: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira
3
Ich fand diese Methode 7,6-mal schneller als die ausgewählte Antwort. (2,45 µs / 322 ns) Siehe Ipython-Notizbuch
Dave Butler
1
NB: Dies verhindert nicht die Zuordnung zum fgetWeg @property. Um Unveränderlichkeit / Idempotenz sicherzustellen, müssen Sie eine __set__()Methode hinzufügen , die ausgelöst wird AttributeError('can\'t set attribute')(oder welche Ausnahme / Nachricht auch immer zu Ihnen passt, aber dies ist es, was propertyausgelöst wird). Dies kommt leider mit einer Auswirkung auf die Leistung von einem Bruchteil einer Mikrosekunde , da __get__()wird bei jedem Zugriff aufgerufen werden , anstatt ziehen fget Wert von dict auf den zweiten und den späteren Zugriff. Es lohnt sich meiner Meinung nach, die Unveränderlichkeit / Idempotenz aufrechtzuerhalten, was für meine Anwendungsfälle von entscheidender Bedeutung ist, aber YMMV.
Scanny
4

Hier ist eine aufrufbare , die ein optionales Timeout - Argument, in der __call__Sie auch die Kopie über konnten __name__, __doc__, __module__von func Namespace:

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

propertyist eine Klasse. Ein Deskriptor um genau zu sein. Leiten Sie einfach daraus ab und implementieren Sie das gewünschte Verhalten.

class lazyproperty(property):
   ....

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

Was Sie wirklich wollen, ist der reify(Quelle verknüpft!) Dekorateur von Pyramid:

Verwendung als Dekorator für Klassenmethoden. Es funktioniert fast genau wie der Python- @propertyDekorator, fügt jedoch das Ergebnis der Methode, die es dekoriert, nach dem ersten Aufruf in das Instanz-Diktat ein und ersetzt die von ihm dekorierte Funktion effektiv durch eine Instanzvariable. Im Python-Sprachgebrauch handelt es sich um einen Nicht-Daten-Deskriptor. Das Folgende ist ein Beispiel und seine Verwendung:

>>> 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
quelle
1
Schön, macht genau das, was ich brauchte ... obwohl Pyramide eine große Abhängigkeit für einen Dekorateur sein könnte:)
am
@detly Die Decorator-Implementierung ist einfach und Sie können sie selbst implementieren, ohne dass die pyramidAbhängigkeit erforderlich ist.
Peter Wood
Daher sagt der Link "Quelle verknüpft": D
Antti Haapala
@AnttiHaapala Ich habe es bemerkt, dachte aber, ich möchte hervorheben, dass es einfach zu implementieren ist für diejenigen, die dem Link nicht folgen.
Peter Wood
1

Es gibt eine Verwechslung von Begriffen und / oder Verwechslungen von Konzepten sowohl in Frage als auch in Antworten.

Eine verzögerte Auswertung bedeutet nur, dass etwas zur Laufzeit zum letztmöglichen Zeitpunkt ausgewertet wird, wenn ein Wert benötigt wird. Der Standarddekorateur @propertymacht genau das. (*) Die dekorierte Funktion wird nur ausgewertet und jedes Mal, wenn Sie den Wert dieser Eigenschaft benötigen. (siehe Wikipedia-Artikel über faule Bewertung)

(*) Tatsächlich ist eine echte faule Bewertung (vergleiche z. B. Haskell) in Python sehr schwer zu erreichen (und führt zu Code, der alles andere als idiomatisch ist).

Auswendiglernen ist der richtige Begriff für das, wonach der Fragesteller zu suchen scheint. Reine Funktionen , die auf Nebenwirkungen für Rückgabewert Auswertung hängen nicht sicher memoized werden können , und es ist eigentlich ein Dekorateur in functools @functools.lru_cache so dass keine Notwendigkeit für das Schreiben von eigenen Dekorateure , wenn Sie Verhalten spezialisiert müssen.

Jason Herbburn
quelle
Ich habe den Begriff "faul" verwendet, weil in der ursprünglichen Implementierung das Mitglied zum Zeitpunkt der Objektinitialisierung aus einer Datenbank berechnet / abgerufen wurde, und ich möchte diese Berechnung verschieben, bis die Eigenschaft tatsächlich in einer Vorlage verwendet wurde. Dies schien mir der Definition von Faulheit zu entsprechen. Ich bin damit einverstanden, dass @property"faul" zu diesem Zeitpunkt wenig Sinn macht , da meine Frage bereits eine Lösung mit " voraussetzt ". (Ich dachte auch an Memoisation als eine Karte von Eingaben zu zwischengespeicherten Ausgaben, und da diese Eigenschaften nur eine Eingabe haben, nichts, schien eine Karte komplexer als nötig zu sein.)
Detly
Beachten Sie, dass alle Dekorateure, die von den Leuten als "out of the box" -Lösungen vorgeschlagen wurden, nicht existierten, als ich dies fragte.
Detly
Ich stimme Jason zu, dies ist eine Frage zum Zwischenspeichern / Auswendiglernen, nicht zur faulen Bewertung.
Poindexter
@poindexter - Caching deckt es nicht ganz ab ; Es wird nicht unterschieden, ob der Wert zum Zeitpunkt der Objektinitialisierung nachgeschlagen und zwischengespeichert wird, sondern ob der Wert beim Zugriff auf die Eigenschaft nachgeschlagen und zwischengespeichert wird (was hier das Hauptmerkmal ist). Wie soll ich es nennen? Dekorator "Cache-after-first-use"?
Detly
@detly auswendig lernen. Sie sollten es Memoize nennen. en.wikipedia.org/wiki/Memoization
poindexter
0

Sie können dies schön und einfach tun, indem Sie eine Klasse aus der nativen Python-Eigenschaft erstellen:

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

Wir können diese Eigenschaftsklasse wie eine reguläre Klasseneigenschaft verwenden (sie unterstützt auch die Elementzuweisung, wie Sie sehen können).

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)

Der Wert wurde nur beim ersten Mal berechnet und danach haben wir unseren gespeicherten Wert verwendet

Ausgabe:

I am calculating value
My calculated value
My calculated value
2
2
rezakamalifard
quelle