Python Grundlagen

Worum geht es hier?
Die Aufgaben im Praktikum sollen in der Programmiersprache Python bearbeitet werden. Diese Sprache wird in der Grundvorlesung "Einführung in die Programmierung" eingeführt. Dieses Kapitel dient dazu, einige nützliche Bibliotheken für Python kennenzulernen, die wir im Praktikum ausgiebig nutzen werden. Für Quereinsteiger gibt es auch nochmal eine kurze Zusammenfassung einiger wichtiger Konzepte sowie Links zu verschiedenen Python-Tutorials.


Selbstverständlich ist es auch möglich, die Aufgaben im Praktikum in einer anderen Sprache umzusetzen, z.B. mit JAVA oder C++. Hierfür können wir aber leider keinen Support (Ratschläge, Musterlösungen, Frameworks, etc.) garantieren.


Python und die Bibliotheken
Python ist eine interpretierte Hochsprache). Sie kombiniert Ideen dynamisch typisierter, objektorientierter Sprachen (insbesondere inspiriert vom Klassiker Smalltalk) mit einer leicht zu lesenden Syntax und vielen Features, die die Benutzung der Sprache und das Programmieren damit angenehmer machen. Besonders berühmt ist die Sprache für ihr reichhaltiges Ökosystem an Bibliotheken. Es gibt für fast alles eine Lösung, und die Kultur der Python-Community betont gute Dokumentation und leichte Handhabung. Im wissenschaftlichen Umfeld erfreuen sich die SciPy und die SciKit Bibliotheken besonderer Beliebtheit; zwei davon (NumPy, MatPlotLib) werden wir uns hier anschauen, viele andere (z.B. scikit-learn) laufen Ihnen im weiteren Studiums mit Sicherheit auch noch über den Weg.


In Python geschriebene Programme kann man sowohl in der Konsole ablaufen lassen, als auch mit interaktiven graphischen Benutzerschnittstellen ausstatten. Letzteres ist für unsere Veranstaltung wichtig, da wir uns graphische Objekte interaktiv anschauen wollen (daher müssen wir uns die dazu nötigen Bibliotheken näher anschauen; wir werden hier Qt für Python benutzen, welches sehr mächtig, ausgereift und weit verbreitet ist).


In diese Kapitel wollen wir uns insbesondere die Python-Bibliotheken anschauen, die für unsere Veranstaltung besonders wichtig sind. Dies umfasst vor allem die Bibliotheken Numpy (für effiziente Numerik), Matplotlib (zum Plotten von Graphen und zur Visualisierung von Daten) und, wie gesagt, die GUI Bibliothek Qt, hier in Form von PyQt5.


Alle diese Bibliotheken sind schon einzeln sehr umfangreich; unser kurzes Kapitel gibt nur einen kurzen Überblick mit dem Wichtigsten, um die Aufgaben bearbeiten zu können. Es gibt natürlich noch viel mehr Möglichkeiten - nehmen Sie ruhig die offiziellen Dokumentationen oder die verlinkten Tutorials / Quick-References zur Hand.


Anmerkung: Wir konzentrieren uns hier auf die aktuell neueste Version von Python (python3.6). Falls Sie bereits mit python2.7 vertraut sind (eine eigentlich überholte Version, welche aber von manchen Python Anwendern immer noch bevorzugt wird), können Sie auch diese Version verwenden (sprechen Sie dies aber kurz mit Ihrem Übungsleiter ab).


Python Basics & Installation

Los geht es mit Python. Zunächst: In dem Kasten unten gibt es einige Links und Quellen zum Lernen oder Auffrischen von Python Grundlagen. Für alle, die schon einmal eine objektorientierte Sprache benutzt haben, sollte es sehr einfach sein, Python zu lernen.


Nützliche Quellen

Entwicklungsumgebungen:

Falls Sie sich eine Entwicklungsumgebung für Python wünschen, gibt Ihnen folgende Liste einige IDEs vor, die Ihnen bei der Arbeit mit Python behilflich sein könnten.

Tutorials:

Die folgenden Quellen können sowohl zum Erlernen der Sprache Python, als auch als Wiederholung dienen. Insbesondere enthalten die Links auch die Installation und Verwendung von Python auf den meisten Betriebssystemen.

Cheat-Sheets (keine Erklärungen):

Um schnell etwas nachzuschlagen, empfehlen wir Ihnen einen Blick auf diese Kurzübersichten zu werfen:

Import
Ein Python Programm beginnt in der Regel damit, dass wir Pakete (Bibliotheken) einbinden, die wir später benötigen. Der folgende Code bindet z.B. die NumPy-Bibliothek ein:
import numpy as np

Erklärung: np ist hierbei nur ein Name, den Sie selbst wählen können. Nachdem Sie Numpy importiert haben, können Sie Numpy-Funktionen mit dem Prefix np benutzen. Falls Sie as np weglassen wird numpy selbst als Name gewählt. Im Folgenden wird davon ausgegangen, dass Sie Python unter dem Namen np importiert haben.

Importumbenennung

Nehmen wir an, Sie wissen bereits, dass Sie eine Funktion, wie etwa np.sin (die Sinus-Funktion in der Numpy-Umgebung) oder np.exp häufig verwenden werden. In dem Fall können Sie auch eine Kurzform hierfür anlegen:
from numpy import sin, exp
# nutzt eigentlich np.sin bzw. np.exp
print(sin(0) + exp(0))

aber auch umbenennen:
from numpy import sin as sinus, exp
# nutzt eigentlich np.sin bzw. np.exp
print(sinus(0) + exp(0))

Achtung: Denken Sie immer daran, welche Funktion Sie gerade verwenden! Beispielsweise hat auch das math-Paket in Python eine sin-Funktion. Auch wenn der Import ohne Suffix bequem ist, kann dies in größeren Programmen zu Problemen führen. Verwenden Sie daher diese Komfortfunktionen mit Bedacht; das kann sonst zu einer lästigen Fehlersuche führen. Bei nicht-trivialen Programmen ist es in der Regel sauberer, auf Umbenennung bzw. unqualifizierten Import (ohne Prefix) zu verzichten.

Main-Methode
In Python können Sie Ihren Code einfach in eine .py-Datei schreiben:
# erst nötige Pakete
import math

# Hilfsmethoden 
def function1(a):
	return a, a**2, a**3

# Main-Methode (wird direkt Ausgeführt)
test = 42
print(function1(test))

Dieser Ansatz ist für kleinere Skripte vollkommen ausreichend, führt aber schnell zu Problemen, wenn Sie die eine Unterfunktion aus einer anderen Datei importieren möchten. In dem Fall wird nämlich der Hauptteil des Skriptes mit ausgeführt. Aus diesem Grund an dieser Stelle die Empfehlung, den Hauptteil Ihres Codes entweder folgendermaßen zu kapseln:
# erst nötige Pakete
import math

# Hilfsmethoden 
def function1(a):
	return a, a**2, a**3

# Main-Methode (wird nur Ausgeführt, wenn Script direkt ausgeführt wird)
if __name__ == "__main__":
	test = 42
	print(function1(test))

Falls Sie das möchten, können Sie auch eine main-Methode definieren und diese explizit aufrufen.
# Erst die nötigen Pakete
import math
import sys

# Hilfsmethoden 
def function1(a):
	return a, a**2, a**3

# Der Name main ist dabei beliebig. (`argv} ist dabei die Liste aller 
# Kommandozeilenargumente an das Skript; der Name ist auch beliebig).
# Der zusätzliche Parameter argv ist selbstverständlich optional - 
# es können auch andere Parameter definiert werden.
def main(argv):
	print(argv)
	test = 42
	print(function1(test))

# Main-Methode (wird nur Ausgeführt, wenn Script direkt ausgeführt wird)
if __name__ == "__main__":
	main(sys.argv)

Python Numpy

Nützliche Quellen zu Numpy

Numpy Arrays
Numpy ist ein Python-Paket, das effiziente Operationen mit (großen) Arrays von numerischen Daten unterstützt. Python als Skriptsprache ist sehr langsam. Bei numerischen Operationen, bei denen große Vektoren mit vielen Einträgen bearbeitet werden sollen, fällt dieser Nachteil besonders ins Gewicht. Das Numpy Packet ist daher in C++ geschrieben (was an sich schon durchaus oft 100x schneller ist; zusätzlich stehen low-level Optimierung wie Cache Optimierungm, und Vektorisierung/SIMD Instruktionen in C++ zur Verfügung). Der Trick bei NumPy ist nun, die eigentlichen Rechenoperationen in Bibliotheksfunktionen (hand-optimiertes C++) auszulagern und nur wenige solche Aufrufe in Python durchzuführen. Gelingt dies, erhält man eine exzellente Performance.
Ein Beispiel
Schauen wir uns das Quadrieren der Einträge einer Liste in reinem Python an:
from math import sin

# normale Python-Liste
a = [1,2,3,5,10,20]

# a^2
aSqr = [ i**2 for i in a ]

# sin(a^3) + a^2 
a2 = [ sin(i**3) + i**2 for i in a ]
Und nun der entsprechende Code ohne Python-for-Schleifen mithilfe von Numpy:
# normale Python-Liste
a = [1,2,3,5,10,20]

# konvertieren zu einem Numpy-Array
b = np.array(a)
# alternativ: b = np.array([1,2,3,5,10,20])

# b^2
bSqr = np.square(b)
# oder
bSqr = b**2

# b^3 + b^2
b2 = np.sin(b**3) + b**2

In diesem Winzigbeispiel (sechs Elemente) wird man natürlich kaum einen Unterschied bemerken. Hat die Liste aber eine Millionen Einträge, wird der Geschwindigkeitsunterschied sofort sehr spürbar. In Anwendungen wie wissenschaftlicher Datenanalyse oder maschinellem Lernen, in denen großen Datenmengen verarbeitet werden müssen, ist dies der Schlüssel zu guter Performance (und ein wesentlicher Grund für den Erfolg von Python in diesen Anwendungsfeldern).

Dynamische Statische Typisierung: dtype in NumPy
Warum ist Pyhton so aberwitzig langsam? Ein wesentlicher Grund ist die dynamische Typisierung. Jedes Datenobjekt (auch jede einzelne Zahl, mit der gerechnet wird) ist ein "Objekt" im Sinne objektorientierter Programmierung und alle Operationen darauf (z.B. Addieren von zwei Zahlen) werden mittels "dynamic dispatch" aufgerufen. Das macht die Sprache extrem flexible, aber auch extrem langsam. Sprachen wie ADA/C/C++/Pascal/Rust/Haskell und zu einem gewissen Grade auch JAVA/C#, sowie viele andere, vermeiden dies durch statische Typisierung. Beim Übersetzen steht schon genau fest, welche Datentypen verwendet werden, wieviel Speicher diese belegen und meistens auch genau, welche Operationen wie durchgeführt werden sollen. Das ist sehr unflexibel, erlaubt aber weitgehende Optimierungen.
Numpy nutzt nun eine Mischung dieser Konzepte: Die Datentypen können zur Laufzeit bestimmt werden (das macht ja Python gerade so flexibel - man muß nicht alles neu kompilieren, wenn man eine neue Art von Daten verwenden möchte), sobald aber die Daten angelegt sind, ist die Struktur eines NumPy-Datenobjektes erstmal fix und teuere Operationen (dispatches, offsets, etc.) können vorberechnet werden. Damit sind wir genau praktisch so schnell wie die statisch typsisierten Sprachen, aber so flexibel wie die dynamisch typsierten (ein Vorteil, den wir trotzdem verlieren, ist daß der Compiler uns nicht (so gut) bei der Fehlersuche helfen kann; statische Typsierung ist also damit nich obsolet geworden; es ist nur ein anderer Kompromis).
Also: Wenn wir von der Geschwindigkeit von C profitieren wollen, folgt, dass wir beim Rechnen mit Numpy-Arrays einen internen Basistypen festlegen müssen, bevor das eigentliche Rechnen beginnt. Wenn Sie also np.array(...) verwenden, versucht Numpy daraus zu schließen, welcher Datentyp gemeint ist. Die Datentypen, die in unserem Zusammenhang interessant sein werden sind: np.int64, np.float64, np.complex128, np.bool. Wichtig ist an dieser Stelle nur, dass Sie sich im Vorfeld bewusst machen wofür Sie die Variable benutzen werden.
Fließkommazahlen
Benötigen Sie Fließkommazahlen? Dann verwenden Sie z.B. np.float32 (einfache Genauigkeit, 32bit) oder np.float64 (doppelte Genauigkeit, 64bit - Standard). Bei Fließkommazahlen muß man immer im Kopf haben, daß Ergebnisse von Fließkommaoperationen in der Regel nur approximativ sind und schnell zu numerischen Fehlern führen. Das heißt z.B., daß es fast nie Sinn macht, Fließkommazahlen auf Gleichheit (==) zu prüfen; statt dessen sollte man prüfen, ob diese stärker voneinander abweichen (Differenz kleiner als \(\epsilon\), z.B. mit \(\epsilon=10^{-6}\)) (wie groß die Toleranz \(\epsilon\) sein kann, hängt vom Kontext ab; oft ist die wahl solcher Parameter gar nicht so einfach).

Achtung: Bei der Konvertierung von float zu int wird abgerundet, int/float zu complex wird nur reell, Zahl zu Bool wird True, falls ein Bit nicht 0 ist (also für alle Zahlen ungleich 0).
a = np.array([0,1,2])
a.dtype # enthält den typ np.int64

# typ ändern
a.astype(np.float64)

# typ kann sich bei bestimmten Rechnungen ändern
(a/2).dtype # = float64
(a//2).dtype # = int64 (gerundet)

Zugriff & Slicing
Der Zugriff auf eindimensionale Arrays funktioniert genauso wie der Zugriff auf Python-Listen. (Falls Sie mithilfe einer Liste zugreifen, werden die Einträge kopiert - siehe auch Abschnitt über Array-Kopien und das Filtern von Arrays!)
a = np.array(range(8))**3

# Eingabe                # Ausgabe
# ---------------------- # -----------------------------------------------
a                        # array([  0,   1,   8,  27,  64, 125, 216, 343])
len(a)                   # 10
a[4]                     # 27
a[2:4]                   # array([ 8, 27]) (Beachte: rechts-exklusiv)
a[2:4] = [-10,2]         #
a[5:10] = np.sin(a[4:9]) # es muss nur die Länge übereinstimmen
a                        # array([ 0,  1,-10,  2, 64,  7, 13, 22, 35, 52])
a[[0,5,2]]               # array([ 0,  125,  8]) (Dies kopiert die Einträge des Arrays!!!)
cond = [False, True, True, False, False, True, True, False]
a[cond]                  # array([ 1,  8,  125, 216]) (Dies kopiert die Einträge des Arrays!!!)

Filtern
Es ist auch möglich mit einem Teil der Daten weiterzurechnen: Sie müssen dazu nur eine bool-Liste in die Listenklammern setzen.
a = np.array(range(8))**3
#   = array([  0,   1,   8,  27,  64, 125, 216, 343, 512, 729])
quersumme = a//100 + (a%100)//10 + a%10
#   = array([ 0,  1,  8,  9, 10,  8,  9, 10,  8, 18])

# wir wollen die Zahlen aus a, die eine gerade quersumme haben
a[quersumme % 2 == 0]
#   = array([  0,   8,  64, 125, 343, 512, 729])

# sie können natürlich auch den gefilterten Teil wie oben manipulieren
a[quersumme % 2 == 0] = 0

Achtung: Beachten Sie, dass solche Filter-Numpy-Operationen in-place auf dem ursprünglichen Objekt agieren! Betrachten Sie dieses Beispiel:
a = np.array(range(8))**3
#   = array([  0,   1,   8,  27,  64, 125, 216, 343, 512, 729])

# wir manipulieren immer einen Teil von a!
b = a[0:4]    # = array([0, 1, 8, 27])
b[0] = 100
b             # = array([100, 1, 8, 27])
a             # = array([100,   1,   8,  27,  64, 125, 216, 343, 512, 729])

# dies gilt auch für die X=-Operatoren
b *= 2
b += 2
# ...

# Dies ändert a jedoch nicht. Warum?
b = b*1       # alternativ: b = a[0:4]*1 oder b = a[0:4]+0
b[0] = 123

Array-Kopien
Wie kopiert man also ein Numpy-Array sauber?
a = ...         # wie vorher
b = np.array(a) # vollständige Kopie!

# falls b schon existiert und die selbe Größe hat wie a wird 
np.copyto(a,b)
# schneller sein

# es funktioniert zwar wie oben gezeigt auch
b = 1*a
# Beachten Sie, dass arithmetische Operationen zur Änderung des internen 
# Datentyps führen können. Für beliebige Datentypen von a sollten Sie also diese
# Option der Erweiterbarkeit Ihres Codes halber nicht verwenden!

Löschen eines Eintrages
a = ...         # wie vorher
np.delete(a,3)  # löscht das Element an der 3. Stelle (ist eine Array Kopie!)

Mehrdimensionale Arrays & Reshape
Das Besondere an Numpy ist, dass es neben schneller vektorisierten Rechnungen auch mit mehrdimensionalen Arrays, also Matrizen (2-dimensionale Arrays/Listen) oder Tensoren (n-dimensionale Arrays/Listen) umgehen kann. Unter anderem ist die Matrix-Vektor-Multiplikation bereits fest integriert. Sie können aber auch ein beliebig-dimensionales Array erstellen und dann, wie vorher komponentenweise (pro Eintrag), rechnen.
An dieser Stelle soll die Mathematik in den Hintergrund rücken. Wir betrachten dieses Feature als die Möglichkeit mit einem Werte-Gitter bzw. einem 2-dimensionalen oder geschachtelten Array zu rechnen.
a = np.array([[1,2],[3,4],[5,6]])
a
# = array([[1, 2],
#      [3, 4],
#      [5, 6]])
a.shape     # gibt die Form von a aus
# = array([3, 2])

# Ändern der Form
a.reshape([2,3])
# = array([[1, 2, 3],
#          [4, 5, 6]])

# Zusammenpressen zu einem Vektor
a.reshape([6])
# oder
a.reshape([-1])
# oder
b = a.reshape(-1)
# = array([[1, 2, 3, 4, 5, 6]])

# reshape kann man immer Rückgängig machen!
b.reshape([3,2]) # == a

# -1 in Reshape gibt an, welche Dimension "passend" gestreckt werden soll.
# beide Arrays sind nach der Operation gleich:
a.reshape([-1, 3])
a.reshape([2, -1])
 
# Rechenoperationen funktionieren Komponentenweise wie vorher auch:
b = a**5 + a**2
# ...

Interessanter ist an dieser Stelle der Zugriff. Hier ist es nämlich auch möglich, eine Spalte oder eine Zeile zu extrahieren. (Achtung: Wieder werden beim Zugriff nur Referenzen auf das ursprüngliche Array zurückgegeben):
a[2,1]	# = 6
a[:,0]	# = array([1,3,5])
a[:,1]	# = array([2,4,6])
a[0,:]	# = array([1,2])
a[1,:]	# = array([3,4])
a[2,:]	# = array([5,6])
a[:,:]	# wieder das ganze Array (jedoch keine Kopie!)

Alles was Sie mit eindimensionalen Arrays machen können, überträgt sich auch auf mehrdimensionale Arrays.
a = np.array([[1,2],[3,4],[5,6]])**3
# = array([[  1,   8],
#      [ 27,  64],
#      [125, 216]])
quersumme = a//100 + (a%100)//10 + a%10
a[quersumme % 2 == 0]
# = array([  8,  64, 125])
# Beachten Sie, dass diese Operation immer einen Vektor ergibt.


Mathematische Funktionalitäten
Mathematische Funktionen
# Absolutbetrag
np.abs(a)

# Vorzeichen
np.sign(a)   # -1 for a[i]<0, sonst 1 (0 für a[i]==0)

# Runden
np.round(x)  # zum nächsten Integer
np.floor(x)  # zum nächsten niedrigen Integer
np.ceil(x)   # zum nächsten höheren Integer

# Trigonometrische Funktionen
np.sin(a)
np.cos(a)

# Exponentialfunktion & Logarithmus
np.exp(a)
np.log(a)
np.exp2(a)   # Komponentenweises 2^a

Summen und Produkte
Wenn in einer Formel ein Summenzeichen steht, kann dies schnell zu einigen verschachtelten for-Schleifen führen. In Numpy is dies sehr übersichtlich:
# Summen
np.sum(a)        # Gesamt-Summe (ergibt eine Zahl)
np.sum(a,axis=0) # Spalten-Summe (ergibt einen Vektor)
np.sum(a,axis=1) # Zeilen-Summe (ergibt einen Vektor)

# Produkte
np.prod(a,...)   # dasselbe gilt auch für das Produkt

Äußeres Produkt
Gegeben sind zwei 1D-Arrays (Vektoren). Das äußere Produkt definiert sich als die zwei-dimensionale Liste/die Matrix aller Produkte des einen Arrays mit den Elementen des anderen Arrays.
np.outer(v1,v2)
# z.B. für len(v1)==3 und len(v2)==3
#  = array([[v1[0]*v2[0], v1[0]*v2[1], v1[0]*v2[2]],
#           [v1[1]*v2[0], v1[1]*v2[1], v1[1]*v2[2]],
#           [v1[2]*v2[0], v1[2]*v2[1], v1[2]*v2[2]]])


Initialisierungsmethoden
Mit Nullen oder Einsen Füllen
Sie können die Form (shape) des Arrays auch zur Initialisierung verwenden:
# mit Nullen gefüllt:
np.zeros([10,2]) # zweidimensionales Array mit zehn Zeilen und zwei Spalten
# mit Einsen gefüllt:
np.ones([10,2]) # zweidimensionales Array mit zehn Zeilen und zwei Spalten
# mit beliebiger anderer Zahl füllen
np.zeros([10,2]) + 1234

arange
np.arange([start,] stop, [schritt]
a = np.arange(7)      # a = array([0, 1, 2, 3, 4, 5, 6])
b = np.arange(2,8)    # b = array([2, 3, 4, 5, 6, 7])
c = np.arange(2,10,3) # c = array([2, 5, 8])

Gitter
# 1-D Gitter (gleichmäßig Abgetastet: von a bis b mit c Stützstellen (b enthalten)
a = np.linspace(a,b,c)

# 1-D Gitter (gleichmäßig Abgetastet: von a bis b mit c Stützstellen (b nicht enthalten)
a = np.linspace(a,b,c, endpoint=False)
	
# 1-D Gitter (gleichmäßig Abgetastet: von a bis b mit Abstand c
a = np.arange(a,b,c)

# 1-D Gitter (gleichmäßig Abgetastet: von a bis b mit Abstand 1
a = np.arange(a,b)

2-D Gitter
a,b # jeweils 1-D Gitter

# Matrix aller kombinatorischen Summen von Elementen aus a mit denen aus b
# (für nähere Erklärungen: suchen Sie nach Numpy-Broadcasting)
a.reshape([1,-1]) + b.reshape([-1,1])

# Matrix aller kombinatorischen Summen, wobei der zweite Teil Imaginär ist
# (entspricht 2-D Gitter auf der komplexen Zahlenebene)
a.reshape([1,-1]) + b.reshape([-1,1])*1j

Zufallszahlen
Einzelne Zahlen können mit Numpy in folgender Weise erzeugt werden:
# Zufallszahl zwischen 0 und 1
afloat = np.random.rand()

# ganze Zufallszahl zwischen 5 und 10 (nicht eingeschlossen!)
aint = np.random.randint(5,10)

Manchmal werden jedoch auch zufällige Numpy-Arrays benötigt:
# Anzahl zu erzeugender Zufallszahlen
num = 10
afloat = np.random.rand(num)
aint = np.random.randint(5,10, num)


Vermischen eines Datensatzes (Zeilenweise)
datensatz = ... # mehrdimensionales Array (ein Datenpunkt pro Zeile)

# Zufallige Indices bestimmen
indices = np.random.permutation(len(datensatz))
vermischt = datensatz[indices]

Laden von DSV-Dateien
Delimiter Seperated Values (DSV) bezeichnen Tabellen, deren Werte durch eine bestimmte Zeichenfolge getrennt werden. Häufig sieht man das Format Komma (,) als Werte-Trenner (Delimiter), und den Zeilenumbruch (\n) als Zeilentrenner. Zum Beispiel könnte eine CSV-Datei (mit dem Namen test.csv) so aussehen.
Name, Wert2, Wert3, Wert4
abc, 0.33, 0.5, 1234 
abc, 0.04, 1.6, 461 
abc, 0.1, 9.2, 873 
...

Alternativ werden die Werte auch oft durch Tabs oder allgemeine Whitespaces (Leerraum-Symbole wie Leerzeichen, Tab, usw.) getrennt. In diesem Fall haben die Dateien oftmals die Dateiendung (.tsv).
Im Allgemeinen kann jedes Trennzeichen verwendet werden. Diese Unterteilung ist nicht die Einzige: Manche Formate fangen nicht in der ersten Zeile an, sondern liefern erst eine Beschreibung des Datensatzes. Andere Formate haben in der ersten Zeile einen "Tabellenkopf", der die Namen der Spalten enthält (wie etwa oben im Beispiel) und nicht selten unterscheiden sich die Formate der Spalten (ganze Zahlen in der einen Spalte, boolesche Werte in der zweiten Spalte, ...). Um diese Möglichkeiten abzudecken, haben die Funktionen, die DSV-Dateien einlesen können, viele Parameter. Hier eine Aufführung einiger davon, die Ihnen bei der Bearbeitung der Aufgaben behilflich sein werden:
# Einlesen der Datei mithilfe von Numpy
array2d = np.loadtxt("dateiname.txt", ...)

# für ... können wir die folgenden optionalen Parameter einsetzen
dtype = np.int64 # für Ganzzahlen oder etwa np.float64 für Fließkommazahlen
skiprows = 12    # Überspringe 12 Zeilen bevor der eigentliche Datensatz anfängt
delimiter = ","  # (Standardmäßig auf alle Whitespace-Zeichen gestellt)
comments = "#"   # Symbol, das angibt, wie Kommentare eingeleitet werden sollen

Python Matplotlib

Nützliche Quellen zu Matplotlib

Importieren von Matplotlib
Zuerst Matplotlib in Ihr Programm einbinden. Wie bei Numpy auch nehmen lassen wir in den Beispielen dieses Skriptes den folgenden Import weg.
import numpy as np
import matplotlib.pyplot as plt

Darstellung von Funktionen oder Punkten
Eine wichtige Funktion von Matplotlib ist die Funktion plot. Mit ihr können wir schnell eine Menge von Punkte-Paaren darstellen lassen.
# x- und y-Koordinaten
X = [0,1,5,10,3,6]
Y = [2,2,8,2,3,0]

# Zeichnen und Anzeige
plt.plot(X, Y)
plt.show()

Das erste Argument von plot erwartet dabei einer Liste von x-Koordinaten, das zweite Argument die zugehörigen y-Koordinaten.

Arbeiten mit Numpy
Praktischerweise können statt Listen auch Numpy-Arrays eingegeben werden. Das praktische daran ist, dass man nun Vektorisiert rechnen kann.
X = np.linspace(0,10, 250)
Y = np.cos(X)*np.cos(2*X)+np.sin(X)

# Zeichnen und Anzeige
plt.plot(X, Y)
plt.show()


Darstellungsoptionen
Natürlich wollen wir etwas mehr Kontrolle darüber haben wie die Daten angezeigt werden. Dazu gehört die Farbe und die Darstellung der Punkte selbst. Die Funktion plot erlaubt ein weiteres (optionales) Argument, das die Farbe und die Form der Punkte bestimmt.
...
plt.plot(X, Y, '.')	   # Punkte, nicht verbunden
plt.plot(X, Y, 'b.')   # Punkte, nicht verbunden, blau
plt.plot(X, Y, 'ro')   # große Punkte, nicht verbunden, rot
plt.plot(X, Y, 'k--')  # gestrichelte Linie, schwarz
...

In der Dokumentation zu der Funktion plot können Sie mithilfe der Tabellen alle Kombinationen von Darstellungen und Farben entnehmen, die sie dem dritten Argument übergeben können. Weitere nützliche benannte Argumente sind auch unter dem angegebenen Link erklärt: linestyle (Linientyp), linewidth (Linienbreite), markerfacecolor (Punktfarbe), marker (Punktstil), markersize (Punktgröße), color (Farbe), alpha (Transparenz).
Beispielsweise können Sie als Farbwert auch Hex-Werte oder 3er (rot,grün,blau) bzw. 4er Tupel (rot,grün,blau,transparenz) angeben:
plt.plot(X, Y, color='#eceffa')
plt.plot(X, Y, color=(0.5,0.2,1))       # Werte jeweils zwischen 0 und 1
plt.plot(X, Y, color=(0.5,0.2,1,0.5))   # ... halb durchsichtig
...

Achsen
Um einen Teil des Plots anzuzeigen können sie mit xlim und ylim den Abschnitt des Koordinatensystems angeben.
...
plt.plot(x, y, 'o')
plt.xlim([-5,3]) # zeichne nur Punkte zwischen -5 und 3 (x-Achse)
plt.xlabel('x-Achse') # Achsen-Name
plt.ylim([0,40]) # zeichne nur Punkte zwischen 0 und 40 (y-Achse)
plt.ylabel('y-Achse') # Achsen-Name
plt.show()

Horizontale und Vertikale Linien
Mit axhline und axvline können Sie horizontale bzw. vertikale Linien Ihrem Plot hinzufügen.
X = np.linspace(0,10, 250)
Y = np.cos(X)*np.cos(2*X)+np.sin(X)
plt.plot(X, Y, 'o')
plt.axhline(0.5, color="b", alpha=0.5)
plt.axvline(0.5, color="b", alpha=0.5)
plt.show()

Mehrere Plots in einem Diagramm und Legende
Selbstverständlich können Sie auch mehrere Plots übereinander legen, wobei im einfachsten Fall beide das gleiche Koordinatensystem verwenden. (Die Darstellungsoptionen können für beide Plots seperat eingestellt werden).
x = np.linspace(0,2*np.pi, 250)
y = np.cos(x)
y2 = np.sin(np.cos(x)*x)

# Zeichnen und Anzeige
plt.plot(x, y, linewidth=3, label="das ist cos(x)")
plt.plot(x, y2, label="das ist sin(cos(x)*x)")
plt.plot(x, y2,"b.",markersize=1)
plt.legend()
plt.show()

Mehrere Plots mit verschiedenen Achsen
x = ...
y = ...
y2 = ...

# erste Achse
fig, ax1 = plt.subplots()
# statt plt.plot nun ax1.plot
ax1.plot(x, y, 'b-')

# aus xlabel und xlim wird set_xlabel und set_xlim
ax1.set_xlabel('Punkte')

# Färben der y-Achse
ax1.tick_params('y', colors='b')

# zweite y-Achse (mit der gleichen x-Achse)
ax2 = ax1.twinx()
ax2.plot(x, y2, 'r.')

Mehrere Plots in mehreren Diagrammen - Subplots
Selbstverständlich können Sie auch mehrere Diagramme nebeneinander legen. Die Funktion subplots erweitert dabei drei Ziffern; die erste gibt die Anzahl der Reihen an, die zweite die Anzahl der Spalten, und die letzte den Index des aktuellen Subplots im so definierten Gitter.
x = np.linspace(0,2*np.pi, 250)
y = np.cos(x)
y2 = np.sin(np.cos(x)*x)

# Zeichnen und Anzeige
fig = plt.figure()
ax_obenlinks   = fig.add_subplot(321)
ax_mitterechts = fig.add_subplot(324)
ax_obenlinks.plot(x, y)
ax_obenlinks.plot(x, x)
ax_mitterechts.plot(x, y2)
plt.show()

Bild mit Matplotlib visualisieren
import matplotlib.pyplot as plt
from PIL import Image

# Bild einlesen (bild.png ist im selben verzeichnis, wie diese Datei enthalten)
# convert("L") übersetzt das bild in ein Graustufenbild (nur ein Kanal, statt dreien)
image = Image.open("bild.png").convert("L")

# konvertieren in ein numpy-array
img = np.asarray(image)

# Anzeigen (in Graustufen)
fig, ax = plt.subplots()
ax.imshow(img, cmap='gray')
plt.show()

Animieren mit Matplotlib
from numpy import sin, cos
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

fig, ax = plt.subplots()
ax.set_xlim([-5,5])
ax.set_ylim([-5,5])

# folgende objekte sollen wie gezeichnet werden
curve, = ax.plot([],[],"-", color="blue")
origin = ax.plot(0,0,"x", color="red")
pos, = ax.plot([],[],"o", color="black")
text = ax.text(0.1, 0.2, '', transform=ax.transAxes)

# wir merken uns alle positionen
curve_pos = [[],[]]

# berechnnung der neuen position
t = np.linspace(0,100, 5000)
a = np.exp(np.cos(t))-2*np.cos(4*t)-np.sin(t/12)**5
curve_pos = [np.sin(t)*a, np.cos(t)*a] #x- und zugehörige y-Werte

# initialisierung der animation (startbild)
def init():
    curve.set_data(curve_pos[0], curve_pos[1])
    pos.set_data([],[])
    text.set_text('')
    return pos,text

# animationsschritt
def step(i):
    pos.set_data([curve_pos[0][i]],[curve_pos[1][i]])
    text.set_text("Schritt "+str(i))
    return pos,text

# interval gibt an, wie lange gewartet wird in ms
# blit=True (nicht alles wird sets neu gezeichnet)
# drittes argument gib an wie sich der index i in step(i) verhält
ani = animation.FuncAnimation(fig, step, np.arange(1, len(t)),
                              interval=25, blit=True, init_func=init)

# als film speichern (mit 15 fps)
# ani.save('butterfly.mp4', fps=15)

# anzeigen
plt.show()

Matplotlib in PyQt
Es folgt ein einfaches Beispiel, das einen Matplotlib-Plot in ein PyQt-Fenster einbettet. (Es ist dabei nicht nötig eine Klasse zu implementieren.)
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
# from PyQt5 import QtCore as qc

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
import numpy as np

class PlotWindow(qw.QDialog):
    def __init__(self, parent=None):
        super(PlotWindow, self).__init__(parent)

        # das Diagramm auf dem wir zeichnen
        self.figure, self.axis = plt.subplots()

		# FigueCanvas ist ein qt-Widget, das das Diagramm anzeigen kann
        self.canvas = FigureCanvas(self.figure)

        # die Matplotlib-NavigationsLeiste
        self.toolbar = NavigationToolbar(self.canvas, self)

		# Layout (wie Sie es bereits kennen)
        self.button = qw.QPushButton('Plot')
        self.button.clicked.connect(self.plot)
        layout = qw.QVBoxLayout()
        layout.addWidget(self.toolbar)
        layout.addWidget(self.canvas)
        layout.addWidget(self.button)
        self.setLayout(layout)

	# Die Plot-Funktion kann nun wie vorher definiert werden:
    def plot(self):
        x = np.linspace(0,10, 250)
        y = np.cos(x)*np.cos(2*x)+np.sin(x)

        # Zeichnen und Anzeige
        self.axis.plot(x, y)

        # Achtung: keine plt.show!
        # (Neu-)Zeichnen des Canva
        self.canvas.draw()

if __name__ == '__main__':
    app = qw.QApplication(sys.argv)
    main = PlotWindow()
    main.show()

    sys.exit(app.exec_())

Python PyQt5

Qt ist ein Toolkit, das dabei hilft, Programme (und insbesondere Programmoberflächen) plattformübergreifend zu entwickeln. Bei der richtigen Verwendung von Qt kann Ihr Programm nicht nur auf Windows und gängigen POSIX-Systemen, sondern mittlerweile auch auf Android, iOS, Windows Phone und eingebetteten Systemen ausgeführt werden.


In dieser Übersicht soll ein kleiner Teil von Qt (in seiner aktuellen Version Qt5.8) vorgestellt werden, der für die Lösung der Aufgaben ausreichen sollte.


Anmerkung: Qt ist eigentlich ein C\+\+-Framework. Hier verwenden wir zwar die Verbindung zu Python (PyQt), Dokumentationen existieren leider vorwiegend für C++. Die Verwendung ist aber bis auf die Sprach-Syntax konsistent. Falls Sie also die Funktionalität einer Klasse genauer inspizieren möchten, können Sie die offizielle Qt-Dokumentation zur Hand nehmen.


Nützliche Quellen zu PyQt5

Dokumentation & Cheat Sheet:

Tutorials:

Einbinden von Qt in Python
Um PyQt verwenden zu können, müssen Sie zunächst - wie beim Numpy-Paket auch - die nötigen Klassen und Methoden importieren. In Qt werden Sie sehr viele Unterklassen benötigen.
Häufig verwendete Module
  • QtCore enthält alle nicht-GUI-Klassen, die auch von den anderen PyQt-Modulen verwendet werden.
  • QtWidgets enthält GUI-Komponenten, um klassische UI-Fenster zu strukturieren. (Buttons, Fenster, Textfelder, etc.)
  • QtGui enthält auch GUI-Komponenten, jedoch auf einer etwas niedriger liegenden Ebene: Bilder, 2D-Graphik, Schrift, Event-Handling

In den folgenden Abschnitten werden wir die wichtigsten Unterklassen einführen, die Sie zur Bearbeitung der Aufgaben benötigen.
Nun zum Importieren von Qt in Python: Um einerseits auf alle Klassen und Methoden zugreifen zu können, empfehlen wir Ihnen eine der beiden folgenden Möglichkeiten zu verwenden:
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc

# Verwendung später:
qw.QKlasseAusQtWidgets(...)
qg.QKlasseAusQtGui(...)
qw.QKlasseAusQtCore(...)
Alternativ können Sie per
import sys
from PyQt5.QtWidgets import QKlasseAusQtWidgets
from PyQt5.QtGui import QKlasseAusQtGui
from PyQt5.QtCore import QKlasseAusQtCore

# Verwendung später:
QKlasseAusQtWidgets(...)
QKlasseAusQtGui(...)
QKlasseAusQtCore(...)

nur die nötigen Klassen importieren. Beide Stile sind wünschenswert für sauberen Code.
Beachten Sie, dass Sie im ersten Fall bereits auf alle Klassen zugreifen können, im zweiten Fall ersparen Sie sich die Präfixe (qw., qg. und qc.).
Achtung! Um die folgenden Beispiele zu vereinfachen, gehen wir davon aus, dass sie die erste Möglichkeit verwenden.

Verwendung von PyQt
Bevor Sie die Klassen aus Qt verwenden können, müssen Sie zuvor ein QApplication-Objekt erstellen. Dieses kümmert sich darum, dass Kommandozeilenargumente an die GUI Ihres Programms richtig übergeben werden, Events/Interaktiv bearbeitet werden, Ihr Programm richtig initialisiert wird, uvm.; strukturieren Sie also Ihr PyQt-Programm mit
# Initialisierung des Programms und Ihrer GUI
app = qw.QApplication(sys.argv)

# ...
# Ihr Programmcode
# ...

# Starten der Ereignisschleife (engl. event loop; Abfangen von Klicken, 
# Tippen, etc.) und beenden des gesamten Programms beim Beenden des Qt-Teils 
# (dies geschieht standardmäßig beim Schließen aller Fenster).
# Diese beiden Schritte zu trennen ist innerhalb dieses Praktikums nicht nötig.
sys.exit(app.exec_())

Achtung: Vergessen Sie beim Testen nicht QApplication zu initialisieren und am Ende auszuführen (app.exec_()). Falls Sie es vergessen, werden Sie unter Umständen ein schwarzes Fenster sehen, auf jeden Fall aber sind dann Ihre Interaktionsmöglichkeiten mit der Anwendung beschränkt.
Hallo Welt - Testen Sie den folgenden Code und versuchen Sie nachzuvollziehen, was geschieht.
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc

app = qw.QApplication(sys.argv)
w = qw.QWidget()
b = qw.QLabel(w)
b.setText("Hallo Welt!")
w.setGeometry(120,120,200,50)
b.move(100,200)
w.setWindowTitle("Tadaa, ein Fenster")
w.show()
sys.exit(app.exec_())

Die QWidget-Basisklasse
Die QWidget-Klasse ist die Basisklasse aller anderen Benutzerschnittstellen-Objekte. Das bedeutet, Buttons, Textfelder, Fenster, usw. liefern dieselbe Funktionalität. Wie Sie vielleicht bereits durch das "Hallo Welt" bemerkt haben, repräsentiert QWidget() ein leeres Fenster.
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc

app = qw.QApplication(sys.argv)
fenster1 = qw.QWidget()
fenster2 = qw.QWidget()
fenster3 = qw.QWidget()
% w.setGeometry(120,120,200,50)

# Fensternamen
fenster1.setWindowTitle("Das ist Fenster 1") # angezeigter Fenstername
fenster2.setWindowTitle("Das ist Fenster 2 (maximiert)")
fenster3.setWindowTitle("Das ist Fenster 3 (minimiert)")

# typische Fenstermanipulation
fenster1.resize(300, 500) # Größe festlegen
fenster1.move(10,10)      # auf dem Bildschirm verschieben

# Anzeigen der Fenster
fenster1.show()
fenster2.showMaximized()
fenster2.showMinimized()

Fenster auf dem Bildschirm zentrieren

# Zentrieren auf dem Bildschirm
fg = fenster4.frameGeometry()
centrum = QDesktopWidget().availableGeometry().center()
fg.moveCenter(center)
fenster4.move(fg.topLeft())

Ableiten von QWidget

Natürlich können Sie auch von der QWidget-Klasse (oder auch von der QMainWindow-Klasse - siehe später) ableiten, um eigene Widgets zu erstellen.
import sys
from PyQt5 import QtWidgets as qw
class MyGUIProgram(qw.QWidget):

	# statische (optionale) Methode zum Starten
	# des GUI-Programms
	def spawn():
		app = qw.QApplication(sys.argv)
		MyGUIProgram()
		sys.exit(app.exec_())

	# Konstruktor
	def __init__(self):
		# Konstruktor von QWidget
		super().__init__()

		# gute Idee: Lagern Sie Code aus, um Ihren Code
		# übersichtlich zu halten
		self.setWindowTitle('SnakeGame')
		self.init_mainWindow() #ausgelagerte GUI-Erstellung
		self.show()

if __name__ == "__main__":
	MyGUIProgram.spawn()

Nützliche GUI-Elemente
Achtung: Vergessen Sie beim Testen nicht QApplication zu initialisieren und am Ende auszuführen (app.exec_()). Falls Sie es vergessen, werden Sie unter Umständen nur ein schwarzes Fenster sehen.
Wie bereits erwähnt, sind auch GUI-Elemente wie Buttons oder Text-Eingabefelder selbst QWidgets - daher erben sie auch die oben aufgeführten Funktionen und können also selbst auch bereits alleinstehend in einem automatisch erstellten Fenster angezeigt werden.
b = qw.QPushButton("drück mich!")
b.resize(150,200)
b.show()

Im Umkehrschluss bedeutet das aber auch, dass QWidget selbst kein Fenster darstellt, sondern lediglich einen leeren "Container". Einzelne Elemente anzuzeigen ist zwar nicht so spannend; bevor wir zur Schachtelung von Elementen kommen, sollen nun einige nützliche Elemente und ihre Funktionsweisen erklärt werden. Probieren Sie die Elemente ruhig aus. Alternativ können Sie ihr Aussehen und mehr Informationen zu diesen auch unter den eingangs genannten Seiten entnehmen. Schauen Sie sich ruhig auch die oben genannte offizielle Dokumentation an, um Genaueres zu erfahren.
Hinweis: Die hier gelistete Übersicht einiger Klassen und zugehöriger Funktionen erhebt keinen Anspruch auf Vollständigkeit. Um die komplette Funktionsweise einzusehen, verweisen wir an dieser Stelle auf die offizielle C\+\+-Dokumentation (in der Einleitung verlinkt). Schrecken Sie dabei nicht vor C++ zurück - die Methoden und ihre Parameter sind in PyQt5 gleich.

QPushButton

"Klickbare" Buttons
btn = qw.QPushButton("drück mich!")
btn.setDefault()        # Standard-Button
btn.setEnabled(False)   # ausgegraut

btn.clicked.connect(fn) # fn wird bei Klick ausgeführt 
# (es sind "echte" Funktionen oder lambda-Ausdrücke möglich, z.B.)
btn.clicked.connect(lambda: print("hey"))
def clicked():
	print("ho")
btn.clicked.connect(clicked)
btn.pressed.connect(fn) # sobald Maus gedrückt wird
btn.released.connect(fn)# sobald Maus losgelassen wird

QLabel

Nicht-veränderbaren Text oder Bilder (wird später behandelt) anzeigen.
lbl = qw.QLabel("Ha! Du kannst mich nicht ändern!")
lbl2 = qw.QLabel()
lbl2.setText("nachträglich verändert")
lbl2.setAlignment(qc.Qt.AlignRight)     # Ausrichtung

QLineEdit

Veränderbarer Text
le  = qw.QLineEdit()
le2 = qw.QLineEdit("Initialtext")

# Einstellungen
le.setMaxLength(10)                   # Maximallänge
le.setAlignment(qc.Qt.AlignRight)     # Ausrichtung
le.setFont(qg.QFont("Arial", 20))     # Schriftart (Arial, Größe 20)

# direkte Validierung
le2.setValidator(QIntValidator())     # nur Ganzzahlen
le2.setValidator(QDoubleValidator())  # nur Fließkommazahlen 
le2.setValidator(QDoubleValidator(0.99, 99.99, 2))  # (festes Format)

# signals (wie bei QPushButton)
le2.cursorPositionChanged.connect(fn) # Parameter: (int alt, int neu)
le2.editingFinished.connect(fn)
le2.selectionChanged.connect(fn)
le2.textChanged.connect(fn)           # parameter: QString
le2.textEdited.connect(fn)            # parameter: QString

QCheckBox

Checkbox mit Text-Annotation
cb = qw.QCheckBox("Possibility 1")
cb.setChecked(True) # Standardmäßig an oder aus?
cb.setText("abc")
cb.text()           # getter (hier: "abc")
cb.isChecked()

# Funktionsaufruf bei Statusänderung
cb.toggled.connect(fn)

Layout Manager

Absolute Positionierung

Jedes Widget hat das optionale Argument parent. Durch Setzen dessen auf ein anderes Widget, wird das neu erstellte als Teil des Widgets parent dargestellt.
# Initialisierung wie üblich
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc
app = qw.QApplication(sys.argv)

win = qw.QWidget()			
win.setGeometry(10, 30, 300, 200) # Position: (10,30) auf dem Bildschirm
                                  # Größe:  (300,200)
btn = qw.QPushButton("Jim",win)
btn.move(20,20)                   # Position (20,20) in parent=win
btn2 = qw.QPushButton("Jim2",parent=win)
btn2.move(120,120)                # Position (120,120) in parent=win

# Anzeigen und Starten
win.show()
app.exec_()

Auf diese Art und Weise können ganze Layouts definiert werden. Erstellen Sie Widgets und arrangieren Sie darin rekursiv andere Widgets. Diese Herangehensweise hat jedoch einige offensichtliche Nachteile:
  • bei Größenänderungen einzelner Widgets oder des Hauptfensters müsste das ganze Layout angepasst werden,
  • Layoutänderungen können mühsam werden, da eine Größenänderungen eines Elements Änderungen an allen anderen Elementen nach sich ziehen kann,
  • die Darstellung des Layouts kann sich auf verschiedenen Geräten und diversen Auflösungen unterscheiden.

Aus diesem Grund gibt es sogenannte Layout Manager, die die absolute Positionierung für Sie übernehmen und bei der dynamische Änderungen eines Layouts helfen können.
Worin sich alle Layouts ähneln, ist, dass man anstelle eines Widgets auch immer ein anderes Layout einsetzen kann. So können selbst sehr verschachtelte GUIs strukturiert werden. Das äußerste Layout muss per win.setLayout(layout) zugewiesen werden.

QHBoxLayout und QVBoxLayout

Diese Layouts arrangieren Widgets und Layouts in einer horizontalen bzw. vertikalen Reihe. Es folgt ein Beispiel, das beides verschachtelt verwendet.
# Initialisierung wie üblich
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc
app = qw.QApplication(sys.argv)

win = qw.QWidget()
btn1 = qw.QPushButton("Jim")
btn2 = qw.QPushButton("Benjamin")
lbl = qw.QLabel("Entscheide dich:")
btn3 = qw.QPushButton("blau")
btn4 = qw.QPushButton("rot")

vbox = qw.QVBoxLayout()
vbox.addWidget(btn1)     # erst btn1
vbox.addWidget(btn2)     # darunter btn2
vbox.addStretch()        # skalierter leerer Platz
vbox.addWidget(lbl)      # darunter ein Label

hbox = qw.QHBoxLayout()
vbox.addLayout(hbox)     # darunter ein neues Layout (als Widget)

hbox.addWidget(btn3)     # links:  btn3
hbox.addStretch()        # Mitte:  gestreckte leere Fläche
hbox.addWidget(btn4)     # rechts: btn4

win.setLayout(vbox)
win.show()
app.exec_()

QFormLayout

Diese Layouts arrangieren Widgets und Layouts in einer typischen Formularansicht: Zeilenweise links ein Label, Rechts ein Widget:
# Initialisierung wie üblich
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc
app = qw.QApplication(sys.argv)

win = qw.QWidget()

form = qw.QFormLayout()
form.addRow(qw.QLabel("test"), qw.QLineEdit())
form.addRow(qw.QLabel("name"), qw.QPushButton())

hbox = qw.QHBoxLayout()
hbox.addWidget(qw.QCheckBox("check1"))
hbox.addWidget(qw.QCheckBox("check2"))
hbox.addWidget(qw.QCheckBox("check3"))

form.addRow(qw.QLabel("check"), hbox)

win.setLayout(form)      # Wichtig! Erst hier wird das Layout an win angefügt
win.show()
app.exec_()

QGridLayout

Das Grid-Layout ermöglicht das Arrangieren der Widgets in einem Gitter. Es können dabei sowohl einzelne Gitterzellen durch je ein Widget befüllt werden, als auch mehrere Zellen durch ein Widget.
# Initialisierung wie üblich
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc
app = qw.QApplication(sys.argv)

win = qw.QWidget()	
grid = qw.QGridLayout()

# einzelne Zellen
for i in range(1,4):
	for j in range(1,3):
		grid.addWidget(qw.QPushBu("Knopf"+str(i)+"/"+str(j)), i,j) # Position (i,j)

# mehrere Zellen
grid.addWidget(qw.QPushBut("Knopf Quad"), 1,3, 2, 2) # Position (1,3), Größe (2,2)
grid.addWidget(qw.QPushBut("Knopf Lang"), 1,5, 4, 1)
grid.addWidget(qw.QPushBu("Knopf Breit"), 4,1, 1, 2)

# Layouts selbst können auch hinzugefügt werden
grid.addLayout(...)

win.setLayout(grid)
win.show()
app.exec_()

Tooltip
Um das Arbeiten mit Ihrem Programm zu erleichtern, können Sie Tooltips einsetzen. Diese werden angezeigt, sobald Sie mit der Maus über das jeweilige Element fahren und warten. Wir erweitern dazu das FormLayout-Beispiel.
# Initialisierung wie üblich
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc
app = qw.QApplication(sys.argv)

win = qw.QWidget()

form = qw.QFormLayout()
testlbl = qw.QLabel("test")
testlbl.setToolTip("Informationen zum Label Test")
le = qw.QLineEdit()
# Der Text kann dabei auch formattiert werden
le.setToolTip("<u>Hier</u> muss <b>eine Eingabe</b> vorgenommen werden.")
form.addRow(testlbl, le) 

win.setLayout(form)      # Wichtig! Erst hier wird das Layout an win angefügt
win.show()
app.exec_()

Dialoge
Angenommen Sie erwarten eine Benutzereingabe, z.B. das Auswählen einer Datei (mittels QFileDialog) oder Sie müssen dem Benutzer etwas mitteilen. Durch einen Dialog kann dies erreicht werden. Die folgenden Beispiele sollten bei der Erstellung Ihrer Benutzeroberflächen als Anreiz reichen. (Eine weitere, hier nicht aufgeführte, Dialogklasse ist QMessageBox)

Entscheidungs-Dialog

Bemerken Sie, wie die Slots und Signals verwendet wurden, um die Nachricht im Hauptfenster sichtbar zu machen.
def dialog():
	d = qw.QDialog()
	d.setWindowTitle("Dialogtitel")

	# kann der Dialog umgangen werden um andere Fenster zu bedienen?
	d.setWindowModality(qc.Qt.ApplicationModal) # nein (Standard)
	# d.setWindowModality(qc.Qt.WindowModal)    # ja

	# ... wie QWidget verwendbar
	btnok = qw.QPushButton("Ok, lass mich in ruhe.",d)
	btnno = qw.QPushButton("Nein.",d)
	hbox = qw.QHBoxLayout()
	hbox.addWidget(btnok)
	hbox.addWidget(btnno)
	d.setLayout(hbox)

	# akzeptieren / ablehnen
	btnok.clicked.connect(lambda: d.accept())
	btnno.clicked.connect(lambda: d.reject())

	# bei Fehler / Erfolg an Hauptfenster melden
	d.rejected.connect(lambda: b.setText("Oh, wie schade!"))
	d.accepted.connect(lambda: b.setText("Erfolg!"))

	# Extras
	btnok.setDefault(True)           # btn wird bei Enter gedrückt
	d.exec_()                        # Fenster ausführen

w = qw.QWidget()	
b = qw.QPushButton("Die Entscheidung",w)
b.clicked.connect(dialog)
w.show()

Dateiauswahl-Dialog

Bemerken Sie, wie auch hier die Slots und Signals verwendet wurden, um die Nachricht im Hauptfenster sichtbar zu machen. Gerade diese Klasse bietet eine Vielzahl von Möglichkeiten, die Auswahl der Dateien zu beschränken oder die Anzeige der Auswahl zu steuern. An dieser Stelle ist nur ein Minimalbeispiel gelistet - ziehen Sie bitte die Dokumentation zu Rate, sollten Sie mehr Kontrolle über dieses Widget wünschen.
def fdialog():
	d = qw.QFileDialog()
	d.setWindowTitle("Dialogtitel")

	# bei Auswahl einer Datei
	d.fileSelected.connect(lambda file: lbl.setText(file))
	
	d.exec_()                        # Fenster ausführen

w = qw.QWidget()	
hbox = qw.QHBoxLayout()
lbl = qw.QLineEdit("standarddateiname.txt")
hbox.addWidget(lbl)
b = qw.QPushButton("Dateiauswahl")
hbox.addWidget(b)
b.clicked.connect(fdialog)
w.setLayout(hbox)
w.show()

Timer
Manchmal benötigen Sie Programmcode, der nach einem Zeitinterval einmalig oder periodisch aufgerufen werden soll. Dieses Beispiel zeigt Ihnen, wie Sie dies mit Qt erreichen können.
timer = QTimer()
timer.setInterval(300)    # Timer-Reset alle 300ms
timer.timeout.connect(fn) # fn sei die Funktion, die aufgerufen werden soll
timer.start(ms)           # ms: Anzahl der Millisekunden, nach denen der Timer die 
                          # Ausführung von fn erstmals startet. (Standardmäßig ms=0)
timer.stop()              # Timer anhalten
timer.setSingleShot(True) # einmalige Ausführung von fn
timer.isActive()

Bilder
Das Arbeiten mit Bildern ist für einige Anwendungen unerlässlich. Das betrifft nicht nur das Anzeigen von bestehenden Bildern, sondern auch das Erstellen neuer Bilddaten. In Qt gibt es dabei zwei Arten von Bildrepräsentationen: QPixmap und QImage.
  • QPixmap repräsentiert dabei ein off-screen Bild, auf dem mithilfe der Klasse QPainter gemalt werden kann. QPainter kann dabei alles, was ein Zeichenprogramm wie Paint auch kann: Quadrate, Kreise, Text zeichnen und dabei eine Auswahl an Pinseln verwenden.
  • QImage repräsentiert hingegen die "rohen" Pixeldaten, die per Hand geändert werden können.

In dieser Übersicht werden wir auf den reinen Pixeldaten arbeiten. Dies reicht für die Praktikumsaufgaben völlig aus, zögern Sie nicht (bei Interesse) sich die Dokumentation von QPixmap/QPainter anzusehen.

Anzeigen einer Bilddatei

Bilder sind keine Widgets und können daher nicht direkt angezeigt werden. Glücklicherweise liefert das Widget QLabel diese Möglichkeit nach, indem man einem diesem ein QPixmap als anzuzeigendes Objekt übergibt. Gehen Sie davon aus, dass die Konvertierung von QImage zu QPixmap schnell von Statten geht. Erinnern Sie sich daran, dass ein Pixmap fest ist und daher beim Neuzeichnen erneut erstellt werden muss. Im folgenden Beispiel gehen wir davon aus, dass sich ein Bild mit dem Namen bild.png in demselben Ordner befindet, in dem auch das Skript gestartet wird.
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc
app = qw.QApplication(sys.argv)

# in QLabel wird das Bild angezeigt
display = qw.QLabel()

# Laden der Datei in eine Pixmap (nicht direkt bearbeitbar)
bild = qg.QPixmap("bild.png")
display.setPixmap(bild)

# Alternativ: QImage (muss zu QPixmap konvertiert werden)
bild = qg.QImage("bild.png")
display.setPixmap(qg.QPixmap.fromImage(bild))

# Fenster anzeigen
display.show()
app.exec_()

Skalieren eines Bildes

Sie können ein Pixmap folgendermaßen skalieren - beachten Sie, dass es sowohl möglich ist die Skalierung an der vorgegeben Größe eines displays (QLabel) festzumachen, aber auch die Größe direkt festzulegen.
# Möglichkeit 1
display = qw.QLabel()
display.resize(200,300)
pixmap = ... # wie oben qg.QPixmap(...) oder qg.QPixmap.fromImage(...)
scaledpixmap = pixmap.scaled(display.size(), Qt.KeepAspectRatio)
display.setPixmap(scaledpixmap)

# Möglichkeit 2
display = qw.QLabel()
pixmap = ... # wie oben qg.QPixmap(...) oder qg.QPixmap.fromImage(...)
scaledpixmap = pixmap.scaled(200,300, Qt.KeepAspectRatio)
display.setPixmap(scaledpixmap)

Erstellen und Manipulation eines Bildes

# wir erstellen zuerst das Bild
breite, hoehe = 400, 300
# QImage-Formate: http://doc.qt.io/qt-5/qimage.html#Format-enum
img = qg.QImage(breite, hoehe, qg.QImage.Format_RGBA8888)
img.fill(qc.Qt.white) # Bild mit Standardwerten füllen (Wichtig!)

# das Format der Pixelfarbe lautet 0xRRGGBBAA, wobei
# RR, GG, BB jeweils die Hex-Darstellung der Farbwerte für 
# Rot, Grün und Blau zwischen 0 und 255 ist (d.h. 195 entspräche C3). 
# 0 bedeutet, dass die Farbe nicht vorhanden ist, 
# 255 ist der Maximalwert jeder Farbe.
# AA beschreibt die Transparenz (0 für Transparent, 255 für Sichtbar)
img.setPixel(10,20, 0x00ff00ff) # Grün

# das Setzen von Hex-Werten bedarf einiger Übung und Gewöhnung.
# Glücklicherweise ist es auch möglich, die Farbwerte im 10er-System
# anzugeben (Werte jeweils zwischen 0 und 255).
img.setPixelColor(20,40,qg.QColor(20,30,155)) 

# Anzeigen in QLabel
display.setPixmap(qg.QPixmap.fromImage(img))

# neue Manipulation ist erst beim Neu-Zeichnen sichtbar!
img.setPixelColor(80,80,qg.QColor(20,30,155)) 

# erneut Zeichnen
display.setPixmap(qg.QPixmap.fromImage(img))

Erstellen eines Bildes aus einem Numpy-Array

breite, hoehe = 400, 300
npbild = np.zeros([hoehe,breite,4], dtype=np.uint8) # RGBA (A: Transparenz)
npbild += 255 # nun ist das Bild weiß
npbild[20,30,:] = [135,23,53,255] # setzen eines Farbpixels im Numpy-Array
npbild[20,30,0] = 255             # setzen des Rotwertes eines anderen Pixels
img = QImage(npbild, breite, hoehe, QImage.Format_RGBA8888)

Speichern eines Bildes

Nicht jedes Programm, das mit Bildern arbeitet, muss eine GUI mitliefern und dieses Bild erst auf dem Bildschirm anzeigen. Hier sehen Sie eine Möglichkeit, das Bild direkt zu speichern.
# wir erstellen zuerst das Bild
breite, hoehe = 400, 300
img = qg.QImage(breite, hoehe, qg.QImage.Format_RGBA8888)
img.fill(qc.Qt.white) # Bild mit Standardwerten füllen (Wichtig!)

# Ändern von Pixelinformationen
img.setPixel(10,20, 0x00ff00ff) # Grün

img.save("neues_bild.png")

QPainter Grundfunktionen

Mit QPainter können sie Ellipsen, Kreissegmente, Rechtecke, Linien, Polygonzüge, Graphiken und Text auf einem Bild zeichnen.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
app = QApplication(sys.argv)

# Qlabel zum Anzeigen des Bildes
display = QLabel()
display.setGeometry(QRect(0,0,400, 600))

# unsere Hauptzeichenfläche
canvas = QImage(400,600, QImage.Format_RGBA8888)
painter = QPainter(canvas)

# Linien
linienbreite = 2
painter.setPen(QPen(Qt.blue, linienbreite, Qt.DotLine, Qt.SquareCap, Qt.BevelJoin))
painter.drawLine(50,50,50,100)
painter.setPen(QPen(QColor(100,200,300,50), 5*linienbreite, Qt.SolidLine, Qt.RoundCap, Qt.BevelJoin))
painter.drawLine(70,50,70,100)

# Rechtecke (beachten Sie die Verwendung von Stiften (Pen) und Pinseln (Brush)
painter.fillRect(100,50,150,200,Qt.white)
painter.setPen(QPen(Qt.red, linienbreite, Qt.DashLine, Qt.SquareCap, Qt.BevelJoin))
painter.drawRect(120,70,170,220)
painter.setPen(Qt.NoPen)
painter.setBrush(Qt.green)
painter.drawRect(220,170,270,320)

# Ellipsen
painter.setBrush(Qt.NoBrush)
painter.setPen(QPen(QColor(0,150,150,100), 20))
painter.drawEllipse(QPoint(230,180),30,50)

# Text
painter.setPen(Qt.black)
groesse = 20
painter.setFont(QFont("Helvetica", groesse));
painter.drawText(100,300, "Hallo Welt!")

display.setPixmap(QPixmap.fromImage(canvas))
display.show()
sys.exit(app.exec_())

Bildebenen

Sofern sie eine odere mehrere QPixmap aufwändig gezeichnet haben, können sie dies Verwenden, um sie übereinander zu setzen. (Nicht gezeichnete Pixel sind Transparent)
# Qlabel zum Anzeigen des Bildes
display = QLabel()
display.setGeometry(QRect(0,0,width*zoom, height*zoom))

# unsere Hauptzeichenfläche
canvas = QPixmap(1000,500) # oder QImage
painter = QPainter(canvas)

# Hintergrund Ebene
ebene1 = QPixmap(1000,500) # oder QImage
painter1 = QPainter(ebene1)
# ... langsamer code, komplexes Zeichnen der Ebene

# Objekt auf eigener Ebene
ebene2 = QPixmap(200,300) # oder QImage
painter2 = QPainter(canvas)
# langsamer code, komplexes Zeichnen der Ebene

# ... in der redraw-Schleife
    # weiß färben
    painter.fillRect(0,0,1000,500,Qt.white)

    # Hintergrund hinzufügen, (0,0) ist die Startposition
	painter.drawPixmap(0,0,ebene1) # bei QImage: .drawImage(...)

    # Objekt hinzufügen
	painter.drawPixmap(234,345,ebene2) # bei QImage: .drawImage(...)

    # skalieren auf die Größe der Zeichenfläche
	# für Canvas vom Typ: QPixmap
    display.setPixmap(canvas)
	# für canvas vom Typ: QImage
	display.setPixmap(QPixmap.fromImage(canvas))

QMainWindow
Das QMainWindow gibt bereits eine GUI-Struktur vor, die bei der GUI-Erstellung häufig verwendet wird: Statusleiste, Menü, anheftbare Widgets (hier nicht behandelt).
# Initialisierung wie üblich
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc
app = qw.QApplication(sys.argv)
	
main = qw.QMainWindow()
main.resize(500,600)
main.setWindowTitle("Hauptfenster")

# Zentrale Teil kann wie Üblich gefüllt werden
win = qw.QWidget()
main.setCentralWidget(win)
hbox = qw.QHBoxLayout()
win.setLayout(hbox)
hbox.addWidget(qw.QPushButton("links"))
hbox.addWidget(qw.QPushButton("Mitte"))
hbox.addWidget(qw.QPushButton("rechts"))

# MainWindow hat auch eine Statusleiste
# (diese kann selbst auch Widgets anzeigen)
stat = main.statusBar()
# stat.addWidget(qw.QLabel("Statustext")) # linksbündig, wird evtl. von showMessage überblendet
stat.addPermanentWidget(qw.QLabel("Statustext2")) # rechtsbündig
stat.showMessage("wird 2s lang eingeblendet", 2000)
# stat.showMessage("wird angezeigt, bis gelöscht wird")
# stat.clearMessage() # löschen angezeigter Nachrichten

# ... und auch eine Menüleiste
menu = main.menuBar()
menu.setNativeMenuBar(False) # optional: OS X stellt das Menü anders dar

m1 = menu.addMenu("Datei")
m1.addAction("Neu")
# Menüeintrag mit Tastenkürzel
m1.addAction("Öffnen")
m1sub = m1.addMenu("Untermenü")
m1sub.addAction("Versteckt")
m1.addSeparator()
close = qw.QAction("Beenden", main)
close.setShortcut("Ctrl+Q")
close.setStatusTip("Schließt das Programm")
close.triggered.connect(lambda: main.close())

m1.addAction(close)
m2 = menu.addMenu("Bearbeiten")
for i in range(3):
	for j in range(4):
		m2.addSeparator()
		m2.addAction("Platzhalter")
menu.addSeparator()

def opendialog(clickable):
	dialog = qw.QDialog()
	dialog.resize(200,150)
	qw.QLabel("Programmversion 3.1415",dialog)
	dialog.exec_()
m3 = menu.addMenu("Info")
info = m3.addAction("Info")
info.triggered.connect(opendialog)

main.show()
app.exec_()

Tastatur- und Mausevents

Tastaturevents

Nicht jede Eingabe soll in Text-Eingabefeldern erfolgen. Häufig benötigt man Eingaben, die erst vorverarbeitet werden müssen. Sie haben im letzten Abschnitt die Möglichkeit kennengelernt Tastenkürzel für Menüeinträge zu definieren. Die Definition war dabei lokal an das jeweilige Menü und das Kürzel selbst gebunden. Im Folgenden sind die Funktionen globaler Natur. Mit anderen Worten - alle Tasten rufen zuerst dieselbe Funktion auf, erst darin wird entschieden was mit dem "Event" gemacht wird.
Zuerst konzentrieren wir uns hier auf das Drücken einer Taste (keyPressEvent), die vorgehensweise beim Event fürs Loslassen der Taste (keyReleaseEvent) ist dabei ganz analog.
Ähnlich zu den Signalen, bei denen Sie per connect die aufzurufende Funktion an das Objekt gebunden haben, können Sie bei den hier aufgeführten Events die entsprechende Funktion überschreiben. In dem folgenden Besipiel wird jedes mal, wenn Sie eine beliebige Taste drücken, dies in der Statusleiste kenntlich gemacht. Im Spezialfall der Escape-Taste wird das Fenster geschlossen.
# Initialisierung wie üblich
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc
app = qw.QApplication(sys.argv)

# Einfaches Layout (nur eine leere Fläche)
win = qw.QMainWindow()            
win.setGeometry(10, 30, 300, 200) # Position: (10,30) auf dem Bildschirm
                                  # Größe:  (300,200)

# Tastendruck in QMainWindow abfangen 
def fn(e):
    win.statusBar().showMessage("Taste mit key-code "+str(e.key())+" gedrückt", 1000)
    if e.key() == qc.Qt.Key_Escape:
        win.close()
win.keyPressEvent = fn

# Anzeigen und Starten
win.show()
app.exec_()

Eben wurde die Funktion, die beim Tastendruck aufgerufen wird, am Objekt selbst überschrieben. Alternativ können sie auch ein eigenes Widget erstellen und dort die von QWidget geerbte Funktion definieren.
# Initialisierung wie üblich
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc

class TastenTest(qw.QWidget):
    
	# einfaches Layout
	def __init__(self):
		super().__init__()
		self.setGeometry(10, 30, 300, 200)
		self.show()

	# Überladen der leeren Standardfunktion
	def keyPressEvent(self, e):
		self.statusBar().showMessage("Taste mit key-code "+str(e.key())+" gedrückt", 1000)
		if e.key() == qc.Qt.Key_Escape:
			win.close()
        
if __name__ == '__main__':
	app = qw.QApplication(sys.argv)
	ex = TastenTest()
	sys.exit(app.exec_())

Tastenanschläge können nun abgefangen werden. Doch welcher Tasten-Code gehört zu welcher Taste? Die leichteste Möglichkeit besteht darin, die PyQt Konstanten (wie etwa bei der Escape-Taste) zu verwenden. Beachten Sie, dass an dieser Stelle nicht zwischen Groß- und Kleinschreibung unterschieden wird. Häufig benutzte Tasten sind:
  • qc.Qt.Key_Escape
  • qc.Qt.Key_Return
  • qc.Qt.Key_Print (Drucken-Taste)
  • qc.Qt.Key_Left, qc.Qt.Key_Right, qc.Qt.Key_Up, qc.Qt.Key_Down (Pfeiltasten)
  • qc.Qt.Key_F1, qc.Qt.Key_F2, qc.Qt.Key_F..., qc.Qt.Key_F35 (Funktionstasten)
  • qc.Qt.Key_0, qc.Qt.Key_1, qc.Qt.Key_..., qc.Qt.Key_9 (Zifferntasten)
  • qc.Qt.Key_A, qc.Qt.Key_B, qc.Qt.Key_..., qc.Qt.Key_Z

Mausevents

Ganz Analog zu den Tastenanschlägen kann auch die Maus abgefangen werden. Es ändert sich einzig die Form, in der das übergebene Event-Objekt (e) vorliegt.
import sys
from PyQt5 import QtWidgets as qw
from PyQt5 import QtGui as qg
from PyQt5 import QtCore as qc

class MausTest(qw.QMainWindow):
    
    # einfaches Layout
    def __init__(self):
        super().__init__()
        self.setGeometry(10, 30, 300, 200)
        self.show()
    
        # Mouse-Events werden sonst nur aufgerufen, wenn eine Maustaste gedrückt wird.
        self.setMouseTracking(True)

    # Überladen der leeren Standardfunktion
    def mouseMoveEvent(self, e):
        self.statusBar().showMessage("Maus-Position ("+str(e.x())+","+str(e.y())+")", 1000)
        if e.button() == qc.Qt.LeftButton:
            self.statusBar().showMessage("Maus-Position ("+str(e.x())+","+str(e.y())+") + Linke Maustaste", 1000)
        
if __name__ == '__main__':
    app = qw.QApplication(sys.argv)
    ex = MausTest()
    sys.exit(app.exec_())

Achtung: Beachten Sie die Einstellung zum Mouse-Tracking. Wird dieses nicht gesetzt werden die Maus-Events nur getriggert, wenn eine Maus-Taste in das Event involviert ist.
Eine vollständige Liste aller Mouse-Buttons können sie wieder der offiziellen Qt-Dokumentation entnehmen.

Andere Events

Neben den Tastatur- und Mausevents gibt es noch einige mehr, was die Darstellung und Interaktion mit Widgets betrifft (z.B. resize Event, leaveEvent, ...). Entnehmen Sie die genauere Funktionsweise bitte der Dokumentation - im Moment sind diese für die Bearbeitung der Übungsaufgaben nicht nötig.