Einführung in python - Ruhr

Werbung
Einführung in das Wissenschaftliche Arbeiten –
Einführung in python
Sommersemester 2017
Ruhr-Universität Bochum
Fakultät für Physik und Astronomie
Lehrstuhl für Theoretische Physik IV
Plasma-Astroteilchen Physik
Dr. Lenka Tomankova
Lukas Merten, M.Sc.
April 2017
1
Inhaltsverzeichnis
1 Grundlagen
1.1 Stärken . . . . . . . . . . . . . .
1.1.1 jupyter/ipython notebook
1.2 Schwächen . . . . . . . . . . . . .
1.3 Download . . . . . . . . . . . . .
.
.
.
.
3
3
3
3
4
2 Erste Schritte
2.1 Basisklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Grundlegende Rechenoperationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 Ein erster Plot (matplotlib) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
4
5
6
3 Fortgeschrittene Programmstrukturen
3.1 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2 Generatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3 Loops/Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
7
9
10
4 Namen, Objekte und Referenzen
10
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5 Numerische Rechnungen: numpy
11
5.1 N-dimensionales Array: numpy.ndarray() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.2 Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
6 Wissenschaftliche Funktionen: SciPy
6.1 scipy.special . . . . . . . . . . . . . . . . . . . . . . . .
6.2 Numerische Integration: scipy.integrate.quad . . . . . .
6.3 Nullstellensuche: scipy.optimize.root . . . . . . . . . .
6.4 Regression / Funktionsfitting: scipy.optimize.curve fit .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
14
14
16
17
18
7 Anhang
19
7.1 The zen of python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
7.2 Nützliche Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2
1
Grundlagen
Python ist eine interpretierte Programmiersprache, die immer einen Interpreter zur Ausführung benötigt.
Das bedeutet, dass in python keine kompilierten “stand alone”-Programme geschrieben werden können. Dies
stellt im allgemeinen Gebrauch allerdings keine große Einschränkung dar.
Python wird häufig als eine leicht zu lernende Programmiersprache bezeichnet, was im Hinblick auf die
einfache Syntax sicherlich auch stimmt. Das berühmte “Hello-World”-Beispiel lässt sich in nur einer Zeile
mit einem einzigen Befehl ausführen:
In [87]: print "Hello World"
Hello World
oder im jupyter notebook sogar ganz ohne einen Befehl:
In [88]: ’Hello World’
Out[88]: ’Hello World’
Das liegt daran, dass in jupyter notebooks der Rückgabewert der letzten Funktion im Output der Zelle
dargestellt wird. Gibt es keinen Rückgabewert erhält man nützliche Infos zum Objekt selbst.
Außerdem muss man in python keine Variablentypen beim deklarieren einer Variable festlegen. So etwas
wie double d = 2.; gibt es in python nicht. Der typ einer Variablen wird in python dynamisch angepasst:
In [89]: a = 2
print "a is of type:", type(a)
a = 2.
print "now a is of type:", type(a)
a is of type: <type ’int’>
now a is of type: <type ’float’>
Dies führt zu einer steilen Lernkurve, da man sich um die ganzen Typzuweisungen (und auch Deklararation) nicht kümmern muss. Allerdings kann dies auch zu “unsauberen” Programmierungen führen, was
später zu ungewollten Programmverhalten führen kann. In python gilt also der Grundsatz: “What quacks
like a duck is a duck!”. (Siehe try Anweisungen und fortgeschrittene Funktionsdefinition)
1.1
Stärken
Python besitzt eine Menge nützliche Bibliotheken, hier Module genannt, die einem viel Programmierarbeit
abnehmen. Im Zweifel gilt der Grundsatz, dass sich schon jemand vor euch mir dem Problem befasst hat
(und es wahrscheinlich auch besser gelöst hat). Nützliche Module sind beispielsweise numpy (numerische
Rechnung auf numpy.arrays), scipy (spezielle Funktionen, numerische Integration, statistische Tests), pandas
(Datenverarbeitung bei großen Datenmengen) und matplotlib (2-dimensionale Plots)
Weiterhin ist python bei der Ausführung von internen funktionen, wie range(10000).sum(), sehr schnell.
Hier sollte man nicht versuchen das Problem durch einen handgeschriebenen loop zu lösen.
1.1.1
jupyter/ipython notebook
Eine Umgebung, die auf einem virtuellen Server läuft, und es ermöglicht kleine Code-Schnipsel aber auch
größere Programme effizient zu testen. Das vorliegende Skript ist als jupyter notebook geschrieben worden.
1.2
Schwächen
Die Standardimplementierung von python “CPython” besitzt einen sog. “Global Interpreter Lock (GIL)”
dieser macht eine intrinsische Parallelisierung unmöglich. Dieses Problem lässt sich entweder durch andere
Implentierungen z.B. PyPy umgehen oder durch das Vorschalten eines anderen Programms z.B. mpi. In den
Anfängen sollte dies aber keine Einschränkung darstellen.
3
1.3
Download
Eine ziemlich umfangreiche Zusammenstellung aller wichtigen Module findet sich im Downloadpaket von
anaconda unter https://www.continuum.io/downloads. Pakete können hier ganz einfach nachinstalliert
werden: Einfach in der Shell eintippen: $ conda install <paket>
2
Erste Schritte
Kommen wir nun zu den wirklichen Basics einer jeden Programmiersprache.
2.1
Basisklassen
Es gibt verschiedene, unterschiedliche Basisklassen, die alle ihre Vor- und Nachteile haben. Hier stellen wir
nun die wichtigsten vor.
1. Integer: Int = 42
2. Float: Float = 3.142
3. String: Str = Hello World
4. List: List = [’A’, 2., [2, 3, 4], ’Hello’]
5. Tuple: Tup = (1, 2, 3)
6. Dictionary: Dict = {’A’:2., ’Hello’:’World’, (1, 2):(3., 4.)}
7. NumpyArray: Arr = numpy.array([[1, 2, 3], [4, 5, 6]])
Für den letzten Typ muss zuvor das Modul “numpy” importiert werden.
In [90]: # Importiere das Modul numpy. Auf die Funktionen kann man nun mit "np.<funktion>/<class>"
import numpy as np
Int = 2; Float = 3.142; Str = "Hello World"; List = [’A’, 2., [2, 3, 4], ’Hello’]
Tup = (1, 2, 3); Dict = {’A’:2., ’Hello’:’World’, (1, 2):(3., 4.)}
Arr = np.array([[1, 2, 3], [4, 5, 6]])
print "Int = {}".format(Int)
print "Pi = {}".format(Float)
print "Str = {}".format(Str)
print "List = {}".format(List)
print "Tup = {}".format(Tup)
# Ein erster for loop:
for key, value in Dict.items():
print "key = {}".format(key), ’\t’, "value = {}".format(value)
print "Arr = {}".format(Arr)
Int = 2
Pi = 3.142
Str = Hello World
List = [’A’, 2.0, [2, 3, 4], ’Hello’]
Tup = (1, 2, 3)
key = A
value = 2.0
key = (1, 2)
value = (3.0, 4.0)
key = Hello
value = World
Arr = [[1 2 3]
[4 5 6]]
4
2.2
Grundlegende Rechenoperationen
In diesem Abschnitt stellen wir kurz die grundlegenden Rechenoperationen vor. Natürlich können in python
alle Grundrechenarten ausgeführt werden. Auch hier sind, wie bei allen Programmiersprachen, die Probleme
mit der Fließkommagenauigkeit u.Ä. zu beachten.
In [91]: a = 1
b = 2
A = 1.
B = 2.
c = a + b
d = a / b
D = A / B
e = a - b
f = a * b
print "c = a+b = {}".format(c)
print "e = a-b = {}".format(e)
print "f = a*b = {}".format(f)
# Achtung bei der Division von Integern
print "d = a/b = {}".format(d)
print "D = A/B = {}".format(D)
c
e
f
d
D
=
=
=
=
=
a+b
a-b
a*b
a/b
A/B
=
=
=
=
=
3
-1
2
0
0.5
Man kann auch mit Arrays elementweise Rechenoperationen ausführen.
In [92]: Einer = np.array(range(10))
Quadrate = Einer**2.
print "Einer**2. = {}".format(Quadrate)
Zehner = Einer*10
print "Einer*10 = {}".format(Zehner)
print """
Man achte auf die unterschiedlichen Typen von "Quadrate" und "Zehner".
Dies wird durch eine interne Typkonvertierung verursacht."""
Einer**2. = [ 0.
1.
4.
9. 16. 25.
Einer*10 = [ 0 10 20 30 40 50 60 70 80 90]
36.
49.
64.
81.]
Man achte auf die unterschiedlichen Typen von "Quadrate" und "Zehner".
Dies wird durch eine interne Typkonvertierung verursacht.
Doch auch auf klassische Vektoroperationen muss man nicht verzichten:
In [93]: # Vektor definieren
e_x = np.array([1., 0., 0.])
# Vektor definieren
e_y = np.array([0., 1., 0.])
# Skalarprodukt
print "<e_x, e_y> = {}".format(np.dot(e_x, e_y))
# und Kreuzprodukt
e_z = np.cross(e_x, e_y)
5
for name, vec in zip([’e_x’, ’e_y’, ’e_z’], [e_x, e_y, e_z]):
print name+’ = {}’.format(vec)
<e x,
ex =
ey =
ez =
e
[
[
[
y> = 0.0
1. 0. 0.]
0. 1. 0.]
0. 0. 1.]
2.3
Ein erster Plot (matplotlib)
Webpage: http://matplotlib.org/
Citing: Hunter, J. D. Matplotlib: A 2D graphics environment, Computing in Science & Engineering, 9,
90–95 (2007)
Häufig geht es bei der Auswertung von Versuchen darum, Daten möglichst passend darzustellen. Das
ist keine leichte Aufgabe aber zumindest das Erstellen von grundlegenden Abbildungen ist mit python sehr
einfach.
In [94]: #jupyter line magic: sorgt dafür, dass die Plots in die Seite eingebunden werden.
%matplotlib inline
# importiert die Graphik-Bibliothek
import matplotlib.pyplot as plt
#
x
#
y
200 x-Werte zwischen -4 Pi und 4 Pi erstellen
= np.linspace(-4*np.pi, 4*np.pi, 200)
y-Werte berechnen
= np.sin(x)
# Den grundsätzlichen Plot erstellenn
plt.plot(x, y)
# x-Achsen-Beschriftung
plt.xlabel("x")
# y-Achsen-Beschriftung
plt.ylabel("f(x)=sin(x)")
# Titel
plt.title(r"Figure 1: Sinusfunktion im Bereich $x\in(-4\pi, 4\pi)$")
# Plot ausgeben
plt.show()
6
3
Fortgeschrittene Programmstrukturen
Funktionen, Generatoren, if-statements und Schleifen
Im vorangegangenen Abschnitt sind die Basisklassen eingeführt worden und ein erster simpler Plot ist
erstellt worden. Im nächsten Kapitel werden nun etwas fortgeschrittenere Techniken erläutert. Diese erlauben
es den Code besser zu strukturieren und Teile des Codes immer wieder zu verwenden.
Grundsätzlich kann man auch selbst komplett eigene Module entwickeln und dann wie z.B. numpy in den
eigenen Code einbauen. Das geht sogar ohne das Verständinis komplizierten Konstrukte wie Klassen o.Ä.
3.1
Funktionen
Funktionen sind das wohl nützlichste Handwerkszeug um python Code les- und wiederverwendbar zu gestalten. Im Grunde funktionieren sie wie eine einfache Zuordnungsvorschrift x → f(x). Funktionen werden mit
einem def eingeleitet und einen return bestimmt den Rückgabewert:
In [96]: def quadrat(x):
"""Gibt das Quadrat des Eingabewertes zurück"""
return x*x
# Funktioniert sowohl mit einem integer
print quadrat(2)
# als auch mit einem Array:
print quadrat(np.linspace(1,10, 10))
# aber nicht mit einer Liste:
7
print quadrat(range(10))
# Das Programm wird abgebrochen!
4
[
1.
4.
9.
16.
25.
36.
49.
64.
81.
100.]
--------------------------------------------------------------------------TypeError
Traceback (most recent call last)
<ipython-input-96-e7ae568bd5b5> in <module>()
10
11 # aber nicht mit einer Liste:
---> 12 print quadrat(range(10))
13 # Das Programm wird abgebrochen!
<ipython-input-96-e7ae568bd5b5> in quadrat(x)
1 def quadrat(x):
2
"""Gibt das Quadrat des Eingabewertes zurück"""
----> 3
return x*x
4
5 # Funktioniert sowohl mit einem integer
TypeError: can’t multiply sequence by non-int of type ’list’
Mann kann diesen Fehler in einem größeren Programmablauf abfangen. Dazu nutzt man einen try-undexcept-Block:
In [98]: def quadrat_neu(x):
"""Funktion: quadrat_neu(x):
Gibt das Quadrat des Eingabewertes zurück
Außerdem werden Fehler jetzt vernünftig abgefangen"""
try:
x2 = x*x
return x2
# Fängt nur Fehler vom Typ "TypeError" ab
except TypeError, e:
print "Input Variable hat den falschen Type, sie hat den Typ: {}".format(type(x))
print "Error Message:", e
return None
# Funktioniert immer noch sowohl mit einem integer
print "Out1", quadrat_neu(2)
# als auch mit einem Array:
print "Out2", quadrat_neu(np.linspace(1,10, 10))
# und immer noch nicht mit einer Liste.
# Aber der Fehler wird ordentlich abgefangen und das Programm nicht abgebrochen!
print "Out3", quadrat_neu(range(10))
# Das Programm wird nicht abgebrochen!
8
# Außerdem kann man sich den docstring der Funktion anschauen:
print "Out4", quadrat_neu.func_doc
Out1 4
Out2 [
1.
4.
9.
16.
25.
36.
49.
64.
81. 100.]
Out3 Input Variable hat den falschen Type, sie hat den Typ: <type ’list’>
Error Message: can’t multiply sequence by non-int of type ’list’
None
Out4 Funktion: quadrat neu(x):
Gibt das Quadrat des Eingabewertes zurück
Außerdem werden Fehler jetzt vernünftig abgefangen
3.2
Generatoren
Generatoren sind eine ziemlich praktische Sache und können ähnlich verwendet werden wie Funktionen. Eine
detaillierte Beschreibung würde hier zu weit gehen, aber grundsätzlich stellt ein Generator das Grundgerüst
für viele python Implementierungen dar. Häufig sind Generatoren bei sehr großen Schleifendurchläufen hilfreich, oder wenn eine Aktion nur ein einziges Mal durchgeführt werden muss. Ein Generator erzeugt die
Ausgabe-Objekte nur auf “Zuruf” und nicht schon vorab, was eine Menge Speicherplatz sparen kann. Hier
ein einfaches Beispiel:
In [99]: def Fibonacci(n):
"""Fibonacci-Generator:
Generates the n first Fibonacci numbers"""
x, y = 0,1
for i in range(n):
if x < y:
yield x
x=x+y
else:
yield y
y=x+y
# Der Fibonacci-Genrator an sich gibt
print "Out1", Fibonacci(10)
noch keine Werte zurück.
# Erst wenn z.B. in einem Loop darüber iteriert wird, werden nach und nach Werte erzeugt.
print "Out2"
for f in Fibonacci(5):
print f
# Auch in einer list comprehension kann man einen Generator nutzen.
print "Out3", sum([x for x in Fibonacci(10)])
Out1 <generator object Fibonacci at 0x0000000003E335E8>
Out2
0
1
1
2
3
Out3 88
9
3.3
Loops/Schleifen
An der ein oder anderen Stelle sind uns Loops schon begegnet. Grundsätzlich gibt es es in python zwei Arten
von loops: 1. for-each-loop 2. while-loop
Sie tun eigentlich genau das, was der Name sagt. Wichtig ist in python, dass man direkt über viele
Objekte loopen kann, ohne umständlich über die Indizes gehen zu müssen:
In [100]: Farben = [’Blau’, ’Rot’]
# So nicht:
for i in range(len(Farben)):
print Farben[i]
# so auch nicht:
j = 0
while j < len(Farben):
print Farben[j]
j += 1
# Besser so:
for Farbe in Farben:
print Farbe
# oder so, wenn man auch an den Index dran möchte:
for index, Farbe in enumerate(Farben):
print index, Farbe
Blau
Rot
Blau
Rot
Blau
Rot
0 Blau
1 Rot
4
Namen, Objekte und Referenzen
Python hat eine etwas gewöhnungsbedürftige Art Objekte zu referenzieren. Hier soll nur eine kleine Übersicht
gegeben werden. Weiterführende Infos auch zur Speicherverwaltung in python kann man z.B. unter dem
Stichwort “Reference counter” finden.
Grundsätzlich gibt es zwei unterschiedliche Objektarten in python: Mutable (veränderliche) und immutable (unveränderliche) Objekte. Immutable sind z.B. Strings, Tupel und Integer. Der große Rest der python
Objekte ist mutable. Sie verhalten sich beim Referenzieren grundverschieden. Das lässt sich am einfachsten
in einem kleinen Beispiel zeigen:
In [101]: a = 1
print
b = a
print
b = 2
print
# Erzeugt den Namen "a", erzeugt ein Integerobjekt mit dem Wert "1"
# und referenziert "1" mit "a"
"a = {}".format(a)
# Erzeugt den Namen "b", erzeugt eine Kopie vom Objekt auf das "a" referenziert und
# referenziert dieses mit "b"
"b = {}".format(b)
# Erzeugt ein neues Objekt "2", löscht das alte Objekt und referenziert "2" mit "b"
"a, b = {}, {}".format(a, b)
10
a = 1
b = 1
a, b = 1, 2
Hier verhält sich also alles so, wie man es erwarten könnte. b=a legt eine physische Kopie vom Inhalt von
“a” an. Wenn wir diese Kopie dann mit b=2 ändern, ändert sich der Wert von “a” nicht.
Intern passiert leider (und zum Glück für die Performance) noch mehr, da häufig genutzte Objekte, wie
kleine Integer, immer im Speicher bereit gehalten werden und gar nicht erst gelöscht werden.
In [102]: A = [1, 2, 3]
print
B = A
print
B[-1]
print
# Erzeugt den Namen "A", erzeugt eine Liste mit dem Wert "[1, 2, 3]"
# und referenziert "[1, 2, 3]" mit "A"
"A = {}".format(A)
# Erzeugt den Namen "B" und referenziert, das Objekt das "A" referenziert auch
# mit "B". Es wird keine(!) neue Kopie angelegt.
"B = {}".format(B)
= -4
# Ändert das Objekt, das von "B" referenziert wird.
"A, B = {}, {}".format(A, B) #ACHTUNG: Es ändert sich natürlich auch der Wert von "A"
A = [1, 2, 3]
B = [1, 2, 3]
A, B = [1, 2, -4], [1, 2, -4]
Man kann dieses Verhalten gut oder schlecht finden. In der ein oder anderen Situation is es auf jeden Fall
nützlich. Man darf aber auf keinen Fall vergessen, dass python dieses Verhalten hat.
Um eine Kopie einer Liste anzulegen, kann man wie folgt vorgehen:
In [103]: A = [1, 2, 3]
# Erzeugt den Namen "A", erzeugt eine Liste mit dem Wert "[1, 2, 3]"
# und referenziert "[1, 2, 3]" mit "A"
print "A = {}".format(A)
B = A[:]
# Erzeugt den Namen "B" und referenziert eine Kopie des Objektes, welches von
# referenziert wird
print "B = {}".format(B)
B[-1] = -4
# Ändert das Objekt, das von "B" referenziert wird.
print "A, B = {}, {}".format(A, B)
A = [1, 2, 3]
B = [1, 2, 3]
A, B = [1, 2, 3], [1, 2, -4]
Manche andere python Klasse besitzt eine eingebaute Kopierfunktion, z.B. numpy.array.copy(), die
eine Kopie des Objektes anlegt und nicht nur eine neue Referenz.
5
Numerische Rechnungen: numpy
Webpage: http://www.numpy.org/
Citing: Stéfan van der Walt, S. Chris Colbert and Gaël Varoquaux. The NumPy Array: A Structure for
Efficient Numerical Computation, Computing in Science & Engineering, 13, 22-30 (2011)
Bereits in den vergangenen Abschnitten sind einige numpy Funktionen aufgetaucht. Hier werden jetzt
ausgewählte Funktionen von numpy nochmal genauer unter die Lupe genommen. Außerdem werden die
Laufzeitunterschiede verschiedener Techniken beleuchtet.
5.1
N-dimensionales Array: numpy.ndarray()
Das wichtigste Objekt in python ist das n-dimensionale Array, welches die Daten für die wichtigsten Operationen bereit hält. Es ist sozusagen der Container, der die Daten für die mathematischen Operationen
11
zur Verfügung stellt. Das numpy.ndarray stellt selbst schon viele nützliche Funktionen, die mit dem PunktOperator ndarray.function() ausgeführt werden, zur Verfügung.
Doch zu Beginn steht der Zugriff auf die einzelnen Elemente im Array:
In [104]: import numpy as np
# 1-dim Array von 1...9
Arr_1dim = np.linspace(1,27, 27)
print "Out1", Arr_1dim
# Gibt die Form des Arrays an.
print "Out2", Arr_1dim.shape
# Gibt ein Array der ersten drei Elemente zurück.
print "Out3", Arr_1dim[:4]
# Gibt ein Array der letzten drei Einträge zurück.
print "Out4", Arr_1dim[-3:]
# Gibt ein Array zurück, indem nur jeder dritte Eintrag genutzt wird.
print "Out5", Arr_1dim[::3]
# Wir bilden nun ein 3x9 array.
Arr_2dim = Arr_1dim.copy().reshape(3, 9)
print "Out6", Arr_2dim
print "Out7", Arr_2dim.shape
# 1. Spaltenvektor
print "Out8", Arr_2dim[:,0]
# 1. Zeilenvektor
print "Out9", Arr_2dim[0,:]
# Auch ein 3x3x3 Array funktioniert.
Arr_3dim = Arr_1dim.copy().reshape(3, 3, 3)
print "Out10", Arr_3dim
print "Out11", Arr_3dim.shape
# Der Eintrag mit Index (0,0,0)
print "Out12", Arr_3dim[0,0,0]
Out1 [ 1.
2.
3.
4.
5.
6.
7.
8.
9. 10.
16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26.
Out2 (27L,)
Out3 [ 1. 2. 3. 4.]
Out4 [ 25. 26. 27.]
Out5 [ 1.
4.
7. 10. 13. 16. 19. 22. 25.]
Out6 [[ 1.
2.
3.
4.
5.
6.
7.
8.
9.]
[ 10. 11. 12. 13. 14. 15. 16. 17. 18.]
[ 19. 20. 21. 22. 23. 24. 25. 26. 27.]]
Out7 (3L, 9L)
Out8 [ 1. 10. 19.]
Out9 [ 1. 2. 3. 4. 5. 6. 7. 8. 9.]
Out10 [[[ 1.
2.
3.]
[ 4.
5.
6.]
[ 7.
8.
9.]]
[[ 10.
[ 13.
[ 16.
11.
14.
17.
12.]
15.]
18.]]
[[ 19.
[ 22.
[ 25.
20.
23.
26.
21.]
24.]
27.]]]
12
11. 12.
27.]
13.
14.
15.
Out11 (3L, 3L, 3L)
Out12 1.0
Nützliche Funktionen, die jedes Array besitzt:
In [105]: # Summe über alle Elemente
print "Out1", Arr_3dim.sum()
# Der Mittelwert und viele weitere Funktionen sind auch verfügbar
print "Out2", Arr_3dim.mean()
# Man kann auch über nur eine Achse summieren
print "Out3", Arr_3dim.sum(axis=(0))
# Oder auch über mehrere Achsen
print "Out4", Arr_3dim.sum(axis=(0,2))
Out1 378.0
Out2 14.0
Out3 [[ 30.
[ 39. 42.
[ 48. 51.
Out4 [ 99.
33. 36.]
45.]
54.]]
126. 153.]
Das Ganze funktioniert auch in weiteren Dimensionen, ist dann aber natürlich nicht mehr vernünftig zu
visualisieren.
Es gibt noch viele weitere Dinge, die numpy leisten kann, wie beispielsweise die Integration von C/C++
oder FORTRAN code. Auch eine ziemlich mächtige Random-number-Einbindung, als numpy.random, ist
darunter.
5.2
Performance
iPython bietet mit %%timeit eine leichte Line-magic-Funktion, um die Performance von Codeteilen zu testen.
Wir testen hier die Zeit, die zum Aufsummieren der ersten 1000 natürlichen Zahlen benötigt wird:
In [106]: %%timeit
Numbers = np.linspace(1, 1000, 1001)
Sum = 0
for i in range(len(Numbers)):
Sum += Numbers[i]
1000 loops, best of 3: 619 µs per loop
In [107]: %%timeit
Numbers = np.linspace(1, 1000, 1001)
Sum = 0
for n in Numbers:
Sum += n
1000 loops, best of 3: 567 µs per loop
In [108]: %%timeit
np.sum(np.linspace(1, 1000, 1001))
10000 loops, best of 3: 55.3 µs per loop
In [109]: %%timeit
np.linspace(1, 1000, 1001).sum()
13
10000 loops, best of 3: 48.5 µs per loop
Hier wird also deutlich, dass man mit sehr geringem Aufwand die Performance seines Codes signifikant
verbessern kann. Je größer die Arrays werden, desto schneller werden die numpy-Funktionen gegenüber
selbst geschriebenen Schleifen. Als praktischer Nebeneffekt wird der Code auch besser lesbar und leichter zu
verwalten.
Wenn man mit sehr großen Datenmengen zu tun hat, empfiehlt sich ein Blick auf die “pandas”-Bibliothek.
Diese bringt nochmal Performancezuwachs gegenüber numpy und stellt eine Menge Funktionen zur statistischen Analyse bereit.
6
Wissenschaftliche Funktionen: SciPy
Webpage: https://www.scipy.org/
Citing: Jones E, Oliphant E, Peterson P, et al. SciPy: Open Source Scientific Tools for Python, 2001-,
http://www.scipy.org/ [Online; accessed 2017-04-06]
Wie man Funktionen selbst schreibt, ist weiter oben schon vorgestellt worden. Allerdings kann das bei
vielen in der Physik üblichen Funktionen langwierig und fehleranfällig sein. Niemand möchte freiwillig,
Hypergeometrische-, Bessel- oder Sphärisch-Harmonische Funktionen 10.-Ordnung in ein Programm tippen.
Aus diesem Grund bietet scipy eine Menge der gebräuchlichen aber auch weniger üblichen Funktionen in
einem auch auf Performance optimierten Paket an.
Neben diesen in scipy.special hinterlegten Funtionen, beinhaltet scipy auch eine Menge Algorithmen, um
bspw. numerisch zu integrieren oder Nullstellen zu berechnen.
6.1
scipy.special
Eventuell ist die Bessel’sche Differentialgleichung noch aus der Mechanik oder Quantenmechanik bekannt:
2
2
2
f = 0.
Bessel’sche Differentialgleichung x2 ddxf2 + x df
dx + x − ν
Die Lösungen dieser Differentialgleichung sind die Besselfunktionen. Es gibt Besselfunktion 1. Art Jν (x) =
P∞ (−1)r ( x2 )2r+ν
Jν (x) cos(νπ)−J−ν (x)
, welche noch den Parameter ν,
r=0 Γ(ν+r+1)r! und Besselfunktion 2. Art Yν (x) =
sin(νπ)
welcher die Ordnung des Lösung angibt modifiziert wird.
Diese Funktion is in scipy hinterlegt:
In [110]: # Importieren der Besselfunktionen
from scipy.special import jv, yv
# Definitionsbereich für den Plot festlegen
z = np.linspace(0, 20, 200)
# Es sollen die ersten drei Ordnungen geplottet werden
order = [0,1,2]
# Legt die Größe (in inch) der Abbildung fest.
plt.figure(figsize=(7.5, 4))
for o in order:
# Berechnen der Besselfunktion
B = jv(o, z)
#Plotten und Erstellen eines Labels.
plt.plot(z, B, label=r’$\nu$ = ’+str(o))
plt.title(’Besselfunktion 1. Art’)
# Die Label können dann in der Legende verarbeitet werden.
plt.legend(loc=’best’)
plt.show()
14
In [111]: plt.figure(figsize=(7.5, 4))
for o in order:
D = yv(o, z)
plt.plot(z, D, label=r’$\nu$ = ’+str(o))
plt.title(’Besselfunktion 2. Art’)
plt.ylim(-2, .6)
plt.legend(loc=’best’)
plt.show()
15
Man kann bei der Bessel-Funktion 2. Art die (logarithmische) Polstelle an der Stelle x = 0 erkennen.
6.2
Numerische Integration: scipy.integrate.quad
Um die Numerische Integration wird man früher oder später in der wissenschaftlich Laufbahn nicht rum
kommen. Selbst, wenn sich Ergebnisse noch analytisch aufschreiben lassen, ist die Auswertung der Lösungen
häufig nur noch numerisch möglich. Hier wird ein einfaches Beispiel betrachtet, welches mit der analytischen
Lösung verglichen werden kann:
In [112]: # quad ist eine einfache, aber oft ausreichende, Integrationsroutine
from scipy.integrate import quad
def g(x):
"""Funktion, die integriert werden soll"""
return x
def f(x):
"""Stammfunktion für den Fall, dass x_0=0 ist"""
return 0.5*x*x
# Berechnung des Integrals
print ’F(x;x_0) = {}’.format(quad(g, 0, 5)[0])
def F(x0, x1, func=f):
"""Eine Funktion, die sich wie die Stammfunktion verhält
Sie funktioniert sohl für einzelne Werte als auch für Arrays."""
try:
F=np.array([quad(func, x0, x1) for x0, x1 in zip(x0, x1)])
return F[:,0]
except TypeError:
F = quad(func, x0, x1)
return F[0]
# Untere Grenze
x0 = np.zeros(100)
# Obere Grenze
x1 = np.linspace(0,10,100)
plt.plot(x1, f(x1), label=’exact’)
plt.plot(x1, F(x0, x1, g), linestyle=’-.’, label=’quad’)
plt.legend(loc=’best’)
plt.title(’Vergleich zwischen numerischer Integration \n und exaktem Ergebnis’)
plt.show()
F(x;x 0) = 12.5
16
Man erkennt, dass für eine so einfache, glatte Funktion die numerische Integration sehr gut funktioniert.
Sollte die Funktion sich nicht so gutartig verhalten, oder über einen sehr großen Bereich ausgewertet werden
müssen, bietet quad eine Vielzahl an Optionen, um Einfluss auf die Integration zu nehmen. Auch kann man
natürlich im Vorfeld aus der Analysis bekannte Techniken, wie Substituation oder das Zerlegen der Funktion
in kleine Abschnitte, nutzen, um bessere Resultate zu erzielen. Welches Verfahren zum Erfolg führt, hängt
selbstverständlich stark vom gegebenen Problem.
6.3
Nullstellensuche: scipy.optimize.root
Eine weitere häufige Problemstellung ist die Suche von Nullstellen. Das ist numerisch nicht so einfach und
vor allem das Finden von mehreren Nullstellen (oder gar allen) ist sehr aufwendig. Die Suche funktioniert
häufig nur mit der Vorgabe eines Startwertes. Ob eine Nullstelle gefunden werden kann und wenn ja welche,
hängt auf jeden Fall von der Wahl des Startwertes ab.
Betrachten wir das einfache Beispiel einer nach unten verschobenen Parabel:
In [113]: # Import der passenden Funktion
from scipy.optimize import root
def f(x):
"""Nach unten verschobene Parabel
Nullstellen sind x_1=-2**0.5 und x_2=2**0.5"""
return x*x-2
def jac(x):
"""Jacobi-Matrix der zu untersuchenden Funktion
Nicht nötig, aber hilfreich. Erhöht die Konvergenzwahrscheinlichkeit."""
return 2*x
17
# Ausgabe der Nullstelle (inklusive vieler weiterer Infos, wenn gewünscht)
print "x_1={}".format(root(f, x0=-0.1, jac=jac).x[0])
# Die gefundene Nullstelle hängt vom Startwert ab
print "x_2=+{}".format(root(f, x0=0.1, jac=jac).x[0])
x 1=-1.41421356237
x 2=+1.41421356237
6.4
Regression / Funktionsfitting: scipy.optimize.curve fit
Als letztes betrachten wir die Aufgabe, eine bekannte Funktion an Messdaten anzupassen. Diese Aufagbe
sollte aus dem Praktikum bekannt sein und lässt sich sehr leicht mit python lösen. Wie bei allen numerischen
Verfahren, gilt auch hier, dass eine vorsichtige Auswahl der Paramter zum Fitten nötig ist, um eine stabile
Lösung zu finden, welche im besten Fall auch noch gegen die echte Lösung konvergiert.
Im Beispiel betrachten wir eine exponentiell abklingende Sinus-Funktion, bei der wir die Zerfallszeit τ
und die Amplitude A bestimmen wollen. Die Messungenauigkeit wird durch sog. Weißes Rauschen simuliert,
welches einer Normalverteilung folgt. Es handelt sich also um eine Idealisierung des Messprozesses, welcher
die Konvergenz des Fittings verbessert.
In [114]: # Import von curve_fit
from scipy.optimize import curve_fit
def Expectation(x, A, tau):
"""Funktion, die unseren Prozess beschreibt.
"""
E = A*np.sin(x)*np.exp(-x/tau)
return E
def Expectation2(x, A, tau, phi_0):
"""Alternative Funktion, um einen misslungenen Fit zu zeigen"""
E = A*np.cos(x)*np.exp(-x/tau)
return E
# Messpunkte
x = np.linspace(0, 20, 50)
# Weißes Rauschen, mit Mittelwert E=0 und Varianz Var=0.8
white_noise = np.random.normal(0.,0.8, 50)
# Messdaten, erzeugt aus dem echten Wert verschmiert mit dem Rauschen.
Data = Expectation(x, 10., 4.)+white_noise
# Plotten der Messwerte.
plt.plot(x, Data, linewidth=0., marker=’o’, label=’Data’)
# Plotten der echten Lösung
plt.plot(x, Expectation(x, 10., 4.), label="True solution")
# Curve Fit, die zu fittenden Parameter werden auomatisch ermittelt.
# popt enhält die best-fit Parameter.
# pcov enthält die Kovarianzmatrix, aus der sich der Fehler berechnen lässt.
popt, pcov = curve_fit(Expectation, x, Data)
# Plot der best-fit Funktion
plt.plot(x, Expectation(x, *popt), linestyle=’-.’, label=’Fit’)
print "Fit1: A={}({}), tau={}({})".format(np.round(popt[0], 1), np.round(np.sqrt(np.diag(pcov)
np.round(popt[1], 1), np.round(np.sqrt(np.diag(pcov))[1],
18
# Das gleiche nochmal für dem anderen Fit.
popt, pcov = curve_fit(Expectation2, x, Data)
plt.plot(x, Expectation2(x, *popt), linestyle=’--’, label=’Fit2’)
print "Fit2: A={}({}), tau={}({})".format(np.round(popt[0], 1), np.round(np.sqrt(np.diag(pcov)
np.round(popt[1], 1), np.round(np.sqrt(np.diag(pcov))[1],
plt.legend(loc=’best’)
plt.show()
Fit1: A=10.0(0.7), tau=4.6(0.5)
Fit2: A=2.6(inf), tau=1.6(inf)
7
Anhang
7.1
The zen of python
• Beautiful is better than ugly.
• Explicit is better than implicit.
• Simple is better than complex.
• Complex is better than complicated.
• Flat is better than nested.
• Sparse is better than dense.
• Readability counts.
• Special cases aren’t special enough to break the rules.
• Although practicality beats purity.
19
• Errors should never pass silently.
• Unless explicitly silenced.
• In the face of ambiguity, refuse the temptation to guess.
• There should be one—and preferably only one—obvious way to do it.
• Although that way may not be obvious at first unless you’re Dutch.
• Now is better than never.
• Although never is often better than right now.
• If the implementation is hard to explain, it’s a bad idea.
• If the implementation is easy to explain, it may be a good idea.
• Namespaces are one honking great idea—let’s do more of those!
|Tim Peters
Programs must be written for people to read, and only incidentally for machines to execute.
|Abelson & Sussman, Structure and Interpretation of Computer Programs
7.2
Nützliche Links
Hilfreiche Hinweise, wie man einen Code im python-Stil schreiben sollte: good code (pdf) Englisch
Anleitung was man alles besser vermeiden sollte: bad code (webpage) English
20
Herunterladen