Skip to content
Snippets Groups Projects
Commit a949bd8c authored by Prof. Dr. Robert Jäschke's avatar Prof. Dr. Robert Jäschke
Browse files

+ direkter PythonTutor-Link

parent 17a1020a
No related branches found
No related tags found
No related merge requests found
%% Cell type:markdown id: tags:
# Seminar Problemorientierte Programmierung
## 6 Ertragreiche Funktionen
[Chapter 6: Fruitful function](http://greenteapress.com/thinkpython2/html/thinkpython2007.html)
Viele Python-Funktionen die wir bis jetzt genutzt haben, wie z.B. die Mathematik-Funktionen aus dem `math`-Modul, erzeugen Rückgabewerte (*return values*). Aber die meisten Funktionen die wir selber geschrieben haben sind "leer": sie bewirken etwas, beispielsweise die Ausgabe eines Wertes (mit Hilfe der `print`-Funktion) oder die Bewegung einer Schildkröte, aber sie haben keinen Rückgabewert. In diesem Kapitel werden wir lernen, wie wir "ertragreiche Funktionen", also solche mit Rückgabewert, schreiben können.
![RFC 1149.5 specifies 4 as the standard IEEE-vetted random number.](https://imgs.xkcd.com/comics/random_number.png)
([Random Number](https://xkcd.com/221/), Randall Munroe)
### Ihre Lernziele:
Beschreiben Sie in 2-3 Stichpunkten kurz was Sie im Seminar heute lernen wollen. Klicken Sie dazu doppelt auf diesen Text und bearbeiten Sie dann den Text:
-
-
-
%% Cell type:markdown id: tags:
## Exkurs: Was mir an Python gefällt
Das Modul os stellt Funktionen bereit, um Funktionalitäten des Betriebssystems zu nutzen. Beispielsweise können wir damit Verzeichnis-Inhalte auflisten, durch Verzeichnisse navigieren, Informationen zu Dateien bekommen und Dateieigenschaften verändern. Das folgende Programm gibt eine Liste aller Jupyter-Notebooks im aktuellen Verzeichnis zusammen mit der Dateigröße aus und berechnet die Gesamtgröße der Dateien:
%% Cell type:code id: tags:
```
## Modul 6
import os
# Tabellenkopf ausgeben
print("Bytes\tName")
print("--------------------------------------------------------------")
# Gesamtgröße in Bytes
bytes_sum = 0
# Inhalt des aktuellen Verzeichnisses durchlaufen
for entry in os.scandir():
if entry.is_file() and entry.name.endswith(".ipynb"):
size = entry.stat().st_size
bytes_sum +=size
print("{:5d}".format(size), entry.name, sep='\t')
print("--------------------------------------------------------------")
print(bytes_sum, "bytes =", bytes_sum/1000, "kilobytes =", bytes_sum/1000000, "Megabytes")
```
%% Cell type:markdown id: tags:
### 6.1 Rückgabewerte
Der Aufruf einer Funktion erzeugt einen Rückgabewert, den wir üblicherweise einer Variable zuweisen oder als Teil eines Ausdrucks verwenden:
```python
e = math.exp(1.0)
height = radius * math.sin(radians)
```
Die (meisten) Funktionen, die wir bisher geschrieben haben sind "leer" - sie haben keinen Rückgabewert. Präziser ausgedrückt ist ihr Rückgabewert `None` (also nichts).
In diesem Kapitel schreiben wir (endlich) ertragreiche Funktionen. Das erste Beispiel ist die Funktion `kreisflaeche`, die die Fläche eines Kreises für einen gegebenen Radius berechnet:
%% Cell type:code id: tags:
```
def kreisflaeche(radius):
a = math.pi * radius**2
return a
```
%% Cell type:markdown id: tags:
Wir haben die `return`-Anweisung vorher schon einmal gesehen, aber in ertragreichen Funktionen folgt hinter der `return`-Anweisung ein Ausdruck (im Beispiel oben `a`). Die Anweisung bedeutet: "Beende sofort diese Funktion und verwende den folgenden Ausdruck als Rückgabewert." Der Ausdruck kann beliebig kompliziert sein, wir könnten diese Funktion also auch kürzer schreiben:
%% Cell type:code id: tags:
```
def kreisflaeche(radius):
return math.pi * radius**2
```
%% Cell type:markdown id: tags:
Auf der anderen Seite können uns **temporäre Variablen** wie `a` beim Debugging helfen.
Manchmal ist es nützlich, mehrere `return`-Anweisungen zu haben - eine in jedem Zweig einer Verzweigung:
%% Cell type:code id: tags:
```
def betrag(x):
if x < 0:
return -x
else:
return x
```
%% Cell type:markdown id: tags:
Da solche `return`-Anweisungen in alternativen (sich gegenseitig ausschließenden) Zweigen liegen, wird nur eine davon ausgeführt.
Sobald eine `return`-Anweisung ausgeführt wird, wird die Funktion beendet, ohne die folgenden Anweisungen auszuführen. Code, der nach einer `return`-Anweisung folgt oder an einer anderen Stelle, die während der Ausführung niemals erreicht werden kann, wird **toter Code** (*dead code*) genannt.
![dead code](https://amor.cms.hu-berlin.de/~jaeschkr/teaching/spp/deadcode.svg)
In einer eintragreichen Funktion sollten wir sicherstellen, dass jeder mögliche Pfad durch den Code eine `return`-Anweisung erreicht. Zum Beispiel:
%% Cell type:code id: tags:
```
def betrag(x):
if x < 0:
return -x
if x > 0:
return x
```
%% Cell type:markdown id: tags:
Diese Funktion ist falsch, beziehungsweise unvollständig, denn wenn `x` gleich 0 ist, ist keine der beiden Bedingungen erfüllt und die Funktion wird beendet, ohne dass eine `return`-Anweisung erreicht wird. Wenn die Ausführung das Ende einer Funktion statt einer `return`-Anweisung erreicht, ist der Rückgabewert `None`, was nicht der Betrag von 0 ist:
%% Cell type:code id: tags:
```
print(betrag(0))
```
%% Cell type:markdown id: tags:
Übrigens, Python bietet eine eingebaute Funktion `abs` die den Betrag einer Zahl berechnet:
%% Cell type:code id: tags:
```
print(abs(-42))
print(abs(0))
```
%% Cell type:markdown id: tags:
Schreiben Sie eine Funktion `compare`, die zwei Parameter `x` und `y` erwartet und `1` für `x > y`, `0` für `x == y` und `-1` für `x < y` zurückliefert:
%% Cell type:code id: tags:
```
# Implementieren Sie hier die Funktion compare
```
%% Cell type:markdown id: tags:
### 6.2 Schrittweise Entwicklung
Wenn Sie größere Funktionen schreiben, kann es sein, dass Sie mehr Zeit mit der Fehlersuche (Debugging) verbringen.
Um mit zunehmend komplexeren Programmen klarzukommen, können Sie eine Methode verwenden, die sich **schrittweise Entwicklung** (*incremental development*) nennt. Das Ziel bei der schrittweisen Entwicklung ist die Vermeidung langer Fehlersuch-Sitzungen, indem immer nur kleine Codestücke hinzugefügt und getestet werden.
Nehmen wir z.B. an, dass wir die Entfernung zwischen zwei Punkten berechnen wollen, die durch die Koordinaten $(x_1, y_1)$ und $(x_2, x_2)$ gegeben sind. Nach dem [Satz des Pythagoras](https://de.wikipedia.org/wiki/Satz_des_Pythagoras) ist die Entfernung:
$entfernung = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$
Im ersten Schritt sollten wir uns überlegen, wie die Funktion `entfernung` in Python aussehen sollte. In anderen Worten: Was sind die Eingaben (Parameter) und was ist das Ergebnis (Rückgabewert)?
In diesem Fall sind die Eingaben zwei Punkte, die wir durch vier Zahlen repräsentieren können. Das Ergebnis ist die Entfernung, repräsentiert als Gleitkommazahl.
Mit dieser Information können wir sofort eine Skizze der Funktion schreiben:
%% Cell type:code id: tags:
```
def entfernung(x1, y1, x2, y2):
return 0.0
```
%% Cell type:markdown id: tags:
Ganz offensichtlich berechnet diese Variante nicht die Entfernung, sie liefert stets Null zurück. Aber sie ist syntaktisch korrekt und sie läuft, das heißt, wir können die Funktion testen, bevor wir sie verkomplizieren.
Rufen Sie die Funktion mit Beispielargumenten auf, um sie zu testen:
%% Cell type:code id: tags:
```
entfernung(1, 2, 4, 6)
```
%% Cell type:markdown id: tags:
Diese Werte sind so gewählt, dass die horizontale Distanz drei ist und die vertikale Distanz 4 - dadurch ist das Ergebnis 5 - die Hypothenuse eines Dreiecks mit den Seitenlängen 3-4-5. Wenn wir die Funktion testen ist es hilfreich, das richtige Ergebnis zu kennen.
![Satz des Pythagoras](https://upload.wikimedia.org/wikipedia/commons/d/d1/01-Rechtwinkliges_Dreieck-Pythagoras.svg)
([Petrus3743](https://commons.wikimedia.org/wiki/File:01-Rechtwinkliges_Dreieck-Pythagoras.svg), Wikimedia Commons)
An dieser Stelle haben wir uns davon überzeugt, dass die Funktion syntaktisch korrekt ist. Wir können also damit beginnen, Code zum Rumpf hinzuzufügen. Ein naheliegender nächster Schritt ist, die Differenzen $x_2-x_1$ und $y_2-y_1$ zu berechnen. Die nächste Version speichert die Werte in temporären Variablen und gibt sie aus:
%% Cell type:code id: tags:
```
def entfernung(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
print('dx ist', dx)
print('dy ist', dy)
return 0.0
entfernung(1, 2, 4, 6)
```
%% Cell type:markdown id: tags:
Wenn die Funktion richtig funktioniert, sollte `dx ist 3` und `dx ist 4` ausgegeben werden. Wenn dem so ist, wissen wir, dass die Funktion die Argumente richtig erhalten hat und die ersten Berechnungen korrekt durchgeführt wurden. Falls nicht, gibt es nur wenige Zeilen Code, die wir überprüfen müssen.
Als nächstes berechnen wir die Summe der Quadrate von `dx` und `dy`:
%% Cell type:code id: tags:
```
def entfernung(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
dquadrat = dx**2 + dy**2
print('dquadrat ist: ', dquadrat)
return 0.0
entfernung(1, 2, 4, 6)
```
%% Cell type:markdown id: tags:
Wieder rufen wir die Funktion mit bekannten Werten auf und prüfen das Ergebnis (das 25 sein sollte). Abschließend können wir die Funktion `math.sqrt` nutzen um das Ergebnis zu berechnen und zurückzugeben:
%% Cell type:code id: tags:
```
import math
def entfernung(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
dquadrat = dx**2 + dy**2
ergebnis = math.sqrt(dquadrat)
return ergebnis
entfernung(1, 2, 4, 6)
```
%% Cell type:markdown id: tags:
Falls das richtig funktioniert, sind wir fertig. Ansonsten könnten wir beispielsweise den Wert von `ergebnis` vor der `return`-Anweisung mit `print` ausgeben.
Die endgültige Version der Funktion zeigt nichts an (gibt nichts auf dem Bildschirm aus), wenn sie ausgeführt wird; sie gibt nur einen Wert zurück. Die `print`-Anweisungen die wir zwischendurch geschrieben haben sind hilfreich für die Fehlersuche, aber sobald die Funktion funktioniert, sollten wir sie entfernen. Solcher Code wird **Hilfscode** (*scaffolding*) genannt, denn er hilft beim Schreiben des Programms aber ist nicht Teil des endgültigen Produkts.
Wenn Sie mit Programmieren beginnen, sollten sie jeweils nur ein bis zwei Zeilen auf einmal hinzufügen. Sobald Sie mehr Erfahrung gesammelt haben, werden Sie merken, dass Sie größere Stücke Code auf einmal schreiben und testen können. In jedem Fall kann Ihnen schrittweise Entwicklung viel Zeit bei der Fehlersuche ersparen.
Die wichtigsten Punkte dieses Vorgehens sind:
1. Beginnen Sie mit einem kleinen funktionierenden Programm und führen Sie nur inkrementelle (schrittweise) Änderungen durch. Falls ein Fehler auftritt, sollten Sie zu jeden Zeitpunkt eine Ahnung haben, in welcher Zeile der Fehler sich befindet.
2. Nutzen Sie Variablen, um Zwischenwerte zu speichern, so dass Sie diese mit `print` ausgeben und überprüfen können.
3. Sobald das Programm funktioniert, sollten Sie Teile des Hilfscodes entfernen und gegebenenfalls mehrere Anweisungen zu einer Verbundanweisung zusammenfügen, aber nur, wenn sich dadurch die Lesbarkeit des Programms nicht verschlechtert.
**Übung:** Nutzen Sie das Prinzip der schrittweisen Entwicklung, um eine Funktion `hypothenuse` zu schreiben, die die Länge der Hypothenuse eines rechtwinkligen Dreiecks zurückgibt, wenn die Längen der beiden Katheden gegeben sind. Dokumentieren Sie jeden Entwicklungsschritt hier im Notebook (d.h., erzeugen Sie eine Kopie der Funktion, bevor Sie den nächsten Entwicklungsschritt durchführen).
%% Cell type:code id: tags:
```
# beginnen Sie hier mit der Entwicklung der Funktion
```
%% Cell type:markdown id: tags:
![Right Triangle](http://www.mezzacotta.net/owls/comics/0336.png)
([Nina Owens](http://geometry157.blogspot.de/2013/06/types-of-triangles-there-are-several.html))
%% Cell type:markdown id: tags:
### 6.3 Komposition
Wie Sie mittlerweile wissen sollten, können wir eine Funktion innerhalb einer anderen aufrufen. Als Beispiel werden wir eine Funktion schreiben, die zwei Punkte erwartet - den Mittelpunkt eines Kreises und einen Punkt auf dem Kreisumfang - und uns daraus die Fläche des Kreises berechnet.
Angenommen, die Koordinaten des Mittelpunktes sind in den Variablen `xc` und `yc` gespeichert und die des Punktes auf dem Kreisumfang in `xp` und `yp`. Der erste Schritt ist, den Radius des Kreises zu berechnen, der sich aus der Entfernung der beiden Punkte ergibt. Wir haben gerade eine Funktion `entfernung` geschrieben, die das erledigt:
%% Cell type:code id: tags:
```
radius = entfernung(xc, yc, xp, yp)
```
%% Cell type:markdown id: tags:
Der nächste Schritt ist, die Fläche eines Kreises mit diesem Radius zu berechnen. Das haben wir auch schon implementiert:
%% Cell type:code id: tags:
```
ergebnis = kreisflaeche(radius)
```
%% Cell type:markdown id: tags:
Wenn wir diese Schritte in einer Funktion verkapseln, erhalten wir:
%% Cell type:code id: tags:
```
def kreisflaeche_2(xc, yc, xp, yp):
radius = entfernung(xc, yc, xp, yp)
ergebnis = kreisflaeche(radius)
return ergebnis
```
%% Cell type:markdown id: tags:
Die Hilfsvariablen `radius` und `ergebnis` sind hilfreich für Entwicklung und Debugging, aber sobald das Programm funktioniert können wir es kompakter aufschreiben durch die **Komposition** von Funktionsaufrufen:
%% Cell type:code id: tags:
```
def kreisflaeche_2(xc, yc, xp, yp):
return kreisflaeche(entfernung(xc, yc, xp, yp))
```
%% Cell type:markdown id: tags:
![Kreisfläche](https://upload.wikimedia.org/wikipedia/commons/d/d2/Circle_Area_de.svg)
%% Cell type:markdown id: tags:
### 6.4 Boolesche Funktionen
Funktionen können Boolesche Werte zurückliefern. Das ist praktkisch, um komplizierte Tests in einer Funktion zu verstecken. Zum Beispiel:
%% Cell type:code id: tags:
```
def ist_teilbar(x, y):
if x % y == 0:
return True
else:
return False
```
%% Cell type:markdown id: tags:
Es ist üblich, Booleschen Funktionen Namen zu geben, die wie Ja-/Nein-Fragen klingen; `ist_teilbar` gibt entweder `True` oder `False` zurück und zeigt damit an, ob `x` durch `y` teilbar ist.
Hier ist ein Beispiel:
%% Cell type:code id: tags:
```
ist_teilbar(6, 4)
```
%% Cell type:code id: tags:
```
ist_teilbar(6, 3)
```
%% Cell type:markdown id: tags:
Das Ergebnis des `==`-Operators ist ein Boolescher Wert, daher können wir die Funktion kompakter aufschreiben, indem wir den Wert direkt zurückgeben:
%% Cell type:code id: tags:
```
def ist_teilbar(x, y):
return x % y == 0
```
%% Cell type:markdown id: tags:
Boolesche Funktionen werden oft in Verzweigungen genutzt:
%% Cell type:code id: tags:
```
if ist_teilbar(x, 2):
print('x ist eine gerade Zahl')
```
%% Cell type:markdown id: tags:
Es mag verlockend erscheinen, stattdessen folgendes zu schreiben:
%% Cell type:code id: tags:
```
if ist_teilbar(x, 2) == True:
print('x ist eine gerade Zahl')
```
%% Cell type:markdown id: tags:
Aber dieser zusätzliche Vergleich ist unnötig.
Schreiben Sie als Übung eine Funktion `ist_zwischen(x, y, z)`, die `True` zurückgibt, wenn $x \le y \le z$ gilt und ansonsten `False`.
%% Cell type:code id: tags:
```
# implementieren Sie hier die Funktion
```
%% Cell type:markdown id: tags:
### 6.5 Noch mehr Rekursion
Wir haben bisher nur eine kleine Teilmenge von Python kennengelernt aber vielleicht interessiert es Sie zu wissen, dass diese Teilmenge eine *komplette* Programmiersprache darstellt. Das heißt, alles was berechnet werden kann, können wir mit den bisher erlernten Anweisungen und Funktionen ausdrücken! Jedes jemals geschriebene Programm könnten wir umschreiben, so dass es nur mit den Sprachmerkmalen auskommt, die wir bis jetzt gelernt haben (gut, wir bräuchten noch ein paar Anweisungen, um Geräte wie z.B. die Maus, Festplatten, etc. zu kontrollieren).
![Alan Turing](https://upload.wikimedia.org/wikipedia/commons/thumb/7/79/Alan_Turing_az_1930-as_%C3%A9vekben.jpg/372px-Alan_Turing_az_1930-as_%C3%A9vekben.jpg)
Diese Behauptung zu beweisen, ist eine nicht so ganz einfache Aufgabe, die zuerst von [Alan Turing](https://de.wikipedia.org/wiki/Alan_Turing) gelöst wurde. Er war einer der ersten Informatiker (einige würden argumentieren, dass er ein Mathematiker war, aber viele der ersten Informatiker begannen als Mathematiker). Dementsprechend wird dies oft als [Turing-These](https://de.wikipedia.org/wiki/Church-Turing-These) bezeichnet.
Um einen Idee davon zu bekommen, was wir mit den Werkzeugen, die wir bisher kennengelernt haben, schon erreichen können, wollen wir einige rekursiv definierte mathematische Funktionen implementieren. Eine rekursive Definition ist ähnlich einer [zirkulären Definition](https://en.wikipedia.org/wiki/Circular_definition) (*circular definition* - leider konnte ich dafür keinen deutschen Begriff finden) in dem Sinne, dass die Definition eine Referenz auf das, was definiert wird, enthält. Eine richtig zirkuläre Definition ist nicht sehr nützlich:
**vorpal:** Ein Adjektiv welches genutzt wird, um etwas zu beschreiben, was vorpal ist.
Wenn Sie so eine Definition in einem Wörterbuch sehen, sind Sie vermutlich verärgert. Andererseits, wenn wir uns die Definition der Fakultätsfunktion heraussuchen (die mit dem Symbol ! bezeichnet wird), finden wir vermutlich etwas in der Art:
\begin{align}
0! &= 1\\
n! &= n(n-1)!
\end{align}
Diese Definition sagt aus, dass die Fakultät von 0 gleich 1 ist und die Fakultät jedes anderen Wertes $n$ entspricht $n$ multipliziert mit der Fakultät von $n-1$.
Also ist 3! gleich 3 mal 2!, was 2 mal 1! ist, was 1 mal 0! ist. Zusammengenommen ist 3! also gleich 3 mal 2 mal 1 mal 1 - also 6.
Wenn wir etwas rekursiv definieren können, dann können wir auch eine Python-Funktion schreiben, um das ganze auszuwerten. Der erste Schritt ist, zu entscheiden, was die Parameter sein sollen. In diesem Beispiel sollte es klar sein, dass `fakultaet` eine ganze Zahl erwartet:
```python
def fakultaet(n):
```
%% Cell type:markdown id: tags:
Wenn das Argument 0 übergeben wird, müssen wir einfach nur 1 zurückgeben:
%% Cell type:code id: tags:
```
def fakultaet(n):
if n == 0:
return 1
```
%% Cell type:markdown id: tags:
Ansonsten, und das ist der spannende Teil, müssen wir einen rekursiven Aufruf machen, um die Fakultät von $n-1$ zu berechnen und dann mit $n$ zu multiplizieren:
%% Cell type:code id: tags:
```
def fakultaet(n):
if n == 0:
return 1
else:
rekursion = fakultaet(n-1)
ergebnis = n * rekursion
return ergebnis
fakultaet(3)
```
%% Cell type:markdown id: tags:
Der Kontrollfluss dieses Programms ist ähnlich dem von `countdown` in [Abschnitt 5.8](seminar05.ipynb#5.8-Rekursion). Wenn wir `fakultaet` mit dem Wert 3 aufrufen, passiert folgendes:
Da 3 ungleich 0 ist, führen wir den zweiten Zweig aus und berechnen die Fakultät von $n-1$ ...
- Da 2 ungleich 0 ist, führen wir den zweiten Zweig aus und berechnen die Fakultät von $n-1$ ...
- Da 1 ungleich 0 ist, führen wir den zweiten Zweig aus und berechnen die Fakultät von $n-1$ ...
- Da 0 gleich 0 ist, führen wir den ersten Zweig aus und geben 1 zurück, ohne weitere rekursive Aufrufe zu tätigen.
Der Rückgabewert, 1, wird mit n multipliziert, was 1 ist, und das Ergebnis zurückgegeben.
Der Rückgabewert, 1, wird mit n multipliziert, was 2 ist, und das Ergebnis zurückgegeben.
Der Rückgabewert, 2, wird mit n multipliziert, was 3 ist, und das Ergebnis 6 wird zum Rückgabewert des Funktionsaufrufs, der den ganzen Vorgang gestartet hat.
Die folgende Abbildung zeigt wie das Stapeldiagramm für diese Folge von Funktionsaufrufen aussieht:
![Stapeldiagramm](http://amor.cms.hu-berlin.de/~jaeschkr/teaching/spp/stapeldiagramm_fakultaet.svg)
Das Diagramm zeigt, wie die Rückgabewerte im Stapel weiter nach oben durchgereicht werden. In jedem Block ist der Rückgabewert der Wert von `ergebnis`, was das Produkt von `n` und `rekursion` ist.
Im untersten (letzten) Block existieren die lokalen Variablen `rekursion` und `ergebnis` nicht, denn derjenige Zweig, welcher diese erzeugt, wird nicht ausgeführt.
Nutzen Sie auch http://pythontutor.com/, um die einzelnen Schritte nachzuvollziehen!
Nutzen Sie auch [http://pythontutor.com/](http://pythontutor.com/visualize.html#code=def%20fakultaet%28n%29%3A%0A%20%20%20%20if%20n%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20rekursion%20%3D%20fakultaet%28n-1%29%0A%20%20%20%20%20%20%20%20ergebnis%20%3D%20n%20*%20rekursion%0A%20%20%20%20%20%20%20%20return%20ergebnis%0A%0Afakultaet%283%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false), um die einzelnen Schritte nachzuvollziehen!
%% Cell type:markdown id: tags:
### 6.6 Vertrauensvorschuss
Dem Kontrollfluss zu folgen ist eine Möglichkeit, Programme zu lesen, aber das kann ganz schön aufwendig sein. Eine Alternative ist, dem Code einen "Vertrauensvorschuss" zu geben. Wenn wir einen Funktionsaufruf sehen, können wir, statt dem Kontrollfluss zu folgen, einfach *annehmen*, dass die Funktion richtig arbeitet und das korrekte Ergebnis zurückliefert.
Tatsächlich praktizieren wir das bisher schon mit den eingebauten Funktionen. Wenn wir `math.cos` oder `print` aufrufen, schauen wir uns den Rumpf dieser Funktionen nicht an. Wir gehen einfach davon aus, dass sie funktionieren, weil die Leute, die sie geschrieben haben, gute Programmierer/innen sind. (Zumindest nehmen wir das vielleicht an ;-) )
Das gleiche gilt, wenn wir eine unserer eigenen Funktionen aufrufen. Beispielsweise haben wir in [Abschnitt 6.4](#6.4-Boolesche-Funktionen) eine Funktion `ist_teilbar` geschrieben, die bestimmt, ob eine Zahl durch eine andere teilbar ist. Sobald wir uns davon überzeugt haben, dass diese Funktion korrekt arbeitet - durch Verstehen des Codes und Testen - können wir die Funktion nutzen, ohne uns den Rumpf noch einmal anzuschauen.
Das gleiche gilt für rekursive Programme. Wenn wir auf einen rekursiven Funktionsaufruf treffen, können wir, anstatt dem Kontrollfluss zu folgen, annehmen, dass der rekursive Aufruf funktioniert (also den richtigen Wert zurückliefert) und uns selbst beispielsweise fragen "Angenommen, ich kann die Fakultät von $n-1$ berechnen, kann ich dann die Fakultät von $n$ berechnen?" Das funktioniert offensichtlich - indem wir mit $n$ multiplizieren.
Natürlich ist es etwas seltsam, anzunehmen, dass die Funktion richtig arbeitet, wenn wir sie noch nicht fertig implementiert haben, aber daher wird das ganze ja auch Vertrauensvorschuss genannt.
%% Cell type:markdown id: tags:
### 6.7 Ein weiteres Beispiel
Neben der Fakultät ist ein weiteres übliches Beispiel für eine rekursiv definierte mathematische Funktion die [Fibonacci-Folge](https://de.wikipedia.org/wiki/Fibonacci-Folge):
\begin{align}
fibonacci(0) &= 0\\
fibonacci(1) &= 1\\
fibonacci(n) &= fibonacci(n-1) + fibonacci(n-2)\\
\end{align}
Übersetzt nach Python schaut das so aus:
%% Cell type:code id: tags:
```
def fibonacci(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fibonacci(n-1) + fibonacci(n-2)
fibonacci(7)
```
%% Cell type:markdown id: tags:
Wenn Sie hier versuchen, dem Kontrollfluss zu folgen, wird - selbst für kleine Werte von $n$ - ihr Kopf explodieren. Aber mit Vertrauensvorschuss - wenn wir annehmen dass die zwei rekursiven Aufrufe korrekt funktionieren - wird klar, dass wir das richtige Ergebnis durch Addition der Werte erhalten.
**Übung:** Probierem Sie aus, bis zu welchem Wert von $n$ Sie die Funktion noch aufrufen können, ohne zu lange warten zu müssen. Wenn Sie wollen, können Sie auch `print`-Ausgaben zur Funktion hinzufügen, um den Ablauf nachzuverfolgen (`print(' '*n, n)` erzeugt beispielsweise eine übersichtliche Ausgabe). Rufen Sie die Funktion dann aber besser mit sehr kleinen Werten für `n` auf.
![Fibonacci-Folge](https://upload.wikimedia.org/wikipedia/commons/9/95/FibonacciBlocks.svg)
([Borb](https://commons.wikimedia.org/wiki/File:FibonacciBlocks.svg))
%% Cell type:markdown id: tags:
### 6.8 Typen prüfen
Was passiert, wenn wir `fakultaet` mit dem Wert `1.5` als Argument aufrufen?
%% Cell type:code id: tags:
```
fakultaet(1.5)
```
%% Cell type:markdown id: tags:
Das sieht nach einer unendlichen Rekursion aus. Aber wie kann das sein? Die Funktion hat doch einen Basisfall - wenn `n == 0` ist.
![What the heck?](http://s2.quickmeme.com/img/02/0213043c2680cd6a7dad73df5359c75ce089262b3b86476a62e6564f54c40c08.jpg)
Nun, wenn `n` keine ganze Zahl ist, können wir den Basisfall *verpassen* und eine unendliche Rekursion wird durchgeführt.
Im ersten rekursiven Aufruf ist der Wert von `n` gleich 0.5. Im nächsten ist der Wert `-0.5`. Ab da wird der Wert immer kleiner (immer negativer) aber er wird nie gleich `0` sein.
Wir haben zwei Optionen, dieses Problem zu beheben:
1. Wir können versuchen, die Funktion `fakultaet` zu verallgemeinern, so dass sie auch mit Gleitkommazahlen arbeitet.
2. Wir können `fakultaet` anpassen, so dass der Typ des übergebenen Arguments geprüft wird.
Die erste Option nennt sich [Gamma-Funktion](https://de.wikipedia.org/wiki/Gammafunktion) und sprengt den Rahmen dieses Kurses. Also schauen wir uns die zweite Option an.
Mit Hilfe der eingebauten Funktion `isinstance` können wir den Typ des Arguments prüfen. Und wenn wir schon einmal dabei sind, können wir auch gleich sicherstellen, dass das Argument positiv ist:
%% Cell type:code id: tags:
```
def fakultaet(n):
if not isinstance(n, int):
print('Die Fakultät ist nur für ganze Zahlen definiert.')
return None
elif n < 0:
print('Die Fakultät für negative ganze Zahlen ist nicht definiert.')
return None
elif n == 0:
return 1
else:
return n * fakultaet(n-1)
```
%% Cell type:markdown id: tags:
Der erste Basisfall behandelt Zahlen die keine ganzen Zahlen sind; der zweite behandelt negative ganze Zahlen. In beiden Fällen gibt die Funktion eine Fehlermeldung aus und gibt `None` zurück, um anzuzeigen, dass etwas schiefgelaufen ist:
%% Cell type:code id: tags:
```
print(fakultaet("fred"))
```
%% Cell type:code id: tags:
```
print(fakultaet(-2))
```
%% Cell type:markdown id: tags:
Wenn wir beide Überprüfungen "bestehen", dann wissen wir, dass `n` eine positive ganze Zahl oder Null ist. Damit können wir zeigen, dass die Rekursion terminiert.
Dieses Programm demonstriert ein Entwurfsmuster, welches manchmal **Wächter** (*guardian*) genannt wird. Die ersten beiden Verzweigungen agieren als Wächter, die den darauffolgenden Code vor Werten beschützen, die Fehler hervorrufen könnten. Die Wächter ermöglichen uns, die Korrektheit des Codes zu beweisen.
Im [Abschnitt 11.4](seminar11.ipynb#reverse-lookup) werden wir eine flexiblere Alternative kennenlernen, um eine Fehlermeldung auszugeben: Ausnahmebehandlung.
%% Cell type:markdown id: tags:
### 6.9 Debugging
Ein großes Programm in kleinere Funktionen zu zerlegen erzeugt ganz natürliche Kontrollpunkte. Wenn eine Funktion nicht funktioniert, gibt es drei Möglichkeiten, die wir in Betracht ziehen sollten:
1. Es stimmt etwas nicht mit den Argumenten der Funktion; eine Vorbedingung ist verletzt.
2. Es stimmt etwas nicht mit der Funktion; eine Nachbedingung ist verletzt.
3. Es stimmt etwas nicht mit dem Rückgabewert der Funktion oder der Art und Weise, wie dieser verwendet wird.
Um die erste Möglichkeit auszuschließen, können wir `print`-Anweisungen am Anfang der Funktion einfügen und die Werte der Parameter (und vielleicht deren Typ) ausgeben. Oder wir können Code einfügen, der die Vorbedingungen explizit prüft (wie wir es bei `fakultaet` gerade eben gemacht haben).
Wenn die Parameter gut aussehen, dann können wir eine `print`-Anweisung vor jeder `return`-Anweisung einfügen und den Rückgabewert anzeigen. Falls möglich, prüfen wir den Wert von Hand. Wir können auch in Betracht ziehen, die Funktion mit Werten aufzurufen, die uns das überprüfen des Ergebnisses erleichtern (wie in [Abschnitt 6.2](#6.2-Schrittweise-Entwicklung)).
Wenn die Funktion richtig arbeitet (oder es zumindest danach aussieht), sollten wir uns die Stelle anschauen, an der die Funktion aufgerufen wird und sicherstellen, dass der Rückgabewert richtig bzw. überhaupt verwendet wird.
Das Hinzufügen von `print`-Anweisungen am Anfang und Ende einer Funktion kann uns helfen, den Kontrollfluss besser sichtbar zu machen. Hier ist beispielsweise eine Version von `fakultaet` mit `print`-Anweisungen:
%% Cell type:code id: tags:
```
def fakultaet(n):
space = ' ' * (4 * n)
print(space, 'fakultaet', n)
if n == 0:
print(space, 'returning 1')
return 1
else:
rekursion = fakultaet(n-1)
ergebnis = n * rekursion
print(space, 'returning', ergebnis)
return ergebnis
```
%% Cell type:markdown id: tags:
Dabei ist `space` eine Zeichenkette voller Leerzeichen, die die Einrückung der Ausgabe kontrolliert. Probieren Sie es aus:
%% Cell type:code id: tags:
```
fakultaet(4)
```
%% Cell type:markdown id: tags:
Wenn Sie der Kontrollfluss verwirrt, dann kann diese Art der Ausgabe hilfreich sein. Es braucht etwas Zeit, guten Hilfscode zu entwickeln, aber etwas Hilfscode kann uns viel Zeit beim Debuggen ersparen.
![When you Google an error message and it gets no results, you can be pretty sure you've found a clue to the location of Martin's sword.](https://imgs.xkcd.com/comics/debugging_2x.png)
([Debugging](https://xkcd.com/1722/), Randall Munroe)
%% Cell type:markdown id: tags:
### 6.10 Glossar
Legen wir uns eine Liste mit den wichtigsten Begriffen an, die wir im Kapitel 6 gelernt haben:
- temporäre Variable:
- toter Code:
- schrittweise Entwicklung
- Hilfscode:
- Wächter: Auch *guardian* genannt, beschützt der "Wächter" darauf folgenden Code vor Eingaben, die zu Fehlern führen können.
Ergänzen Sie die Liste in eigenen Worten. Das ist eine gute Erinnerungs- und Übungsmöglichkeit.
%% Cell type:markdown id: tags:
### 6.11 Übung
#### Aufgabe 1
Zeichnen Sie (mit Bleistift und Papier) ein Stapeldiagramm für das folgende Programm wenn bei der Ausführung die Zeile `x = x + 1` in der Funktion `a` erreicht wurde. Was gibt das Programm aus?
%% Cell type:code id: tags:
```
def b(z):
prod = a(z, z)
print(z, prod)
return prod
def a(x, y):
x = x + 1
return x * y
def c(x, y, z):
total = x + y + z
square = b(total)**2
return square
x = 1
y = x + 1
print(c(x, y+3, x+y))
```
%% Cell type:markdown id: tags:
#### Aufgabe 2
Die [Ackermannfunktion](https://de.wikipedia.org/wiki/Ackermannfunktion), $A(m, n)$ ist folgendermaßen definiert:
\begin{equation}
A(m,n) =
\begin{cases}
n+1 & \ \ \text{falls}\ m=0\\
A(m-1, 1) & \ \ \text{falls}\ m > 0\ \text{und}\ n = 0\\
A(m-1, A(m, n-1)) & \ \ \text{falls}\ m > 0\ \text{und}\ n > 0
\end{cases}
\end{equation}
Schreiben Sie eine Funktion `ack` die die Ackermannfunktion berechnet. Berechnen Sie mit ihrer Funktion `ack(3,4)`, was 125 ergeben sollte. Was passiert für größere Werte von `m` und `n`?
%% Cell type:code id: tags:
```
# Implementieren Sie hier die Ackermannfunktion
# Testaufruf
ack(3,4)
```
%% Cell type:markdown id: tags:
<a data-flickr-embed="true" href="https://www.flickr.com/photos/jasoneppink/4964471335" title="Spoiler Alert"><img src="https://farm5.staticflickr.com/4110/4964471335_1f86a923f3_n.jpg" width="320" height="213" alt="Spoiler Alert"></a><script async src="//embedr.flickr.com/assets/client-code.js" charset="utf-8"></script>
(Quelle: Jason Eppink, Flickr)
1. Wie muss die Grundstruktur der Funktion aussehen, was ist der Kopf, was sind die Parameter?
2. Wie ist die Funktion aufgebaut, wie viele Zweige hat sie? Schreiben Sie die leeren Zweige mit `return`-Anweisungen auf.
3. Was sind die Bedingungen für die einzelnen Zweige, übergeben Sie diese den verschiedenen `if`-Anweisungen.
4. Was wird im Basisfall $m=0$ zurückgegeben? Schreiben Sie die `return`-Anweisung für diesen Zweig.
5. Was passiert wenn n gleich null ist? Mit welchen Werten wird ack aufgerufen? Geben Sie den neuen Aufruf hinter der `return`-Anweisung ein.
6. Was passiert im letzten Zweig? Hier wird es ein wenig kompliziert, aber im Prinzip müssen sie den Ausdruck aus der Formel oben nur hinter der `return`-Anweisung abschreiben
7. Vielleicht wollen Sie jetzt noch Wächter einbauen oder eine `Doc-String` schreiben, die dem Nutzer klar sagt, für welche Zahlen `ack` definiert ist.
%% Cell type:code id: tags:
```
def ack(m,n):
if m<0 or n<0:
print("Funktion nicht definiert")
return None
if not isinstance (n, int) or not isinstance (m, int):
print ("Funktion nicht definiert")
return None
if m==0:
return n+1
if n==0 and m>0:
return ack (m-1,1)
if m>0 and n>0:
return ack (m-1, ack(m, n-1))
ack(3,4)
```
%% Cell type:markdown id: tags:
#### Aufgabe 3
Ein [Palindrom](https://de.wikipedia.org/wiki/Palindrom) ist ein Wort, welches vorwärts und rückwärts gelesen gleich ist. Beispielsweise "neben" oder "hangnah" (wenn wir Großschreibung ignorieren, gibt es auch Substantive, z.B. "Reliefpfeiler" oder "Anna"). Rekursiv definiert, ist ein Wort ein Palindrom, wenn der erste und letzte Buchstabe identisch sind und der Mittelteil ein Palindrom ist.
Die folgenden Funktionen erwarten eine Zeichenkette als Argument und geben die ersten, letzten und mittleren Buchstaben zurück:
%% Cell type:code id: tags:
```
def first(word):
return word[0]
def last(word):
return word[-1]
def middle(word):
return word[1:-1]
```
%% Cell type:markdown id: tags:
Wir werden in [Kapitel 8](seminar08.ipynb) sehen, wie sie funktionieren.
1. Testen Sie diese Funktionen. Was passiert, wenn Sie `middle` mit einer Zeichenkette mit nur zwei Zeichen aufrufen? Oder mit nur einem Zeichen? Was passiert mit der leeren Zeichenkette, geschrieben '', die keine Zeichen enthält?
2. Schreiben Sie eine Funktion `ist_palindrom`, die eine Zeichenkette als Argument erwartet und `True` zurückliefert, wenn die Zeichenkette ein Palindrom ist und ansonsten `False`. (Erinnern Sie sich daran, dass Sie mit der eingebauten Funktion `len` die Länge einer Zeichenkette ermitteln können.)
%% Cell type:code id: tags:
```
# Testen Sie hier die Funktionen first, last und middle
```
%% Cell type:code id: tags:
```
# Implementieren Sie die Funktion ist_palindrom
```
%% Cell type:markdown id: tags:
<a data-flickr-embed="true" href="https://www.flickr.com/photos/jasoneppink/4964471335" title="Spoiler Alert"><img src="https://farm5.staticflickr.com/4110/4964471335_1f86a923f3_n.jpg" width="320" height="213" alt="Spoiler Alert"></a><script async src="//embedr.flickr.com/assets/client-code.js" charset="utf-8"></script>
(Quelle: Jason Eppink, Flickr)
1. Verwenden Sie die Funktionen first, last und middle.
2. Überlegen Sie wann ein Palindrom ein Palindrom ist und wie sie das einfach testen können.
3. Wenn Sie von außen nach innen immer Zeichenpaare vergleichen und diese Stimmen miteinander überein, dann haben Sie ein Palindrom.
4. Wir wollen das ganze rekursiv implementieren, was ist dabei der Basisfall?
5. Eine Zeichenkette mit einem oder keinem Zeichen ist immer ein Palindrom und damit der Basisfall und einer der Zweige in denen die Funktion abbricht und einen Wert- nämlich `True`zurückgibt. Wir können die Länge der Zeichenkette mit der `len` Funktion testen.
6. Wann sonst bricht die Funktion ab, gibt aber `False` zurück?
7. Wenn das erste und letzte Zeichen, der momentanen Zeichenkette nicht übereinstimmen
8. Wir rufen `ist_palindrom` rekursiv mit `middle` auf um die Zeichenkette ohne den ersten und letzten Buchstaben zu erhalten, damit wird die Zeichenkette immer kürzer und bricht entweder ab, weil sie zu kurz ist- dann ist es ein Palindrom- oder weil das erste und letzte Zeichen nicht übereinstimmen - dann ist es kein Palindrom
%% Cell type:code id: tags:
```
def ist_palindrom(s):
if len(s)<=1:
return True
elif first(s)!=last(s):
return False
return ist_palindrom(middle(s))
ist_palindrom("gohangasalamiimalasagnahog")
```
%% Cell type:markdown id: tags:
![Palindrom](https://imagesvc.timeincapp.com/v3/mm/image?url=https%3A%2F%2Ftimedotcom.files.wordpress.com%2F2015%2F04%2Fgo-hang-a-salami.jpg&w=800&q=85)
([Farrar, Straus and Giroux](http://time.com/3771063/mark-saltveit-world-palindrome-championship/))
#### Aufgabe 4
Eine Zahl $a$ ist eine Potenz von $b$, wenn $a$ durch $b$ teilbar ist und $a/b$ eine Potenz von $b$ ist. (Beispielsweise ist 27 eine Potenz von 3, denn 27 ist durch 3 teilbar und 9 ist eine Potenz von 3.) Schreiben Sie eine Funktion `ist_potenz` die Parameter `a` und `b` erwartet und `True` zurückgibt, wenn `a` eine Potenz von `b` ist (ansonsten `False`). Hinweis: Überlegen Sie sich, was der Basisfall ist und wie Sie diesen behandeln.
%% Cell type:code id: tags:
```
# Implementieren Sie hier die Funktion ist_potenz
```
%% Cell type:markdown id: tags:
<a data-flickr-embed="true" href="https://www.flickr.com/photos/jasoneppink/4964471335" title="Spoiler Alert"><img src="https://farm5.staticflickr.com/4110/4964471335_1f86a923f3_n.jpg" width="320" height="213" alt="Spoiler Alert"></a><script async src="//embedr.flickr.com/assets/client-code.js" charset="utf-8"></script>
(Quelle: Jason Eppink, Flickr)
1. Schreiben Sie erst den Kopf der Funktion inklusive Parametern
2. Bevor Sie den Rest der Funktion implementieren, müssen Sie sich den Basisfall überlegen.
3. Der Basisfall ist b=1 -- und darausfolgend auch a=b
4. Was ist die Ausgabe, wenn der Basisfall erreicht wird?
5. Was müssen Sie überprüfen um herauszufinden ob $a$ eine Potenz von $b$ ist?
6. Wenn $a$ nicht restlos durch $b$ teilbar ist, kann $a$ keine Potenz von $b$ sein. Implementieren Sie diese Aussage
7. Wie und wo muss die Funktion sich selber aufrufen?
8. Die Funktion muss sich !mit `return`-Anweisung! selber aufrufen wenn a%b==0 gilt
9. Dabei wird $a/b$ für $a$ übergeben.
%% Cell type:code id: tags:
```
def ist_potenz(a,b):
if b==1 or a==b:
return True
if a%b==0:
return ist_potenz (a/b, b)
else:
return False
ist_potenz(12,3)
```
%% Cell type:markdown id: tags:
#### Aufgabe 5
Der [größte gemeinsame Teiler](https://de.wikipedia.org/wiki/Gr%C3%B6%C3%9Fter_gemeinsamer_Teiler) (ggT) von $a$ und $b$ ist die größte Zahl die beide Zahlen ($a$ und $b$) ohne Rest teilt.
Eine Möglichkeit den ggT zweier Zahlen zu berechnen, beruht auf der Beobachtung, dass, wenn $r$ der Rest der Division von $a$ durch $b$ ist, dann $ggT(a,b) = ggT(b,r)$ gilt. Als Basisfall können wir $ggT(a,0)=a$ nutzen.
Schreiben Sie eine Funktion `ggt`, die zwei Parameter `a` und `b` erwartet und den größten gemeinsamen Teiler zurückgibt.
%% Cell type:code id: tags:
```
# Implementieren Sie hier die Funktion ggt
```
%% Cell type:markdown id: tags:
<a data-flickr-embed="true" href="https://www.flickr.com/photos/jasoneppink/4964471335" title="Spoiler Alert"><img src="https://farm5.staticflickr.com/4110/4964471335_1f86a923f3_n.jpg" width="320" height="213" alt="Spoiler Alert"></a><script async src="//embedr.flickr.com/assets/client-code.js" charset="utf-8"></script>
(Quelle: Jason Eppink, Flickr)
1. Wie muss der Kopf der Funktion aussehen, welche Parameter werden übergeben?
2. Was müssen wir für den Basisfall überprüfen? Schreiben Sie die passende `if`-Bedingung
3. Was wird im Basisfall zurück gegeben? Schreiben Sie die `return`-Anweisung
4. Wie können Sie den Rest der Division von $a/b$ berechnen?
5. Rufen Sie die Funktion rekursiv auf, übergeben Sie dabei die passenden Varibablen und vergessen Sie die `return`-Anweisung dabei nicht.
%% Cell type:code id: tags:
```
def ggt (a,b):
if b==0:
return a
r=a%b
return ggt(b,r)
ggt (175,25)
```
%% Cell type:markdown id: tags:
![Speichern](https://amor.cms.hu-berlin.de/~jaeschkr/teaching/spp/floppy.png) Speichern Sie dieses Notebook, so dass Ihre Änderungen nicht verlorengehen (nicht auf einem Pool-Rechner). Klicken Sie dazu oben links auf das Disketten-Icon und nutzen Sie beispielsweise einen USB-Stick, E-Mail, Google Drive, Dropbox oder Ihre [HU-Box](https://box.hu-berlin.de/).
%% Cell type:markdown id: tags:
![Smiley](https://upload.wikimedia.org/wikipedia/commons/7/70/Face-devil-grin.svg)
Herzlichen Glückwunsch! Sie haben das 6. Kapitel geschafft. Weiter geht es in [7: Iteration](seminar07.ipynb).
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment