Zwischenspeichern von Klassenattributen in Python

73

Ich schreibe eine Klasse in Python und habe ein Attribut, dessen Berechnung relativ lange dauert. Daher möchte ich es nur einmal ausführen . Außerdem wird es nicht von jeder Instanz der Klasse benötigt, daher möchte ich es nicht standardmäßig in tun__init__ .

Ich bin neu in Python, aber nicht in der Programmierung. Ich kann mir einen Weg ausdenken, dies ziemlich einfach zu tun, aber ich habe immer wieder festgestellt, dass die 'Pythonic'-Art, etwas zu tun, oft viel einfacher ist als das, was ich mir aus meiner Erfahrung in anderen Sprachen ausgedacht habe.

Gibt es in Python einen "richtigen" Weg, dies zu tun?

mwolfe02
quelle
4
IMO ist keine dieser Antworten richtig. OP wollte eine zwischengespeicherte Klasseneigenschaft , z Foo.something_expensive. Alle diese Antworten beziehen sich auf zwischengespeicherte Instanzeigenschaften , was bedeutet something_expensive, dass für jede neue Instanz neu berechnet wird, was in den meisten Fällen nicht optimal ist
Steve

Antworten:

81

Python ≥ 3.8 @property und @functools.lru_cachewurden in kombiniert @cached_property.

import functools
class MyClass:
    @functools.cached_property
    def foo(self):
        print("long calculation here")
        return 21 * 2

Python ≥ 3,2 <3,8

Sie sollten beide @propertyund @functools.lru_cacheDekorateure verwenden:

import functools
class MyClass:
    @property
    @functools.lru_cache()
    def foo(self):
        print("long calculation here")
        return 21 * 2

Diese Antwort enthält detailliertere Beispiele und erwähnt auch einen Backport für frühere Python-Versionen.

Python <3.2

Das Python-Wiki verfügt über einen zwischengespeicherten Property Decorator (MIT-lizenziert), der folgendermaßen verwendet werden kann:

import random
# the class containing the property must be a new-style class
class MyClass(object):
   # create property whose value is cached for ten minutes
   @cached_property(ttl=600)
   def randint(self):
       # will only be evaluated every 10 min. at maximum.
       return random.randint(0, 100)

Oder jede Implementierung, die in den anderen Antworten erwähnt wird und Ihren Anforderungen entspricht.
Oder der oben erwähnte Backport.

Maxime R.
quelle
3
lru_cache wurde auch nach Python 2 zurückportiert
Buttons840
1
-1 lru_cachehat eine Standardgröße von 128, wodurch die Eigenschaftsfunktion möglicherweise zweimal aufgerufen wird. Wenn Sie lru_cache(None)alle Instanzen verwenden, bleiben sie dauerhaft am Leben.
Orlp
3
@orlp lru_cache hat eine Standardgröße von 128 für 128 verschiedene Argumentkonfigurationen. Dies ist nur dann ein Problem, wenn Sie mehr Objekte als Ihre Cache-Größe generieren, da sich hier nur das Argument self ändert. Wenn Sie so viele Objekte generieren, sollten Sie wirklich keinen unbegrenzten Cache verwenden, da dies Sie dazu zwingt, alle Objekte, die die Eigenschaft jemals aufgerufen haben, auf unbestimmte Zeit im Speicher zu belassen, was ein schrecklicher Speicherverlust sein könnte. Unabhängig davon wäre es wahrscheinlich besser, wenn Sie eine Caching-Methode verwenden, bei der der Cache im Objekt selbst gespeichert wird, sodass der Cache damit bereinigt wird.
Taywee
Die @property @functools.lru_cache()Methode gibt mir einen TypeError: unhashable typeFehler, vermutlich weil sie selfnicht hashbar ist.
Daniel Himmelstein
5
Achtung! Es scheint mir, functools.lru_cachedass Instanzen der Klasse GC vermeiden, solange sie sich im Cache befinden. Bessere Lösung ist functools.cached_propertyin Python 3.8.
user1338062
48

Früher habe ich das so gemacht, wie es der Knabber vorschlug, aber irgendwann hatte ich die kleinen Reinigungsschritte satt.

Also habe ich meinen eigenen Deskriptor erstellt:

class cached_property(object):
    """
    Descriptor (non-data) for building an attribute on-demand on first use.
    """
    def __init__(self, factory):
        """
        <factory> is called such: factory(instance) to build the attribute.
        """
        self._attr_name = factory.__name__
        self._factory = factory

    def __get__(self, instance, owner):
        # Build the attribute.
        attr = self._factory(instance)

        # Cache the value; hide ourselves.
        setattr(instance, self._attr_name, attr)

        return attr

So würden Sie es verwenden:

class Spam(object):

    @cached_property
    def eggs(self):
        print 'long calculation here'
        return 6*2

s = Spam()
s.eggs      # Calculates the value.
s.eggs      # Uses cached value.
Jon-Eric
quelle
12
Wunderbar! So funktioniert es: Instanzvariablen haben Vorrang vor Nicht-Daten-Deskriptoren . Beim ersten Zugriff auf das Attribut gibt es kein Instanzattribut, sondern nur das Deskriptorklassenattribut, und daher wird der Deskriptor ausgeführt. Während der Ausführung erstellt der Deskriptor jedoch ein Instanzattribut mit dem zwischengespeicherten Wert. Dies bedeutet, dass beim zweiten Zugriff auf das Attribut das zuvor erstellte Instanzattribut anstelle des ausgeführten Deskriptors zurückgegeben wird.
Florian Brucker
3
Es gibt ein cached_propertyPaket auf PyPI . Es enthält thread-sichere und zeitlich abgelaufene Versionen. (Auch danke, @Florian, für die Erklärung.)
Leez
4
Ja, für esoterische Eckfälle: Sie können bei der Verwendung keinen cached_propertyDeskriptor verwenden __slots__. Slots werden mithilfe von Datendeskriptoren implementiert, und die Verwendung eines cached_propertyDeskriptors überschreibt einfach den generierten Slot-Deskriptor, sodass der setattr()Aufruf nicht funktioniert, da __dict__das Attribut nicht festgelegt werden kann und der einzige für diesen Attributnamen verfügbare Deskriptor der cached_property.. Einfach ausgedrückt ist hier, um anderen zu helfen, diese Falle zu vermeiden.
Martijn Pieters
38

Der übliche Weg wäre, das Attribut zu einer Eigenschaft zu machen und den Wert bei der ersten Berechnung zu speichern

import time

class Foo(object):
    def __init__(self):
        self._bar = None

    @property
    def bar(self):
        if self._bar is None:
            print "starting long calculation"
            time.sleep(5)
            self._bar = 2*2
            print "finished long caclulation"
        return self._bar

foo=Foo()
print "Accessing foo.bar"
print foo.bar
print "Accessing foo.bar"
print foo.bar
John La Rooy
quelle
3
Gibt es in Python3.2 + eine Motivation, diesen Ansatz zu verwenden @property + @functools.lru_cache()? Der quasi-private Attributweg scheint an Java / Setter / Getter zu erinnern; Meiner bescheidenen Meinung nach ist es pythonischer, nur mit lru_cache zu dekorieren
Brad Solomon
(Wie in @ Maximes Antwort )
Brad Solomon
4
@Brad @functools.lru_cache()würde das mit dem Argument verschlüsselte Ergebnis zwischenspeichern self, und dies würde auch verhindern, dass diese Instanz GC-fähig wird, solange sie sich im Cache befindet.
Rektalogik
18

Python 3.8 enthält den functools.cached_propertyDekorator.

Transformieren Sie eine Methode einer Klasse in eine Eigenschaft, deren Wert einmal berechnet und dann als normales Attribut für die Lebensdauer der Instanz zwischengespeichert wird. Ähnlich wie property()beim Hinzufügen von Caching. Nützlich für teure berechnete Eigenschaften von Instanzen, die ansonsten effektiv unveränderlich sind.

Dieses Beispiel stammt direkt aus den Dokumenten:

from functools import cached_property

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)

Die Einschränkung besteht darin, dass das Objekt mit der Eigenschaft, die zwischengespeichert werden soll, ein __dict__Attribut haben muss , das eine veränderbare Zuordnung ist und Klassen mit ausschließt, __slots__sofern dies nicht __dict__in definiert ist __slots__.

SuperShoot
quelle
2
class MemoizeTest:

      _cache = {}
      def __init__(self, a):
          if a in MemoizeTest._cache:
              self.a = MemoizeTest._cache[a]
          else:
              self.a = a**5000
              MemoizeTest._cache.update({a:self.a})
Mouad
quelle
1

Sie könnten versuchen, sich mit Memoisierung zu befassen. Wenn Sie einer Funktion dieselben Argumente übergeben, wird das zwischengespeicherte Ergebnis zurückgegeben. Weitere Informationen zur Implementierung in Python finden Sie hier .

Abhängig davon, wie Ihr Code eingerichtet ist (Sie sagen, dass er nicht von allen Instanzen benötigt wird), können Sie auch versuchen, ein Fliegengewichtsmuster oder ein verzögertes Laden zu verwenden.

NT3RP
quelle
1

Das dickensPaket (nicht meine) Angebote cachedproperty, classpropertyund cachedclasspropertyDekorateure.

So speichern Sie eine Klasseneigenschaft zwischen :

from descriptors import cachedclassproperty

class MyClass:
    @cachedclassproperty
    def approx_pi(cls):
        return 22 / 7
Scharfsinn
quelle
-2

Der einfachste Weg, dies zu tun, wäre wahrscheinlich, einfach eine Methode zu schreiben (anstatt ein Attribut zu verwenden), die das Attribut umschließt (Getter-Methode). Beim ersten Aufruf berechnet, speichert und gibt diese Methode den Wert zurück. später wird nur der gespeicherte Wert zurückgegeben.

ChrisM
quelle
-4

Mit Python 2, aber nicht mit Python 3, mache ich Folgendes. Dies ist ungefähr so ​​effizient wie möglich:

class X:
    @property
    def foo(self):
        r = 33
        self.foo = r
        return r

Erläuterung: Grundsätzlich überlade ich nur eine Eigenschaftsmethode mit dem berechneten Wert. Nach dem ersten Zugriff auf die Eigenschaft (für diese Instanz) ist foosie keine Eigenschaft mehr und wird zu einem Instanzattribut. Der Vorteil dieses Ansatzes besteht darin, dass ein Cache-Treffer so billig wie möglich self.__dict__ist, da er als Cache verwendet wird, und dass kein Instanz-Overhead entsteht, wenn die Eigenschaft nicht verwendet wird.

Dieser Ansatz funktioniert nicht mit Python 3.

user1054050
quelle