Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
1 Einleitung
2 Die Programmiersprache Python
Teil I Einstieg in Python
3 Erste Schritte im interaktiven Modus
4 Der Weg zum ersten Programm
5 Kontrollstrukturen
6 Dateien
7 Das Laufzeitmodell
8 Funktionen, Methoden und Attribute
9 Informationsquellen zu Python
Teil II Datentypen
10 Das Nichts – NoneType
11 Operatoren
12 Numerische Datentypen
13 Sequenzielle Datentypen
14 Zuordnungen
15 Mengen
16 Collections
17 Datum und Zeit
18 Aufzählungstypen – Enum
Teil III Fortgeschrittene Programmiertechniken
19 Funktionen
20 Modularisierung
21 Objektorientierung
22 Ausnahmebehandlung
23 Iteratoren
24 Kontextobjekte
25 Manipulation von Funktionen und Methoden
Teil IV Die Standardbibliothek
26 Mathematik
27 Kryptografie
28 Reguläre Ausdrücke
29 Schnittstelle zu Betriebssystem und Laufzeitumgebung
30 Kommandozeilenparameter
31 Dateisystem
32 Parallele Programmierung
33 Datenspeicherung
34 Netzwerkkommunikation
35 Debugging und Qualitätssicherung
36 Dokumentation
Teil V Weiterführende Themen
37 Anbindung an andere Programmiersprachen
38 Distribution von Python-Projekten
39 Grafische Benutzeroberflächen
40 Python als serverseitige Programmiersprache im WWW – ein Einstieg in Django
41 Wissenschaftliches Rechnen
42 Insiderwissen
43 Von Python 2 nach Python 3
A Anhang
Stichwortverzeichnis

Download:
- Beispielprogramme, ca. 464 KB

Jetzt Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Python 3 von Johannes Ernesti, Peter Kaiser
Das umfassende Handbuch
Buch: Python 3

Python 3
Pfeil 35 Debugging und Qualitätssicherung
Pfeil 35.1 Der Debugger
Pfeil 35.2 Formatierte Bildschirmausgabe – pprint
Pfeil 35.3 Logdateien – logging
Pfeil 35.3.1 Das Meldungsformat anpassen
Pfeil 35.3.2 Logging Handler
Pfeil 35.4 Automatisiertes Testen
Pfeil 35.4.1 Testfälle in Docstrings – doctest
Pfeil 35.4.2 Unit Tests – unittest
Pfeil 35.5 Analyse des Laufzeitverhaltens
Pfeil 35.5.1 Laufzeitmessung – timeit
Pfeil 35.5.2 Profiling – cProfile
Pfeil 35.5.3 Tracing – trace
Pfeil 35.6 Optimierung
Pfeil 35.6.1 Die Optimize-Option
Pfeil 35.6.2 Mutabel vs. immutabel
Pfeil 35.6.3 Schleifen
Pfeil 35.6.4 Funktionsaufrufe
Pfeil 35.6.5 C
Pfeil 35.6.6 Lookups
Pfeil 35.6.7 Exceptions
Pfeil 35.6.8 Keyword Arguments
Pfeil 35.6.9 Alternative Interpreter: PyPy
 
Zum Seitenanfang

35.6    Optimierung Zur vorigen ÜberschriftZur nächsten Überschrift

Als Programmierer sollten Sie mit der Zeit einen Sinn für die Ästhetik und Eleganz eines Programms entwickeln, der Ihnen sagt, wann Ihr Programm »schön« ist. Zwei wichtige Punkte eines eleganten Programms sind die Einfachheit des Ansatzes und die Laufzeiteffizienz. Diese beiden Grundsätze stehen sich in gewissem Maße gegenüber, denn häufig ist der effizienteste Ansatz nicht gerade von Klarheit geprägt.

Aus diesem Grund ist es bei einem nicht-zeitkritischen Programm absolut legitim, Effizienz und Einfachheit gegeneinander abzuwägen und zu einem gesunden Kompromiss zu gelangen. Bei einem zeitkritischen Programm ist die beste Lösung hingegen immer die effizienteste. Wir werden uns in diesem Kapitel mit der Optimierung der Laufzeit eines Python-Programms beschäftigen. Abgesehen von der Laufzeit können Programme noch in Hinblick auf andere Bereiche optimiert werden. So ist es beispielsweise durchaus üblich, eine Speicherplatzoptimierung durchzuführen, und letztlich kann ein Programm auch im Hinblick auf die Einfachheit und Klarheit des Quelltextes hin optimiert werden. Der Begriff »Optimierung« wird im Folgenden ausschließlich im Sinne von »Laufzeitoptimierung« verstanden.

Beachten Sie, dass wir uns dabei rein auf Python-spezifische Optimierungsstrategien konzentrieren. Den höchsten Laufzeitgewinn erzielen Sie jedoch mit der Optimierung der Algorithmik selbst. Doch das Optimieren von Algorithmen ist ein Thema für sich und soll hier keine Berücksichtigung finden. Beachten Sie zudem, dass wir häufig kleinere Python-Codes einander gegenüberstellen werden, die den gleichen Effekt haben, sich jedoch teils gravierend in ihrer Laufzeit unterscheiden. Die in diesem Zusammenhang als »falsch« dargestellte Alternative ist natürlich nicht tatsächlich falsch, sondern im Vergleich zur anderen Variante nur ineffizient. Dennoch führen beide Alternativen zum gesteckten Ziel, sodass Sie frei entscheiden können, welcher der Varianten Sie den Vorzug gewähren möchten.

Bei einigen der hier gezeigten Optimierungsmöglichkeiten, beispielsweise dem »Inlinen« von Funktionsaufrufen oder dem Umgehen von Lookups, ist der absolute Laufzeitgewinn gering und der Verlust an Übersichtlichkeit im Quellcode deutlich. Deswegen sollten Sie solche Optimierungsstrategien nur einsetzen, wenn es unbedingt nötig ist. Beachten Sie auch, dass die angegebenen Laufzeitmessungen beispielhafte Spezialfälle beschreiben, die nicht auf beliebige reale Implementierungen übertragbar sind.

Zu guter Letzt werden wir Ihnen den alternativen Interpreter PyPy vorstellen, der im Gegensatz zum Referenz-Interpreter CPython über einen Just-in-time-Compiler verfügt. Deshalb lassen sich insbesondere algorithmisch aufwendige Programme mit PyPy schneller ausführen als mit CPython.

 
Zum Seitenanfang

35.6.1    Die Optimize-Option Zur vorigen ÜberschriftZur nächsten Überschrift

Grundsätzlich können Sie das Laufzeitverhalten eines Python-Programms beeinflussen, indem Sie es mit der Kommandozeilenoption -O ausführen. Diese Option veranlasst den Interpreter dazu, den resultierenden Byte-Code zu optimieren. Das bedeutet, dass assert-Anweisungen und Konstrukte wie

if __debug__:
mache_etwas()

nicht ins Kompilat aufgenommen werden und somit keinen Einfluss mehr auf das Laufzeitverhalten des optimierten Programms haben.

Durch die Kommandozeilenoption -OO ist es möglich, das Programm über das normale Maß hinaus zu optimieren. Wenn dies gewünscht ist, werden alle im Quelltext enthaltenen Docstrings ignoriert und nicht mit in das Kompilat aufgenommen. Auch damit erreichen Sie ein wenig mehr Laufzeiteffizienz, obwohl sich der Gewinn in Grenzen halten sollte. Es ist dann aber beispielsweise für die Built-in Function help nicht mehr möglich, eine Hilfeseite zu Elementen Ihres Moduls zu generieren, weil keine Docstrings mehr vorhanden sind.

 
Zum Seitenanfang

35.6.2    Mutabel vs. immutabel Zur vorigen ÜberschriftZur nächsten Überschrift

Bei der Einführung der Basisdatentypen zu Beginn dieses Buchs haben wir unterschieden zwischen Datentypen, die mutabel (also veränderlich), und solchen, die immutabel (also unveränderlich) sind. Dass diese Unterscheidung auch performancetechnische Relevanz hat, sehen Sie in diesem Abschnitt.

Im folgenden Beispiel soll ein Tupel mit den Zahlen von 0 bis 1000 gefüllt werden. Selbstverständlich kennen wir dafür effiziente Möglichkeiten, doch versuchen wir es einmal mit der naiven Herangehensweise:

tup = ()
for i in range(1000):
tup += (i,)

Zunächst wird das Tupel angelegt und dann in einer Schleife über alle Zahlen von 0 bis 1000 die jeweils aktuelle Zahl an das Tupel angehängt. Leider haben wir beim Schreiben dieses Codes nicht berücksichtigt, dass es sich bei tuple um einen unveränderlichen Datentyp handelt. Das bedeutet, dass beim Hinzufügen eines Wertes zu unserem Tupel tup jedes Mal eine neue tuple-Instanz erzeugt wird und die Einträge der alten Instanz in diese neue umkopiert werden müssen. Das kostet Zeit.

Als Alternative zu diesem Beispiel verwenden wir nun das veränderliche Pendant des Tupels, die Liste:

lst = []
for i in range(1000):
lst += [i]

Im Gegensatz zur vorherigen Version muss hier beim Anhängen einer neuen Zahl keine neue list-Instanz erzeugt werden, was sich beim Vergleich der Laufzeit der beiden Beispiele deutlich niederschlägt: Die zweite Variante kann ca. 16-mal so schnell ausgeführt werden wie die erste.

Interessant ist in diesem Beispiel auch die Entwicklung des Laufzeitunterschieds in Abhängigkeit von der Anzahl der einzutragenden Zahlen. Je mehr Elemente ein Tupel enthält, desto aufwendiger ist es, seine Elemente in eine neue tuple-Instanz zu überführen. Hätten wir die oben genannten Beispiele also mit Zahlen zwischen 0 und 10000 durchgeführt, hätte sich ein noch um einiges eindrucksvollerer Laufzeitunterschied zwischen den beiden Varianten ergeben.

[»]  Hinweis

Das hier gezeigte Beispiel hat in Python-Versionen vor 2.5 auch für Strings funktioniert. In neueren Versionen greifen hier Optimierungen des Python-Interpreters, die verhindern, dass in jedem Schleifendurchlauf ein neuer String erzeugt wird.

 
Zum Seitenanfang

35.6.3    Schleifen Zur vorigen ÜberschriftZur nächsten Überschrift

Betrachten wir noch einmal das zweite Beispiel aus dem vorangegangenen Abschnitt:

lst = []
for i in range(1000):
lst += [i]

Wenn eine Schleife zum Erzeugen einer Liste, eines Dictionarys oder eines Sets verwendet wird, sollten Sie sich stets überlegen, ob Sie dasselbe Ergebnis nicht auch mit einer List bzw. Dict oder Set Comprehension erreichen können. Diese können schneller ausgeführt werden als eine analoge for-Schleife. Die folgende List Comprehension, die die gleiche Liste wie die oben dargestellte Schleife erzeugt, kann mehr als doppelt so schnell ausgeführt werden:

lst = [i for i in range(1000)]
[»]  Hinweis

Ein Aufruf der Built-in Function map ist ähnlich effizient wie eine List Comprehension.

 
Zum Seitenanfang

35.6.4    Funktionsaufrufe Zur vorigen ÜberschriftZur nächsten Überschrift

Eine weitere – laufzeittechnisch gesehen – teure Angelegenheit sind Funktionsaufrufe, weswegen Sie Funktionsaufrufe in häufig durchlaufenen Schleifen auf ihre Notwendigkeit hin überprüfen sollten. Bei besonders simplen Funktionen kann es sinnvoll sein, die Funktion zu »inlinen«, den Funktionsinhalt also direkt in die Schleife zu schreiben.

Der folgende – zugegebenermaßen etwas künstliche – Code:

def f(s):
return s.upper()
ergebnis = f("Hallo Welt")

kann durch diese mehr als 1,5-mal so schnelle Variante ersetzt werden:

ergebnis = "Hallo Welt".upper()

Insbesondere bei dieser Optimierung ist anzumerken, dass der Laufzeitgewinn durch das »inlinen« von f natürlich in Relation gesehen werden muss zur Laufzeit von f selbst.

 
Zum Seitenanfang

35.6.5    C Zur vorigen ÜberschriftZur nächsten Überschrift

Ein in Python geschriebener Programmteil ist aufgrund des zwischengeschalteten Interpreters in der Regel langsamer als ein vergleichbares C-Programm. Aus diesem Grund sollten Sie an einer laufzeitkritischen Stelle so oft wie möglich auf Algorithmen zurückgreifen, die in C implementiert wurden. So lohnt es sich beispielsweise immer, eine Built-in Function einzusetzen, anstatt den entsprechenden Algorithmus selbst zu implementieren.

In Kapitel 37, »Anbindung an andere Programmiersprachen«, erfahren Sie, wie Sie eigene Module oder Programmteile in C schreiben.

 
Zum Seitenanfang

35.6.6    Lookups Zur vorigen ÜberschriftZur nächsten Überschrift

Wenn über einen Modulnamen auf eine Funktion zugegriffen wird, die in diesem Modul enthalten ist, muss bei jedem Funktionsaufruf ein sogenannter Lookup durchgeführt werden. Dieser Lookup ist nicht erforderlich, wenn eine direkte Referenz auf das Funktionsobjekt besteht. Stellen Sie sich einmal vor, Sie wollten die Quadratwurzeln aller natürlichen Zahlen zwischen 0 und 100 bestimmen. Dazu kommt einem zunächst folgender Ansatz in den Sinn:

import math
wurzeln = [math.sqrt(i) for i in range(100)]

Wesentlich effizienter ist es jedoch, die Funktion sqrt des Moduls math direkt zu referenzieren und über diese Referenz anzusprechen:

import math
s = math.sqrt
wurzeln = [s(i) for i in range(100)]

Die Schleife der zweiten Variante kann ca. 1,5-mal so schnell ausgeführt werden wie die Schleife der ersten Variante.

 
Zum Seitenanfang

35.6.7    Exceptions Zur vorigen ÜberschriftZur nächsten Überschrift

[...]

 
Zum Seitenanfang

35.6.8    Keyword Arguments Zur vorigen ÜberschriftZur nächsten Überschrift

Die Übergabe von Positionsparametern beim Funktions- oder Methodenaufruf ist im Vergleich zu Schlüsselwortparametern grundsätzlich effizienter. Dazu schauen wir uns folgende Funktion an, die vier Parameter erwartet:

def f(a, b, c, d):
return "{} {} {} {}".format(a,b,c,d)

Der Funktionsaufruf

f("Hallo", "du", "schöne", "Welt")

kann ca. 1,1-mal so schnell ausgeführt werden wie der Funktionsaufruf

f(a="Hallo", b="du", c="schöne", d="Welt")

An dieser Stelle möchten wir nochmal darauf hinweisen, dass sich solche feinen Optimierungen nur für Funktionen lohnen, die extrem häufig aufgerufen werden.

 
Zum Seitenanfang

35.6.9    Alternative Interpreter: PyPy Zur vorigen ÜberschriftZur nächsten Überschrift

Die in den bisherigen Abschnitten besprochenen Tipps zur Optimierung beziehen sich auf die Programmiersprache Python selbst und dabei insbesondere im Zusammenhang mit ihrer Referenzimplementierung CPython. Neben CPython gibt es eine Reihe alternativer Interpreter, die jeweils einen besonderen Fokus setzen. Ein im Zusammenhang mit der Optimierung erwähnenswerter Python-Interpreter ist PyPy[ 157 ](PyPy ist übrigens selbst in Python geschrieben. ), der im Gegensatz zu CPython über einen Just-in-Time Compiler (JIT Compiler) verfügt. Ein solcher JIT-Compiler übersetzt besonders häufig ausgeführte Teile eines laufenden Programms in Maschinen-Code, um diese effizienter zu machen. Da die Kompilierung erst zur Laufzeit erfolgt, können Informationen über den aktuellen Programmlauf bei der Optimierung berücksichtigt werden, die bei Ahead-of-Time-Kompilierung nicht einfließen können. Eine Just-in-Time Kompilierung ist besonders bei rechenintensiven Programmen sinnvoll. PyPy steht wie CPython unter der freien MIT-Lizenz und kann unter http://www.pypy.org heruntergeladen werden.

Um die Stärken von PyPy zu demonstrieren, verwenden wir das folgende kurze Beispielprogramm, das die Zahlen von 0 bis 10000000 mit alternierendem Vorzeichen aufsummiert:

ergebnis = 0
for n in range(10000000):
ergebnis += (-1)**n * n

Dieses Beispielprogramm lässt sich mit PyPy ca. 5-mal so schnell ausführen wie mit CPython. Selbstverständlich hängt der Laufzeitgewinn stark von der Art des ausgeführten Programms ab. Ein I/O-gebundenes Programm, beispielsweise eines, das hauptsächlich Festplattenzugriffe durchführt, lässt sich durch Just-in-Time-Kompilierung kaum beschleunigen.

[»]  Hinweis

Die amerikanische Firma Dropbox hat die Entwicklung des quelloffenen Python-Interpreters Pyston gestartet. Der auf der Compiler-Architektur LLVM aufbauende Interpreter erlaubt – ähnlich wie PyPy – eine Just-in-Time-Kompilierung und soll deutlich effizienter werden als CPython.

Pyston ist vergleichsweise jung und bislang noch als experimentell anzusehen. Trotzdem lohnt es sich, ein Auge auf das Projekt zu werfen.

 


Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Python 3 Python 3
Jetzt Buch bestellen

 Buchempfehlungen
Zum Rheinwerk-Shop: Einstieg in Python
Einstieg in Python


Zum Rheinwerk-Shop: Python. Der Grundkurs
Python. Der Grundkurs


Zum Rheinwerk-Shop: Algorithmen mit Python
Algorithmen mit Python


Zum Rheinwerk-Shop: Objektorientierte Programmierung
Objektorientierte Programmierung


Zum Rheinwerk-Shop: Raspberry Pi. Das umfassende Handbuch
Raspberry Pi. Das umfassende Handbuch


Zum Rheinwerk-Shop: Roboter-Autos mit dem Raspberry Pi
Roboter-Autos mit dem Raspberry Pi


Zum Rheinwerk-Shop: Neuronale Netze programmieren mit Python
Neuronale Netze programmieren mit Python


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2020
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern