2. Übung
Beispiel 2.1
Bislang haben wir zwar einiges ausprobiert, um Python etwas näher kennenzulernen, haben aber noch keine richtige Aufgabe im Sinne der Informationsverarbeitung umgesetzt. Das wollen wir nun ändern, indem wir einen Algorithmus zur Berechnung der Sinus-Funktion programmieren. Vom Taschenrechner her ist man gewohnt, dass es eine dedizierte Taste mit der Aufschrift sin() zur Berechnung dieser Funktion gibt. Aber die wenigsten werden sich schon einmal gefragt haben, wie die Berechnung im Taschenrechner umgesetzt ist. Man könnte z.B. eine große Wertetabelle im Speicher ablegen, aber es geht viel einfacher und eleganter mit den für die trigonometrischen Funktionen bekannten Reihenentwicklungen. (Die hatten Sie bestimmt schon einmal in der Mathematik!)
Natürlich können wir nicht unendlich viele Reihenglieder aufsummieren, sondern müssen die Reihenentwicklung nach irgendeinem Glied abbrechen. Das ist schon deshalb kein reales Problem, weil unsere Berechnungen im Rechner sowieso nur eine begrenzte Genauigkeit aufweisen. Wir wollen uns der Sache aber zunächst ganz einfach und pragmatisch nähern und beschränken uns in unserem ersten Lösungsansatz auf die ersten 3 Reihenglieder, wie sie in Gleichung (2.3) zu sehen sind:
1# Kleinen Text ausgeben, damit der Nutzer weiß, was hier passiert
2print('Berechnung von sin(x) mittels endlicher Reihe')
3
4# x zunächst einmal einen festen Wert zuweisen
5x = 3.1415/4
6
7# und dann die sin-Näherung mit den ersten 3 Reihengliedern berechnen
8sin_x = x/1 - (x*x*x)/(3*2*1) + (x*x*x*x*x)/(5*4*3*2*1)
9
10# augeben sollten wir das Ergebnis natürlich auch noch
11print('sin(',x,') = ', sin_x)
(Wir denken natürlich auch immer daran, die Programmdatei unter dem entsprechenden Namen - hier: example_2_1.py - abzuspeichern.)
Bevor wir uns Gedanken darüber machen, wie wir die eigentliche Sinus-Berechnung verbessern können, wollen wir das Programm etwas nutzerfreundlicher gestalten, indem wir das Argument x nicht fest vorgeben, sondern von der Konsole einlesen. Für einfache Benutzereingaben gibt es in Python die Funktion input(), die eine Konsoleneingabe entgegen nimmt und dabei solange wartet, bis die 'Return'-Taste betätigt wird:
1# Kleinen Text ausgeben, damit der Nutzer weiß, was hier passiert
2print('Berechnung von sin(x) mittels endlicher Reihe')
3
4# Wir lesen x von der Konsole ein, dabei können wir 'input()' einen
5# String übergeben, mit dem wir dem Nutzer sagen, was wir von ihm wollen
6x = input('Geben Sie x ein! ')
7
8# und dann die sin-Näherung mit den ersten 3 Reihengliedern berechnen
9sin_x = x/1 - (x*x*x)/(3*2*1) + (x*x*x*x*x)/(5*4*3*2*1)
10
11# augeben sollten wir das Ergebnis natürlich auch noch
12print('sin(',x,') = ', sin_x)
Leider wird beim Ausführen des Skripts eine für uns zunächst unverständliche Fehlermeldung ausgegeben und das Programm wird abgebrochen:
1Traceback (most recent call last):
2 File "c:/Users/holtv/Documents/IPP/ipp_1.py", line 396, in <module>
3 exercise_1_2_1()
4 File "c:/Users/holtv/Documents/IPP/ipp_1.py", line 133, in exercise_1_2_1
5 sin_x = x/1 - (x*x*x)/(3*2*1) + (x*x*x*x*x)/(5*4*3*2*1)
6TypeError: unsupported operand type(s) for /: 'str' and 'int'
Python beschwert sich darüber, dass wir eine Operation mit einem nicht unterstützten Datentyp durchführen wollen. Wir haben nämlich leider vergessen, dass die Nutzereingabe, die input() uns liefert eine Zeichenkette (String) darstellt und keine Zahl ist! Dieses Problem lässt sich aber einfach beheben, denn in Python können wir einen String leicht in einen anderen Datentyp konvertieren. Wir müssen nur die Zeile 6 wie folgt abwandeln:
x = float(input('Geben Sie x ein! '))
Nun sollte unser Programm fehlerfrei einen Sinuswert berechnen ... es sei denn, wir geben statt einer Zahl eine Zeichenkette ein, die auch andere Zeichen als Ziffern enthält. Dann bricht Python mit einer anderen Fehlermeldung ab, weil es den String nicht konvertieren kann (ausprobieren!).
Aufgabe 2.1
Unsere Sinus-Berechnung erwartet die Eingabe des Arguments bislang in Radiant. Für uns Menschen ist das Denken in Vielfachen von \(\pi\) aber i.d.R. eher unanschaulich. Darum wollen wir unser Programm so erweitern, dass die Eingabe von x in Grad erfolgen kann.
To Do
Erweitern Sie das Beispiel 2.1 so, dass:
Die Nutzer darüber informiert werden, dass x in Grad einzugeben ist.
- Das Argument von Grad in Radiant umgewandelt wird.\((x_{rad} = x_{grad} \cdot \frac{\pi}{180})\)
Bei der Ausgabe am Ende wieder sin(x in Grad) erscheint.
Beispiel 2.2
Wir knüpfen direkt an Beispiel 2.1 und Aufgabe 2.1 an. Zwei Dinge sind in unserer bisherigen Lösung noch sehr unelegant, wir berechnen nämlich sowohl die Potenzen von x wie auch n! noch "zu Fuß", d.h. wir multiplizieren die Ausdrücke im Progammcode fest verdrahtet aus.
Für die Potenzberechnung gibt es direkt einen Potenz-Operator in Python '**', den wir nun nutzen werden. Für die Fakultätsberechnung wollen wir aber unsere erste eigene Funktion schreiben. Bei der Nutzung von print() und input() haben wir schon gesehen, dass wir Funktionen Argumente übergeben, mit denen diese arbeiten können und das Funktionen uns auch Daten zurückliefern können. Eine eigene Funktion definieren wir in Python wie folgt:
1def funktions_name(argument_1, argument_2, ...):
2 # die nun folgenden Anweisungen müssen einheitlich eingerückt sein!
3 anweisung_1
4 anweisung_2
5 ...
6
7 # mit 'return' können wir keinen, einen oder mehrere Werte als Ergebnis zurückliefern
8 return Rückgabewert(e)
9
10# ab hier gehört der Code nicht mehr zur Funktion, da er nicht eingerückt ist
11anweisung_3
12anweisung_4
13...
Die wesentlichen Hinweise zur Gestalt von Funktionen sind im o.a. Code-Ausschnitt in den Kommentaren enthalten. Im ersten Schritt wollen wir nun nicht gleich eine Funktion erstellen, die allgemein für jeden Wert die Fakultät berechnet, sondern verlagern nur unsere bisherige "zu-Fuß"-Berechnung in eine Funktion:
1# Das ist unsere neue Funktion zur Fakultätsberechnung
2def fak(n):
3 if n == 1:
4 return 1
5 if n == 3:
6 return 3*2*1
7 if n == 5:
8 return 5*4*3*2*1
9 else:
10 # 'None' ist ein vordefinierter Wert, der anzeigt, dass es kein (gültiges) Ergebnis gibt
11 return None
12
13# Hier beginnt unser eigentliches Programm
14print('Berechnung von sin(x) mittels endlicher Reihe')
15
16# Einlesen von x in grad und umwandeln in rad
17...
18
19# Berechnung des Sinus unter Nutzung des Potenz-Operators '**' und der Funktion 'fak(n)'
20sin_x = x_rad**1/fak(1) - (x_rad**3)/fak(3) + (x_rad**5)/fak(5)
21
22# Ausgabe des Ergebnis
23...
(Bei '...' ist der Code, den wir in Aufgabe 2.1 erstellt haben, einzusetzen. In Zeile 20 heißt das Argument hier x_rad.)
Wir schauen uns zunächst die Funktion fak(n) an. Das Argument n ist hierbei ein Platzhalter für den numerischen Wert, den wir aktuell übergeben und von dem wir die Fakultät berechnen sollen. Die eigentliche Berechnung führen wir zunächst weiter "zu Fuß" durch, indem wir abfragen, welcher Wert für n der Funktion übergeben worden ist und dann die entsprechende Berechnung ausführen und mit return als Ergebnis zurückliefern. Für die Abfrage verwenden wir ein uns noch unbekanntes Programmierkonstrukt, ein if-else-Konstrukt:
... Anweisungen vor dem 'if-else'
# nach der Bedingung muss am Ende ein ':' stehen
if Bedingung:
# dieser eingerückte Progammabschnitt wird nur ausgeführt, wenn die
# Bedingung den Wert 'True' ergab
... tue dies
... und noch mehr
# auch hinter dem 'else' ist ein ':' zwingend vorgeschrieben
else:
# dieser Block wird nur ausgeführt, wenn die Bedingung nicht erfüllt war
... tue das
... und noch mehr
# wenn die Einrückung zu Ende ist, geht der Programmablauf für alle
# Ausführungszweige gemeinsam weiter
... Anweisungen nach dem 'if-else'
Als Bedingung kann ein beliebig komplizierter Ausdruck eingesetzt werden, der einen der Werte True oder False ergibt. Es können aber auch Berechnungen als Bedingung angegeben werden, die einen numerischen Wert liefern. Dann gilt
In unserem Fall wird die Bedingung in der Form eines Vergleichs, z.B. 'n == 3', abgeprüft für den wir '==' als Vergleichszeichen verwenden, um dies von der Zuweisung mit '=' zu unterscheiden. Daneben gibt es viele weitere (Vergleichs-)Operatoren, die in Bedingungen verwendet werden können. Auf diese ziemlich stupide Weise haben wir die Berechnung der drei Fakultätswerte für 1, 3 und 5 in unsere Funktion fak(n) gepackt, können diese in Zeile 20 einfach aufrufen mit fak(1), fak(3) und fak(5) und bekommen jeweils den passenden Wert zurückgeliefert. Der Aufruf unserer selbst geschriebenen Funktion sieht im Übrigen genauso aus, wie der Aufruf der vordefinierten Funktionen print() und input().
Wenn wir uns jetzt allerdings ins Gedächtnis rufen, dass der Python-Interpreter das Programm zeilenweise abarbeitet, können wir allerdings ins Grübeln geraten. Aber der Python-Interpreter ist durchaus clever, er erkennt die Funktionsdefinition von fak() und überspringt diese. Tatsächlich führt er nacheinander alle nicht eingerückten Zeilen unseres Programms aus, egal an welcher Stelle im Programmcode diese stehen. (Daraus folgt, dass wir im Prinzip unsere Funktionen - umfangreichere Programme vorausgesetzt - mehr oder weniger wild über die Datei verteilen könnten. Dies ist aber kein guter Programmierstil!)
Merke
Wichtig bei der Definition von Funktionen ist, dass die Definition stets vor dem ersten Aufruf der Funktion erfolgen muss, damit diese an der Stelle des Aufrufs schon bekannt sind. (Der Python-Interpreter führt die Funktionen zwar nicht aus, aber er merkt sich, dass es sie gibt!)
Ein Testlauf unseres geänderten Programms sollte die gleiche Ausgabe wie vorher ergeben, da wir keine für die Nutzer sichtbaren Änderungen vorgenommmen, sondern nur intern umstrukturiert haben.
Der nächste logische Schritt ist es nun, die wirklich unelegante hart codierte fak()-Funktion so zu modifizieren, dass wir die Fakultät ganz allgemein für beliebige Argumente n berechnen können. Um auf eine Idee zu kommen, wie wir die Berechnung in einen geschickten Algorithmus in Python umsetzen können, werfen wir mal einen Blick auf die Bestimmung für die ersten aufeinanderfolgenden Werte:
Bei genauem Hinsehen fällt uns auf, dass sich \(n!\) auch wie folgt berechnen lässt:
Eine solche Berechnungsvorschrift, bei der die Berechnung des n-ten Wertes auf die Berechnung des (n-1)-ten Wertes zurückgeführt wird, nennt man Rekursion. Man könnte jetzt auch in Python eine rekursive Programmlösung zur Berechnung der Fakultät implementieren. Wir wollen allerdings an dieser Stelle auf eine sog. iterative Lösung hinarbeiten. Dazu formulieren wir die Bestimmung der ersten aufeinanderfolgenden Werte mit unserer neuen Erkenntnis noch einmal um:
Damit machen wir einen ersten Ansatz, mit dem wir zumindest die Fakultät bis n=5 berechnen können:
1def fak(n):
2 # Startwert der Berechnung
3 # Wir berechnen mindestens immer 0!
4 i_n = 0 # i_n = Iterator i von 0 bis n
5 # Hiermit merken wir uns jeweils den zuletzt berechneten
6 # Fakultätswert
7 n_fak = 1 # 0! = 1
8
9 # wir überprüfen, ob wir n schon erreicht haben ...
10 if i_n < n:
11 # wenn nicht gehen wir eine "Zeile weiter" => n! = (n-1)! n
12 i_n = i_n + 1
13 n_fak = n_fak * i_n # n_fak = 1!
14 # wir überprüfen, ob wir n schon erreicht haben ...
15 if i_n < n:
16 # wenn nicht gehen wir eine "Zeile weiter" => n! = (n-1)! n
17 i_n = i_n + 1
18 n_fak = n_fak * i_n # n_fak = 2!
19 # wir überprüfen, ob wir n schon erreicht haben ...
20 if i_n < n:
21 # wenn nicht gehen wir eine "Zeile weiter" => n! = (n-1)! n
22 i_n = i_n + 1
23 n_fak = n_fak * i_n # n_fak = 3!
24 # wir überprüfen, ob wir n schon erreicht haben ...
25 if i_n < n:
26 # wenn nicht gehen wir eine "Zeile weiter" => n! = (n-1)! n
27 i_n = i_n + 1
28 n_fak = n_fak * i_n # n_fak = 4!
29 # wir überprüfen, ob wir n schon erreicht haben ...
30 if i_n < n:
31 # wenn nicht gehen wir eine "Zeile weiter" => n! = (n-1)! n
32 i_n = i_n + 1
33 n_fak = n_fak * i_n # n_fak = 5!
34 # Wir könnten jetzt ewig so weiter machen um n! für n > 5 zu berechnen,
35 # aber das machen wir gleich eleganter...
36 return n_fak
Wir testen unsere neue Version der fak()-Funktion und stellen hoffentlich fest, dass sie weiterhin die gleichen, verlässlichen Ergebnisse liefert.
Merke
In der Funktion fak() benutzen wir die beiden Variablen i_n und n_fak. Weil wir diese Variablen innerhalb der Funktion (des eingerückten Abschnitts) definiert haben, sind beide auch nur innerhalb der Funktion bekannt! Wenn wir versuchen würden im Hauptprogramm, von dem aus wir die Funktion aufrufen, auf die Variablen zuzugreifen, sind diese dort unbekannt. Auch, wenn wir im Hauptprogramm 2 Variablen gleichen Namens anlegen, handelt es sich um getrennte Variablen mit ihnen eigenen Werten!
Bei der Implementierung ist uns natürlich aufgefallen, dass der Code für jeden Berechnungsschritt stets identisch ist. Nur die Variablenwerte von i_n und n_fak ändern sich in jedem Schritt. Da wäre es schön, wenn solche sich immer wiederholenden Berechnungen von Python direkt unterstützt würden durch ein entsprechendes Konstrukt. Und in der Tat gibt eine ganze Reihe verschiedener Programmierkonstrukte sowohl in Python wie in anderen Programmiersprachen, die solche oft auftretenden sich wiederholenden Berechnungen unterstützen. Wir lernen als erstes die while-Schleife kennen:
... Anweisungen vor der 'while'-Schleife
# nach der Bedingung muss am Ende ein ':' stehen
while Bedingung:
# dieser eingerückte Progammabschnitt wird solange ausgeführt, wie die
# Bedingung den Wert 'True' ergibt / erfüllt ist
... tue dies
... und noch mehr
# wenn die Einrückung zu Ende ist, geht der Progammablauf nach dem Ende der Schleife hier weiter
... Anweisungen nach der 'while'-Schleife
Wichtig ist, dass im Schleifenkörper (so heißt der eingerückte Programmabschnitt innerhalb der while-Schleife) eine Anweisung mit Bezug zur Bedingung enthalten ist. Wenn die Bedingung nie erfüllt wird, bleibt das Programm bis in alle Ewigkeit in der Schleife!
Ahnen Sie schon, wie leicht der Übergang unserer bisherigen Lösung mit den if-else-Konstrukten auf eine while-Schleife ist? Wir packen einfach die sich immer wiederholenden Berechnungsschritte in den Schleifenkörper und die Bedingung können wir auch direkt übernehmen:
1def fak(n):
2 # Startwert der Berechnung
3 # Wir berechnen mindestens immer 0!
4 i_n = 0 # i_n = Iterator i von 0 bis n
5 # Hiermit merken wir uns jeweils den zuletzt berechneten
6 # Fakultätswert
7 n_fak = 1 # 0! = 1
8
9 # wir überprüfen, ob wir n noch nicht erreicht haben ...
10 while i_n < n:
11 # ... und solange gehen wir eine "Zeile weiter" => n! = (n-1)! n
12 i_n = i_n + 1
13 n_fak = n_fak * i_n
14
15 return n_fak
Diese Lösung sieht doch recht kompakt und verständlich aus, oder? Falls wir dem "Frieden" nicht so recht trauen und genauer wissen möchten, was in jedem Schritt passiert, könnten wir uns in jedem Schleifendurchlauf die Werte der Variablen ausgeben lassen.
Anmerkung
Eine alternative Möglichkeit, sich den Programmablauf Schritt für Schritt anzuschauen, besteht in der Verwendung eines sog. Debuggers. Mit dessen Hilfe kann man jede Anweisung einzeln nacheinander ausführen und sich dabei die Werte aller Variablen anschauen. In nahezu allen Entwicklungsumgebungen sind Debugger integriert. Da die Bedienung je nach Entwicklungsumgebung etwas anders aussieht, macht eine "allgemeine" Darstellung hier keinen Sinn. Wir werden aber im Labor einmal gemeinsam mit dem in VS Code integrierten Debugger durch das Programm laufen.
Aufgabe 2.2
Nun ist der Zeitpunkt gekommen, um den letzten großen Schritt zu tun: die allgemeine Formulierung unseres Programms für die Reihenentwicklung der Sinus-Funktion.
Auch, wenn wir es hier - anders als bei der Fakultät - mit einer Reihe zu tun haben, können wir bei der Überlegung zur Formulierung des Algorithmus sehr ähnlich vorgehen. In diesem Fall haben wir es nämlich mit einer - theoretisch unendlichen - Menge stets gleich zu berechnender Summanden zu tun. Das n. Reihenglied ergibt sich nämlich wie folgt:
Um die Reihe zu berechnen können wir iterativ so vorgehen:
Hierbei stehen Ausdrücke wie \(\sum_{i=0}^{n}\) als Platzhalter für die Teilsumme der ersten n Reihenglieder.
To Do
Ändern Sie den bisherigen Code so ab, dass die hart codierte Zeile zur Berechnung der Reihe durch eine while-Schleife ersetzt wird, die nach dem 10. Reihenglied abbricht.
Lösungshinweise:
das o.a. Summenglied bildet den Kern des Schleifenkörpers
die in den Formeln enthaltene Variable i muss in jedem Schleifendurchlauf inkrementiert werden
die Variable i dient auch zur Überprüfung der Abbruchbedingung, ob das 10. Reihenglied erreicht ist
Beispiel 2.3
Natürlich gibt es auch eine bereits fertige Implementierung der sin()-Funktion in Python. Diese lässt sich allerdings nicht einfach so verwenden wie wir es bei print() und input() getan haben. Die beiden letzteren gehören zu den fest in Python integrierten Funktionen, die standardmäßig zur Verfügung stehen. Die allermeisten Funktionen (und das sind wirklich sehr, sehr viele!) stehen als Module/Bibliotheken zur Verfügung. I.d.R. sind in einem Modul mehrere inhaltlich verwandte Funktionen zusammengefasst. Wir werden später auch selber Module erstellen, wollen aber zunächst einmal deren Nutzung ins Auge nehmen. Um ein Modul verwenden zu können, müssen wir es in unserem Programm importieren/laden. Die sin()-Funktion finden wir im Modul mit dem Namen math, daher müssen wir dieses importieren:
# der Import muss vor der ersten Verwendung stehen, kann ansonsten
# aber an jeder Stelle im Programm stehen
import math
Um eine Funktion aus dem Modul zu nutzen, müssen wir deren Namen und die Parameter kennen. Bei der sin() sieht das Ganze dann so aus:
# der Import muss vor der ersten Verwendung stehen, kann ansonsten
# aber an jeder Stelle im Programm stehen
import math
x = 3.1415/4.0
# Aufruf einer Funktion aus dem Modul im Format "Modulname.Funktionsname"
sin_x = math.sin(x)
Anhand des Aufrufs mit vorangestelltem Modulnamen wird gleich deutlich zu welchem Modul die sin()-Funktion gehört. Das sieht auf den ersten Blick vieleicht etwas umständlich aus, hat aber den Vorteil, dass es keinen Konflikt mit u.U. gleichnamigen Funktione in anderen Modulen gibt. Man spricht in diesem Zusammenhang auch von sog. Namensräumen. Wenn uns das zu umständlich erscheint, können wir aber auch anders vorgehen:
# Wir importieren nur die sin()-Funktion aus dem Modul...
from math import sin
x = 3.1415/4.0
# ... und können diese dann ohne Modulnamensvorsatz aufrufen
sin_x = sin(x)
Wenn wir mehrere Funktionen aus dem Modul nutzen wollen, können wir mit einer Wildcard auch alle Funktionen des Moduls importieren und direkt aufrufen:
# Wir importieren alle Funktionen aus dem Modul...
from math import *
x = 3.1415/4.0
# ... und können diese dann ohne Modulnamensvorsatz aufrufen
sin_x = sin(x)
cos_x = cos(x)
Das hat wiederum den Nachteil, dass sich gleichnamige Funktionen leichter ins Gehege kommen und nicht direkt ersichtlich ist, zu welchem Modul die Funktionen gehören. Daher hat der Aufruf mit vorangestelltem Modulnamen durchaus seinen Charme. Und falls uns nur das dauernde Schreiben von math zu lang ist, gibt es auch hier Abhilfe, denn man kann einem Modul beim Import einen anderen Namen zuweisen, sozusagen eine Art "Spitznamen":
import math as m
x = 3.1415/4.0
# Aufruf einer Funktion aus dem Modul im Format "Modul(spitz)name.Funktionsname"
sin_x = m.sin(x)
Nun müssen wir nur noch aufpassen, dass wir keine Variable oder Funktion mit der Bezeichnung m anlegen, denn dann gibts Probleme.
Merke
Für viele Module haben sich in der Python-Gemeinde Standards für die abkürzenden "Kurz"-Bezeichnungen vieler Module herausgebildet. So wird beispielsweise die Numerik-Bibliothek numpy immer als np importiert und die Bildverarbeitungs-Bibliothek opencv als cv.
Mit diesem Wissen können wir unsere Aufgabenstellung zur Sinus-Berechnung nun so schreiben:
1# Modul importieren
2import math
3
4print('Berechnung von sin(x) mittels endlicher Reihe')
5
6x_deg = float(input('Geben Sie x in Grad ein! '))
7# im Modul 'math' sind auch wichtige mathematische Konstanten wie PI definiert
8x_rad = x_deg/180.0*math.pi
9# zur Berechnung nutzen wir nun die vordefinierte sin()-Funktion
10sin_x = math.sin(x_rad)
11
12print('sin(',x_deg,') = ', sin_x)
Aufgabe 2.3
Zum Abschluss dieser Übungseinheit soll jetzt eine etwas umfangreichere Aufgabe möglichst eigenständig bearbeitet werden.
To Do
Wir wollen das Ergebnis unserer selbst programmierten sin()-Funktion mit der im Modul math definierten Funktion einmal direkt vergleichen. Damit das auch programmiertechnisch elegant gelingt, soll im ersten Schritt unsere Sinusberechnung in eine Funktion gepackt werden:
my_sin(x):
... # hier muss die Berechnung hin
return sin_x
Dann muss das Hauptprogramm noch geändert werden:
Es muss sowohl math.sin() wie my_sin() aufgerufen werden und die Rückgabewerten in unterschiedlichen Variablen gespeichert werden.
Es müssen 2 print()-Ausgaben für beide Ergebniswerte erfolgen.