„Python ist zu langsam.“
Dieser Satz wird häufig in Diskussionen über Programmiersprachen geäußert und überschattet oft die zahlreichen Stärken von Python.
Die Wahrheit ist, dass Python schnell ist, wenn man es auf pythonische Weise schreiben kann.
Der Teufel steckt im Detail. Erfahrene Python-Entwickler verfügen über ein ganzes Arsenal kleiner, aber wirkungsvoller Tricks, mit denen sie die Leistung ihres Codes erheblich steigern können.
Diese Tricks mögen auf den ersten Blick unbedeutend erscheinen, aber sie können zu erheblichen Effizienzsteigerungen führen. Im Folgenden werden 9 dieser Ansätze vorgestellt, die die Art und Weise, wie Sie Python-Code schreiben und optimieren, verändern werden.
1. Schnellere String-Verkettung: Gekonnt „join()“ oder „+“ wählen
Die Verkettung von Strings wird zu einem Engpass in Ihrem Python-Programm, wenn eine große Anzahl von Strings darauf wartet, verarbeitet zu werden.
Grundsätzlich gibt es zwei Möglichkeiten der String-Verkettung in Python:
- Verwenden Sie die Funktion join(), um eine Liste von Zeichenketten zu einer einzigen zusammenzufügen.
- Verwenden Sie das Symbol + oder +=, um jede einzelne Zeichenkette zu einer einzigen zu addieren
Welcher Weg ist also schneller?
Lassen Sie uns 3 verschiedene Funktionen für die Verkettung der gleichen Strings definieren:
mylist = ["Yang", "Zhou", "is", "writing"]
# Using '+'
def concat_plus():
result = ""
for word in mylist:
result += word + " "
return result
# Using 'join()'
def concat_join():
return " ".join(mylist)
# Directly concatenation without the list
def concat_directly():
return "Yang" + "Zhou" + "is" + "writing"
Welche Funktion ist nach Ihrem ersten Eindruck die schnellste und welche die langsamste?
Das tatsächliche Ergebnis wird Sie vielleicht überraschen:
import timeit
print(timeit.timeit(concat_plus, number=10000))
# 0.002738415962085128
print(timeit.timeit(concat_join, number=10000))
# 0.0008482920238748193
print(timeit.timeit(concat_directly, number=10000))
# 0.00021425005979835987
Wie oben gezeigt, ist die join()-Methode bei der Verkettung einer Liste von Strings schneller als das Hinzufügen der eizelnen Strings.
Der Grund dafür ist ganz einfach. Einerseits sind Strings in Python unveränderliche Daten, jede +=-Operation führt zur Erstellung einer neuen Zeichenkette und zum Kopieren der alten Zeichenkette, was rechenintensiv ist.
Andererseits ist die Methode .join() speziell für das Verbinden einer Folge von Zeichenketten optimiert. Sie berechnet die Größe der resultierenden Zeichenkette im Voraus und baut sie dann in einem Durchgang auf. So vermeidet sie den Overhead, der mit der +=-Operation in einer Schleife verbunden ist, und ist daher schneller.
Die schnellste Funktion in unseren Tests ist jedoch die direkte Verkettung von String-Literalen. Ihre hohe Geschwindigkeit ist zurückzuführen auf:
- Der Python-Interpreter kann die Verkettung von String-Literalen zur Kompilierzeit optimieren, indem er sie in ein einziges String-Literal verwandelt. Es sind keine Schleifeniterationen oder Funktionsaufrufe erforderlich, so dass es sich um einen sehr effizienten Vorgang handelt.
- Da alle Zeichenketten zur Kompilierungszeit bekannt sind, kann Python diese Operation sehr schnell durchführen, viel schneller als die Verkettung zur Laufzeit in einer Schleife oder sogar die optimierte Methode .join().
Kurz gesagt, wenn Sie eine Liste von Strings verketten müssen, wählen Sie join() statt +=. Wenn Sie Strings direkt verketten möchten, verwenden Sie einfach + dazu.
2. Schnellere Listenerstellung: Verwenden Sie „[]“ über „list()“
Die Erstellung einer Liste ist keine große Sache. Zwei gängige Methoden sind:
- Verwenden Sie die Funktion list()
- Direkte Verwendung von []
Lassen Sie uns einen einfachen Codeschnipsel verwenden, um ihre Leistung zu testen:
import timeit
print(timeit.timeit('[]', number=10 ** 7))
# 0.1368238340364769
print(timeit.timeit(list, number=10 ** 7))
# 0.2958830420393497
Wie das Ergebnis zeigt, ist die Ausführung der Funktion list() langsamer als die direkte Verwendung von [].
Das liegt daran, dass [] eine Literal-Syntax ist, während list() ein Konstruktoraufruf ist. Der Aufruf einer Funktion erfordert zweifellos zusätzliche Zeit.
Aus der gleichen Logik heraus sollten wir beim Erstellen eines Wörterbuchs auch {} über dict() nutzen.
3. Schnelleres Testen der Mitgliedschaft: Eine Menge statt einer Liste verwenden
Die Leistung eines Mitgliedschaftsprüfungsvorgangs hängt stark von den zugrunde liegenden Datenstrukturen ab:
import timeit
large_dataset = range(100000)
search_element = 2077
large_list = list(large_dataset)
large_set = set(large_dataset)
def list_membership_test():
return search_element in large_list
def set_membership_test():
return search_element in large_set
print(timeit.timeit(list_membership_test, number=1000))
# 0.01112208398990333
print(timeit.timeit(set_membership_test, number=1000))
# 3.27499583363533e-05
Wie der obige Code zeigt, ist die Prüfung der Zugehörigkeit in einer Menge viel schneller als in einer Liste.
Warum ist das so?
- In Python-Listen erfolgt die Prüfung der Zugehörigkeit (Element in der Liste) durch Iteration über jedes Element, bis das gewünschte Element gefunden oder das Ende der Liste erreicht ist. Daher hat diese Operation eine Zeitkomplexität von O(n).
- Mengen in Python sind als Hashtabellen implementiert. Bei der Prüfung auf Zugehörigkeit (Element in Menge) verwendet Python einen Hash-Mechanismus, dessen Zeitkomplexität im Durchschnitt O(1) beträgt.
Hier geht es darum, beim Schreiben von Programmen die zugrunde liegende Datenstruktur sorgfältig zu berücksichtigen. Die Nutzung der richtigen Datenstruktur kann unseren Code erheblich beschleunigen.
4. Schnellere Datengenerierung: Comprehensions statt For Loops verwenden
In Python gibt es vier Arten von Comprehensions: Liste, Wörterbuch, Menge und Generator. Sie bieten nicht nur eine prägnantere Syntax für die Erstellung relativer Datenstrukturen, sondern auch eine bessere Leistung als For Loops. Denn sie sind in der C‑Implementierung von Python optimiert.
import timeit
def generate_squares_for_loop():
squares = []
for i in range(1000):
squares.append(i * i)
return squares
def generate_squares_comprehension():
return [i * i for i in range(1000)]
print(timeit.timeit(generate_squares_for_loop, number=10000))
# 0.2797503340989351
print(timeit.timeit(generate_squares_comprehension, number=10000))
# 0.2364629579242319
Der obige Code ist ein einfacher Geschwindigkeitsvergleich zwischen einem Listenaufruf und einer for-Schleife. Wie das Ergebnis zeigt, ist die Listenverarbeitung schneller.
5. Schnellere Loops: Lokale Variablen bevorzugen
In Python ist der Zugriff auf eine lokale Variable schneller als der Zugriff auf eine globale Variable oder auf ein Attribut eines Objekts.
Hier ist ein Beispiel, um dies zu beweisen:
import timeit
class Example:
def __init__(self):
self.value = 0
obj = Example()
def test_dot_notation():
for _ in range(1000):
obj.value += 1
def test_local_variable():
value = obj.value
for _ in range(1000):
value += 1
obj.value = value
print(timeit.timeit(test_dot_notation, number=1000))
# 0.036605041939765215
print(timeit.timeit(test_local_variable, number=1000))
# 0.024470250005833805
Das ist die Funktionsweise von Python. Intuitiv sind beim Kompilieren einer Funktion die lokalen Variablen innerhalb der Funktion bekannt, aber andere Variablen außerhalb der Funktion brauchen Zeit, um abgerufen zu werden.
Das ist eine Kleinigkeit, aber wir können sie nutzen, um unseren Code zu optimieren, wenn wir eine große Datenmenge verarbeiten.
6. Schnellere Ausführung: Integrierte Module und Bibliotheken bevorzugen
Wenn Ingenieure Python sagen, ist damit standardmäßig CPython gemeint. Denn CPython ist die standardmäßige und am weitesten verbreitete Implementierung der Sprache Python.
Da die meisten der eingebauten Module und Bibliotheken in C geschrieben sind, einer schnelleren und niedrigeren Sprache, sollten wir das eingebaute Arsenal nutzen und das Rad nicht neu erfinden.
import timeit
import random
from collections import Counter
def count_frequency_custom(lst):
frequency = {}
for item in lst:
if item in frequency:
frequency[item] += 1
else:
frequency[item] = 1
return frequency
def count_frequency_builtin(lst):
return Counter(lst)
large_list = [random.randint(0, 100) for _ in range(1000)]
print(timeit.timeit(lambda: count_frequency_custom(large_list), number=100))
# 0.005160166998393834
print(timeit.timeit(lambda: count_frequency_builtin(large_list), number=100))
# 0.002444291952997446
Das obige Programm vergleicht zwei Ansätze zum Zählen der Elementhäufigkeit in einer Liste. Wie wir sehen können, ist die Nutzung des eingebauten Zählers aus dem Sammlungsmodul schneller, übersichtlicher und besser als das Schreiben einer for-Schleife durch uns selbst.
7. Schnellere Funktionsaufrufe: Nutzung des Cache-Dekorators für einfache Memoisierung
Caching ist eine häufig verwendete Technik, um wiederholte Berechnungen zu vermeiden und Programme zu beschleunigen.
Glücklicherweise müssen wir in den meisten Fällen keinen eigenen Code für die Zwischenspeicherung schreiben, da Python einen Standard-Dekorator für diesen Zweck bereitstellt – @functools.cache.
Der folgende Code führt zum Beispiel zwei Funktionen zur Erzeugung von Fibonacci-Zahlen aus, von denen eine einen Caching-Dekorator hat, die andere aber nicht:
import timeit
import functools
def fibonacci(n):
if n in (0, 1):
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@functools.cache
def fibonacci_cached(n):
if n in (0, 1):
return n
return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)
# Test the execution time of each function
print(timeit.timeit(lambda: fibonacci(30), number=1))
# 0.09499712497927248
print(timeit.timeit(lambda: fibonacci_cached(30), number=1))
# 6.458023563027382e-06
Das Ergebnis zeigt, wie der functools.cache-Dekorator unseren Code schneller macht.
Die grundlegende Fibonacci-Funktion ist ineffizient, da sie dieselben Fibonacci-Zahlen während des Prozesses der Ermittlung des Ergebnisses von fibonacci(30) mehrfach neu berechnet.
Die im Cache gespeicherte Version ist wesentlich schneller, da sie die Ergebnisse früherer Berechnungen im Cache speichert. So wird jede Fibonacci-Zahl nur einmal berechnet, und nachfolgende Aufrufe mit denselben Argumenten werden aus dem Cache abgerufen.
Allein das Hinzufügen eines eingebauten Dekorators kann eine so große Verbesserung bewirken.
8. Schnellere Endlosschleife: Bevorzugen Sie „while 1“ gegenüber „while True“
Um eine unendliche while-Schleife zu erstellen, können wir while True oder while 1 verwenden.
Der Unterschied in der Leistung ist normalerweise vernachlässigbar. Aber es ist interessant zu wissen, dass while 1 etwas schneller ist.
Das liegt daran, dass 1 ein Literal ist, während True ein globaler Name ist, der im globalen Gültigkeitsbereich von Python nachgeschlagen werden muss, so dass ein winziger Overhead erforderlich ist.
Schauen wir uns den tatsächlichen Vergleich dieser beiden Möglichkeiten in einem Codeschnipsel an:
import timeit
def loop_with_true():
i = 0
while True:
if i >= 1000:
break
i += 1
def loop_with_one():
i = 0
while 1:
if i >= 1000:
break
i += 1
print(timeit.timeit(loop_with_true, number=10000))
# 0.1733035419601947
print(timeit.timeit(loop_with_one, number=10000))
# 0.16412191605195403
Wie wir sehen können, ist while 1 tatsächlich etwas schneller.
Moderne Python-Interpreter (wie CPython) sind jedoch stark optimiert, und solche Unterschiede sind in der Regel unbedeutend. Wir brauchen uns also keine Sorgen über diesen vernachlässigbaren Unterschied zu machen. Ganz zu schweigen davon, dass while True besser lesbar ist als while 1.
9. Schnellerer Start: Python-Module intelligent importieren
Es scheint selbstverständlich zu sein, alle Module am Anfang eines Python-Skripts zu importieren.
Tatsächlich müssen wir das aber nicht tun.
Wenn ein Modul zu groß ist, ist es außerdem besser, es nach Bedarf zu importieren.
def my_function():
import heavy_module
# rest of the function
Wie der obige Code wird heavy_module innerhalb einer Funktion importiert. Dies ist eine Idee des „lazy loading“, bei der der Import aufgeschoben wird, bis my_function aufgerufen wird.
Der Vorteil dieses Ansatzes besteht darin, dass, wenn my_function während der Ausführung unseres Skripts nie aufgerufen wird, heavy_module auch nie geladen wird, was Ressourcen spart und die Startzeit unseres Skripts verkürzt.
Quelle: medium.com