Kurs
Python Cache: Zwei einfache Methoden
In diesem Artikel lernen wir etwas über Caching in Python. Wir werden verstehen, was es ist und wie man es effektiv einsetzt.
Caching ist eine Technik zur Verbesserung der Anwendungsleistung, bei der die vom Programm erzielten Ergebnisse zwischengespeichert werden, um sie bei Bedarf später wieder zu verwenden.
In diesem Lernprogramm lernen wir verschiedene Techniken für das Caching in Python kennen, darunter die Dekoratoren @lru_cache
und @cache
im Modul functools
.
Für diejenigen unter euch, die es eilig haben, fangen wir mit einer sehr kurzen Caching-Implementierung an und fahren dann mit mehr Details fort.
Kurze Antwort: Python Caching Implementierung
Um einen Cache in Python zu erstellen, können wir den @cache
Dekorator aus dem functools
Modul verwenden. Beachte im folgenden Code, dass die Funktion print()
nur einmal ausgeführt wird:
import functools
@functools.cache
def square(n):
print(f"Calculating square of {n}")
return n * n
# Testing the cached function
print(square(4)) # Output: Calculating square of 4 \n 16
print(square(4)) # Output: 16 (cached result, no recalculation)
Calculating square of 4
16
16
Was ist Caching in Python?
Angenommen, wir müssen ein mathematisches Problem lösen und brauchen eine Stunde, um die richtige Antwort zu finden. Wenn wir am nächsten Tag dasselbe Problem lösen müssten, wäre es hilfreich, unsere vorherige Arbeit wiederzuverwenden, anstatt ganz von vorne anzufangen.
Das Caching in Python folgt einem ähnlichen Prinzip: Es speichert Werte, wenn sie innerhalb von Funktionsaufrufen berechnet werden, um sie bei erneutem Bedarf wiederzuverwenden. Diese Art der Zwischenspeicherung wird auch als Memoisierung bezeichnet.
Schauen wir uns ein kurzes Beispiel an, das die Summe eines großen Zahlenbereichs zweimal ausrechnet:
output = sum(range(100_000_001))
print(output)
output = sum(range(100_000_001))
print(output)
5000000050000000
5000000050000000
Das Programm muss jedes Mal die Summe ausrechnen. Wir können das bestätigen, indem wir die beiden Anrufe zeitlich festlegen:
import timeit
print(
timeit.timeit(
"sum(range(100_000_001))",
globals=globals(),
number=1,
)
)
print(
timeit.timeit(
"sum(range(100_000_001))",
globals=globals(),
number=1,
)
)
1.2157779589979327
1.1848394999979064
Die Ausgabe zeigt, dass beide Aufrufe ungefähr gleich viel Zeit benötigen (je nach unseren Einstellungen können wir schnellere oder langsamere Ausführungszeiten erhalten).
Wir können jedoch einen Cache verwenden, um zu vermeiden, dass wir denselben Wert mehrmals ausrechnen müssen. Wir können den Namen sum
mit der Funktion cache()
im eingebauten Modul functools
umdefinieren:
import functools
import timeit
sum = functools.cache(sum)
print(
timeit.timeit(
"sum(range(100_000_001))",
globals=globals(),
number=1,
)
)
print(
timeit.timeit(
"sum(range(100_000_001))",
globals=globals(),
number=1,
)
)
1.2760689580027247
2.3330067051574588e-06
Der zweite Aufruf dauert jetzt ein paar Mikrosekunden statt über eine Sekunde, weil das Ergebnis der Summe der Zahlen von 0 bis 100.000.000 bereits berechnet und zwischengespeichert wurde - der zweite Aufruf verwendet den Wert, der zuvor berechnet und gespeichert wurde.
Oben haben wir den Dekorator functools.cache()
verwendet, um einen Cache in die eingebaute Funktion sum()
einzubinden. Nebenbei bemerkt ist ein Dekorator in Python eine Funktion, die das Verhalten einer anderen Funktion ändert, ohne deren Code dauerhaft zu verändern. Mehr über Dekoratoren erfährst du in diesem Python Decorators Tutorial.
Der functools.cache()
Dekorator wurde in Python in Version 3.9 hinzugefügt, aber wir können functools.lru_cache()
auch für ältere Versionen verwenden. Im nächsten Abschnitt werden wir beide Möglichkeiten zur Erstellung eines Caches untersuchen, einschließlich der Verwendung der häufiger verwendeten Dekorator-Notation, wie @cache
.
Python Caching: Verschiedene Methoden
Das Python-Modul functools
hat zwei Dekoratoren, mit denen du Funktionen zwischenspeichern kannst. Lass uns functools.lru_cache()
und functools.cache()
anhand eines Beispiels erkunden.
Schreiben wir eine Funktion sum_digits()
, die eine Folge von Zahlen aufnimmt und die Summe der Ziffern dieser Zahlen zurückgibt. Wenn wir zum Beispiel das Tupel (23, 43, 8)
als Eingabe verwenden, dann:
- Die Summe der Ziffern von
23
ist fünf. - Die Summe der Ziffern von
43
ist sieben. - Die Summe der Ziffern von
8
ist acht. - Die Gesamtsumme ist also 20.
Das ist eine Möglichkeit, wie wir unsere sum_digits()
Funktion schreiben können:
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
numbers = 23, 43, 8
print(sum_digits(numbers))
20
Nutzen wir diese Funktion, um verschiedene Möglichkeiten zu erkunden, einen Cache zu erstellen.
Python manuelles Caching
Legen wir den Cache zunächst manuell an. Obwohl wir dies auch leicht automatisieren könnten, hilft uns die manuelle Erstellung eines Caches, den Prozess zu verstehen.
Wir erstellen ein Wörterbuch und fügen jedes Mal, wenn wir die Funktion aufrufen, Schlüssel-Wert-Paare mit einem neuen Wert hinzu, um die Ergebnisse zu speichern. Wenn wir die Funktion mit einem Wert aufrufen, der bereits in diesem Wörterbuch gespeichert ist, gibt die Funktion den gespeicherten Wert zurück, ohne ihn erneut auszuarbeiten:
import random
import timeit
def sum_digits(numbers):
if numbers not in sum_digits.my_cache:
sum_digits.my_cache[numbers] = sum(
int(digit) for number in numbers for digit in str(number)
)
return sum_digits.my_cache[numbers]
sum_digits.my_cache = {}
numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
0.28875587500078836
0.0044607500021811575
Der zweite Aufruf von sum_digits(numbers)
ist viel schneller als der erste, weil er den zwischengespeicherten Wert verwendet.
Erläutern wir nun den obigen Code genauer. Beachte zunächst, dass wir das Wörterbuch sum_digits.my_cache
nach der Definition der Funktion erstellen, obwohl wir es in der Funktionsdefinition verwenden.
Die Funktion sum_digits()
prüft, ob das an die Funktion übergebene Argument bereits einer der Schlüssel im sum_digits.my_cache
Wörterbuch ist. Die Summe aller Ziffern wird nur ausgewertet, wenn sich das Argument nicht bereits im Cache befindet.
Da das Argument, das wir beim Aufruf der Funktion verwenden, als Schlüssel im Wörterbuch dient, muss es ein hashfähiger Datentyp sein. Eine Liste ist nicht hashbar, also können wir sie nicht als Schlüssel in einem Wörterbuch verwenden. Versuchen wir zum Beispiel, numbers
durch eine Liste anstelle eines Tupels zu ersetzen - das ergibt TypeError
:
# ...
numbers = [random.randint(1, 1000) for _ in range(1_000_000)]
# ...
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'
Das manuelle Anlegen eines Caches ist für Lernzwecke gut geeignet, aber jetzt wollen wir herausfinden, wie man es schneller machen kann.
Python caching with functools.lru_cache()
Python verfügt seit Version 3.2 über den Dekorator lru_cache()
. Das "lru" am Anfang des Funktionsnamens steht für "least recently used". Wir können uns den Cache wie eine Kiste vorstellen, in der häufig benutzte Dinge aufbewahrt werden. Wenn er voll ist, wirft die LRU-Strategie das Element weg, das wir am längsten nicht mehr benutzt haben, um Platz für etwas Neues zu schaffen.
Schmücken wir unsere Funktion sum_digits()
mit @functools.lru_cache
aus:
import functools
import random
import timeit
@functools.lru_cache
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
0.28326129099878017
0.002184917000704445
Dank der Zwischenspeicherung benötigt der zweite Aufruf deutlich weniger Zeit.
In der Standardeinstellung speichert der Cache die ersten 128 berechneten Werte. Sobald alle 128 Plätze belegt sind, löscht der Algorithmus den am wenigsten genutzten Wert (LRU), um Platz für neue Werte zu schaffen.
Wir können eine andere maximale Cachegröße festlegen, wenn wir die Funktion mit dem Parameter maxsize
ausschmücken:
import functools
import random
import timeit
@functools.lru_cache(maxsize=5)
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
# ...
In diesem Fall speichert der Cache nur fünf Werte. Wir können das Argument maxsize
auch auf None
setzen, wenn wir die Größe des Caches nicht begrenzen wollen.
Python Caching mit functools.cache()
Python 3.9 enthält einen einfacheren und schnelleren Caching-Dekorator -functools.cache()
. Dieser Dekorator hat zwei Hauptmerkmale:
- Es gibt keine Maximalgröße - es ist ähnlich wie der Aufruf von
functools.lru_cache(maxsize=None)
. - Sie speichert alle Funktionsaufrufe und ihre Ergebnisse (sie verwendet nicht die LRU-Strategie). Dies eignet sich für Funktionen mit relativ kleinen Ausgaben oder wenn wir uns keine Gedanken über Cache-Größenbeschränkungen machen müssen.
Verwenden wir den @functools.cache
Dekorator für die Funktion sum_digits()
:
import functools
import random
import timeit
@functools.cache
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
numbers = tuple(random.randint(1, 1000) for _ in range(1_000_000))
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
print(
timeit.timeit(
"sum_digits(numbers)",
globals=globals(),
number=1
)
)
0.16661812500024098
0.0018135829996026587
Die Einrichtung von sum_digits()
mit @functools.cache
ist gleichbedeutend mit der Zuweisung von sum_digits
an functools.cache()
:
# ...
def sum_digits(numbers):
return sum(
int(digit) for number in numbers for digit in str(number)
)
sum_digits = functools.cache(sum_digits)
Beachte, dass wir auch einen anderen Importstil verwenden können:
from functools import cache
Auf diese Weise können wir unsere Funktionen nur mit @cache
schmücken.
Andere Caching-Strategien
Pythons eigene Tools implementieren die LRU-Caching-Strategie, bei der die am wenigsten genutzten Einträge gelöscht werden, um Platz für neue Werte zu schaffen.
Werfen wir einen Blick auf ein paar andere Caching-Strategien:
- First-in, first-out (FIFO): Wenn der Cache voll ist, wird das erste hinzugefügte Element entfernt, um Platz für neue Werte zu schaffen. Der Unterschied zwischen LRU und FIFO besteht darin, dass LRU die zuletzt verwendeten Elemente im Cache behält, während FIFO das älteste Element unabhängig von der Verwendung verwirft.
- Last-in, first-out (LIFO): Das zuletzt hinzugefügte Element wird entfernt, wenn der Cache voll ist. Stell dir einen Stapel von Tellern in einer Cafeteria vor. Der Teller, den wir zuletzt auf den Stapel gelegt haben (last in), ist der, den wir zuerst abnehmen (first out).
- Am häufigsten verwendet (MRU): Der Wert, der zuletzt verwendet wurde, wird verworfen, wenn der Platz im Cache benötigt wird.
- Zufällige Ersetzung (RR): Bei dieser Strategie wird ein Gegenstand zufällig weggeworfen, um Platz für einen neuen zu schaffen.
Diese Strategien können auch mit der Messung der Gültigkeitsdauer kombiniert werden, d.h. wie lange ein Teil der Daten im Cache als gültig oder relevant angesehen wird. Stell dir einen Nachrichtenartikel in einem Cache vor. Es kann sein, dass häufig auf sie zugegriffen wird (LRU würde sie behalten), aber nach einer Woche sind die Nachrichten vielleicht schon veraltet.
Python Caching: Allgemeine Anwendungsfälle
Bisher haben wir zu Lernzwecken vereinfachte Beispiele verwendet. Das Caching hat jedoch viele reale Anwendungen.
In der Datenwissenschaft führen wir oft wiederholte Operationen an großen Datensätzen durch. Die Verwendung von zwischengespeicherten Ergebnissen reduziert den Zeit- und Kostenaufwand, der mit der wiederholten Durchführung der gleichen Berechnungen mit den gleichen Datensätzen verbunden ist.
Wir können das Caching auch zum Speichern externer Ressourcen wie Webseiten oder Datenbanken nutzen. Betrachten wir ein Beispiel und cachen einen DataCamp-Artikel. Aber zuerst müssen wir das requests
Modul eines Drittanbieters installieren, indem wir die folgende Zeile im Terminal eingeben:
$ python -m pip install requests
Sobald requests
installiert ist, können wir den folgenden Code ausprobieren, der versucht, denselben DataCamp-Artikel zweimal abzurufen und dabei den @lru_cache
Dekorator zu verwenden:
import requests
from functools import lru_cache
@lru_cache(maxsize=10)
def get_article(url):
print(f"Fetching article from {url}")
response = requests.get(url)
return response.text
print(get_article("https://www.datacamp.com/tutorial/decorators-python"))
print(get_article("https://www.datacamp.com/tutorial/decorators-python"))
Fetching article from https://www.datacamp.com/tutorial/decorators-python
<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title>...
<!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title>...
Nebenbei bemerkt, haben wir die Ausgabe gekürzt, weil sie sehr lang ist. Beachte jedoch, dass nur der erste Aufruf von get_article()
die Phrase Fetching article from {url}
ausgibt.
Das liegt daran, dass die Webseite nur beim ersten Aufruf aufgerufen wird. Das Ergebnis wird im Cache der Funktion gespeichert. Wenn wir die gleiche Webseite ein zweites Mal aufrufen, werden stattdessen die im Cache gespeicherten Daten zurückgegeben.
Das Caching sorgt dafür, dass es keine unnötigen Verzögerungen gibt, wenn dieselben Daten wiederholt abgerufen werden. Externe APIs haben oft auch Ratenbeschränkungen und Kosten, die mit dem Abrufen von Daten verbunden sind. Caching senkt die Kosten für APIs und die Wahrscheinlichkeit, an Ratengrenzen zu stoßen.
Ein weiterer häufiger Anwendungsfall sind Anwendungen für maschinelles Lernen , bei denen mehrere teure Berechnungen wiederholt werden müssen. Wenn wir zum Beispiel einen Text tokenisieren und vektorisieren müssen, bevor wir ihn in einem maschinellen Lernmodell verwenden, können wir die Ausgabe in einem Cache speichern. Auf diese Weise müssen wir die rechenaufwändigen Operationen nicht wiederholen.
Häufige Herausforderungen beim Caching in Python
Wir haben die Vorteile des Caching in Python kennengelernt. Es gibt auch einige Herausforderungen und Nachteile, die du bei der Implementierung eines Caches beachten musst:
- Cache-Invalidierung und Konsistenz: Die Daten können sich mit der Zeit ändern. Deshalb müssen die im Cache gespeicherten Werte möglicherweise auch aktualisiert oder entfernt werden.
- Speicherverwaltung: Das Speichern großer Datenmengen in einem Cache erfordert Speicherplatz, was zu Leistungsproblemen führen kann, wenn der Cache ins Unendliche wächst.
- Komplexität: Das Hinzufügen von Caches erhöht die Komplexität des Systems bei der Erstellung und Pflege des Caches. Oft überwiegen die Vorteile diese Kosten, aber diese erhöhte Komplexität könnte zu Fehlern führen, die schwer zu finden und zu korrigieren sind.
Fazit
Wir können das Caching nutzen, um die Leistung zu optimieren, wenn rechenintensive Operationen mit denselben Daten wiederholt werden.
Python verfügt über zwei Dekoratoren, um beim Aufruf von Funktionen einen Cache zu erstellen: @lru_cache
und @cache
im Modul functools
.
Wir müssen jedoch sicherstellen, dass wir den Cache auf dem neuesten Stand halten und den Speicher richtig verwalten.
Wenn du mehr über Caching und Python lernen möchtest, solltest du dir diesen Lernpfad mit sechs Kursen zur Python-Programmierung ansehen.
Ich habe Physik und Mathematik auf UG-Ebene an der Universität Malta studiert. Dann zog ich nach London und machte meinen Doktor in Physik am Imperial College. Ich habe an neuartigen optischen Techniken zur Abbildung der menschlichen Netzhaut gearbeitet. Jetzt konzentriere ich mich darauf, über Python zu schreiben, über Python zu kommunizieren und Python zu unterrichten.
Lerne Python für Data Science!
Kurs
Python Toolbox
Kurs