From 3d6aee5ce7ec66372ee9aadaaff680fa3c9b79e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=A4schke?= <jaeschke@l3s.de> Date: Tue, 21 Nov 2017 08:29:34 +0000 Subject: [PATCH] =?UTF-8?q?+Inhalt=20erg=C3=A4nzt=20bis=20Kapitel=204.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- notebooks/seminar03.ipynb | 2 +- notebooks/seminar04.ipynb | 298 +++++++++++++++++++++++++++++++++++++- 2 files changed, 292 insertions(+), 8 deletions(-) diff --git a/notebooks/seminar03.ipynb b/notebooks/seminar03.ipynb index cf72f4d..8a3583b 100644 --- a/notebooks/seminar03.ipynb +++ b/notebooks/seminar03.ipynb @@ -1468,7 +1468,7 @@ "source": [ "\n", "\n", - "Herzlichen Glückwunsch! Sie haben das 3. Kapitel geschafft. Weiter geht es in [5: Bedingungen und Rekursion](seminar05.ipynb)." + "Herzlichen Glückwunsch! Sie haben das 3. Kapitel geschafft. Weiter geht es in [3 Extra: Reguläre Ausdrücke](seminar03extra.ipynb)." ] } ], diff --git a/notebooks/seminar04.ipynb b/notebooks/seminar04.ipynb index 89534f9..5d6695b 100644 --- a/notebooks/seminar04.ipynb +++ b/notebooks/seminar04.ipynb @@ -69,7 +69,7 @@ "source": [ "Es sollte sich ein Fenster öffnen, in dem ein kleiner Pfeil zu sehen ist - dieser repräsentiert die Schildkröte (\"turtle\"). Schließen Sie das Fenster.\n", "\n", - "*(Hinweis: In Jupyter gibt es manchmal Probleme mit dem `turtle`-Modul. Dann hilft es, das Turtle-Fenster zu schließen und den Code nochmal auszuführen. Wenn es gar nicht klappt, dann nutzen Sie für die Turtle-Programmierung bitte nicht Jupyter-Notebooks, sondern Python-Dateien und führen diese direkt mit Python aus.)*\n", + "*(Hinweis: In Jupyter gibt es manchmal Probleme mit dem `turtle`-Modul. Dann hilft es, das Turtle-Fenster zu schließen und den Code nochmal auszuführen oder im Kernel-Menü den Punkt \"Restart & Clear Output\" aufzurufen. Wenn es gar nicht klappt, dann nutzen Sie für die Turtle-Programmierung bitte nicht Jupyter-Notebooks, sondern Python-Dateien und führen diese direkt mit Python aus.)*\n", "\n", "Probieren Sie nun folgendes (am besten in einem eigenen Jupyter-Notebook, einer eigenen Python-Datei, oder im folgenden Block, den Sie dann schrittweise ergänzen):" ] @@ -110,7 +110,7 @@ "\n", "Das Argument von `fd` ist eine Strecke in Pixeln (den Bildpunkten auf dem Monitor), daher hängt die Entfernung, die `bob` geht, von unserer Monitorauflösung ab.\n", "\n", - "Andere Methode, die wir auf einer Schildkröte aufrufen können sind `bk` für eine Rückwärtsbewegung (**b**ac**k**ward), `lt` für eine Linksdrehung (**l**eft **t**urn) und `rt` für eine Rechtsdrehung (**r**ight **t**urn). Das Argument für `lt` und `rt` ist ein Winkel in Grad.\n", + "Andere Methoden, die wir auf einer Schildkröte aufrufen können sind `bk` für eine Rückwärtsbewegung (**b**ac**k**ward), `lt` für eine Linksdrehung (**l**eft **t**urn) und `rt` für eine Rechtsdrehung (**r**ight **t**urn). Das Argument für `lt` und `rt` ist ein Winkel in Grad.\n", "\n", "Jede Schildkröte hat außerdem einen Stift, der entweder oben oder unten ist. Wenn der Stift unten ist, hinterlässt die Schildkröte eine Spur, wenn sie sich bewegt. Die Methoden `pu` und `pd` stehen für \"Stift hoch\" (**p**en **u**p) und \"Stift herunter\" (**p**en **d**own).\n", "\n", @@ -124,7 +124,12 @@ "\n", "Wenn Sie dieses Programm ausführen, sollte sich `bob` zunächst nach Osten und dann nach Norden bewegen und dabei zwei Strecken zeichnen.\n", "\n", - "Verändern Sie ihr Programm jetzt, so dass `bob` ein Quadrat zeichnet. Fahren Sie erst mit dem Kurs fort, wenn es funktioniert.\n" + "Verändern Sie ihr Programm jetzt, so dass `bob` ein Quadrat zeichnet. Fahren Sie erst mit dem Kurs fort, wenn es funktioniert.\n", + "\n", + "\n", + "<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>\n", + "\n", + "(Quelle: Jason Eppink, Flickr)" ] }, { @@ -190,9 +195,9 @@ "\n", "Die Syntax für die `for`-Schleife ist ähnlich einer Funktionsdefinition: Es gibt einen (Schleifen-)Kopf, der mit einem Doppelpunkt endet und einen eingerückten (Schleifen-)Rumpf. Im Rumpf können beliebig viele Anweisungen stehen.\n", "\n", - "Man nennt dies eine Schleife, denn der Rumpf wird abgearbeitet und dann beginnt der Kontrollfluss wieder von vorn. In unserem Beispiel wird der Rumpf viermal durchlaufen.\n", + "Man nennt dies eine **Schleife**, denn der Rumpf wird abgearbeitet und dann beginnt der Kontrollfluss wieder von vorn. In unserem Beispiel wird der Rumpf viermal durchlaufen.\n", "\n", - "Dieser Code ist etwas anders als der vorherige Code zum Zeichnen eines Quadrats, denn nachdem die letzte Seite des Quadrats gezeichnet wurde wird noch eine weitere Drehung durchgeführt. Diese zusätzliche Drehung kostet etwas Zeit, aber der Code ist etwas einfacher, wenn in jedem Schleifendurchlauf immer die gleiche Folge von Anweisungen ausgeführt wird. Ein Nebeneffekt ist, dsss die Schildkröte am Ende wieder in der Ausgangsposition ist mit Blickrichtung zur Ausgangsrichtung.\n" + "Dieser Code ist etwas anders als der vorherige Code zum Zeichnen eines Quadrats, denn nachdem die letzte Seite des Quadrats gezeichnet wurde wird noch eine weitere Drehung durchgeführt. Diese zusätzliche Drehung kostet etwas Zeit, aber der Code ist etwas einfacher, wenn in jedem Schleifendurchlauf immer die gleiche Folge von Anweisungen ausgeführt wird. Ein Nebeneffekt ist, dass die Schildkröte am Ende wieder in der Ausgangsposition ist mit Blickrichtung zur Ausgangsrichtung.\n" ] }, { @@ -211,7 +216,286 @@ "\n", " *Tipp: Finden Sie den Umfang des Kreises heraus und stellen Sie sicher, dass `length * n` gleich dem Umfang ist.*\n", " \n", - "6. Schreiben Sie eine verallgemeinerte Funktion `circle`, so dass sie ein weiteres Argument `angle` erwartet, welches angibt, welcher Teil eines Kreises gezeichnet werden soll. Der Wert von `angle` ist dabei in Grad, so dass für `angle=360` ein vollständiger Kreis gezeichnet werden sollte." + "6. Schreiben Sie eine verallgemeinerte Funktion `circle`, die Sie `arc` nennen, die ein weiteres Argument `angle` erwartet, welches angibt, welcher Teil eines Kreises gezeichnet werden soll. Die Funktion `arc` soll also einen Kreisbogen zeichnen. Der Wert von `angle` ist dabei in Grad, so dass für `angle=360` ein vollständiger Kreis gezeichnet werden sollte." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Verkapselung\n", + "\n", + "In der ersten Aufgabe sollten Sie den Code zum Zeichnen eines Quadrates in eine Funktion packen und dann in Aufgabe 2 die Funktion aufrufen, indem Sie eine Schildkröte als Argument an die Funktion übergeben. So könnte eine mögliche Lösung aussehen:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def square(t):\n", + " for i in range(4):\n", + " t.fd(100)\n", + " t.lt(90)\n", + "\n", + "square(bob)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Die beiden innersten Anweisungen (`t.fd` und `t.lt`) sind doppelt eingerückt, weil sie innerhalb der `for`-Schleife sind, die innerhalb der Funtionsdefinition steht. Die übernächste Zeile (`square(bob)`) ist wieder bündig mit dem linken Rand ausgerichtet, also sowohl außerhalb der `for`-Schleife als auch außerhalb der Funktionsdefinition.\n", + "\n", + "Innerhalb der Funktion verweist `t` auf die gleiche Schildkröte `bob`, so dass `t.lt(90)` die gleiche Auswirkung hat wie `bob.lt(90)`. Hätten wir dann in der Funktion nicht direkt `bob` aufrufen können oder den Parameter stattdessen `bob` nennen können? Prinzipiell schon, aber die Idee ist, dass `t` irgendeine Schildkröte sein kann, nicht nur `bob`, so dass wir beispielsweise eine zweite Schildkröte erzeugen und sie als Argument an `square` übergeben können:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "alice = turtle.Turtle()\n", + "square(alice)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ein Stück Quellcode in eine Funktion zu \"packen\" nennt man \"Verkapselung\" (Englisch: *encapsulation*). Ein Vorteil der Verkapselung ist, dass für ein Stück Code ein Name vergeben wird, der wie eine Art Dokumentation wirkt, also sagt, was der Code macht. Ein anderer Vorteil ist, dass wir den Code leicht wiederverwenden können, denn es ist deutlich praktischer, eine Funktion zweimal aufzurufen, statt den Quellcode im Funktionsrumpf zu kopieren." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Verallgemeinerung\n", + "\n", + "Der nächste Schritt ist, einen Parameter `length` zu `square` hinzuzufügen. Hier ist eine Lösung:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def square(t, length):\n", + " for i in range(4):\n", + " t.fd(length)\n", + " t.lt(90)\n", + "\n", + "square(bob, 100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Das Hinzufügen des Parameters stellt eine **Verallgemeinerung** dar, denn die Funktion wird dadurch allgemeiner: vorher hatte das gezeichnete Quadrat immer die gleiche Größe, jetzt kann es jede beliebige Größe haben.\n", + "\n", + "Der nächste Schritt ist ebenfalls eine Verallgemeinerung. Anstatt eines Quadrates kann die Funktion `polygon` reguläre Vielecke mit beliebig vielen Seiten zeichnen. So könnte eine Lösung aussehen:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def polygon(t, n, length):\n", + " angle = 360 / n\n", + " for i in range(n):\n", + " t.fd(length)\n", + " t.lt(angle)\n", + "\n", + "polygon(bob, 7, 70)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dieses Beispiel zeichnet ein siebenseitiges Vieleck mit einer Seitenlänge von 70.\n", + "\n", + "Bei einer Funktion mit mehr als nur ein paar Argumenten kann man leicht vergessen, was die Argumente bedeuten oder in welcher Reihenfolge sie angegeben werden müssen. In diesem Fall ist es eine gute Idee, die Namen der Parameter in der Argumentliste explizit anzugeben:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "polygon(bob, n=7, length=70)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Diese werden **Schlüsselwortargumente** genannt, denn Sie enthalten die Parameter als \"Schlüsselwörter\" (diese sollten nicht mit Python-Schlüsselwörtern wie z.B. `while` oder `def` verwechselt werden).\n", + "\n", + "Durch diese Syntax wird das Programm lesbarer. Sie erinnert uns auc daran, wie Argumente und Parameter funktionieren: wenn wir eine Funktion aufrufen, werden unsere Argumente den Parametern zugewiesen." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.6 Schnittstellenentwurf\n", + "\n", + "Der nächste Schritt ist, eine Funktion `circle` zu schreiben, die einen Radius `r` als Parameter erwartet. Hier eine einfache Lösung, die die Funktion `polygon` nutzt, um ein reguläres 50-seitiges Vieleck zu zeichnen:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "def circle(t, r):\n", + " circumference = 2 * math.pi * r\n", + " n = 50\n", + " length = circumference / n\n", + " polygon(t, n, length)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Die erste Zeile berechnet den Umfang eines Kreises mit dem Radius `r` mit Hilfe der Formel 2 π r. Da wir `math.pi` nutzen wollen, müssen wir zunächst `math` importieren. Die Konvention ist, dass `import`-Anweisungen stets am Anfang eines Python-Programms stehen.\n", + "\n", + "Weil `n` die Anzahl der Liniensegmente in unserer Näherung eines Kreises ist, so ist `length` die Länge jedes einzelnen Segmentes. Daher zeichnet `polygon` ein reguläre 50-seitiges Vieleck als Näherun eines Kreises mit Radius `r`.\n", + "\n", + "Eine Beschränkung dieser Lösung ist, dass `n` eine Konstante ist. Daher sind für sehr große Kreise die Liniensegmente zu lang und für kleine Kreise verschwenden wir Zeit mit dem Zeichnen sehr kleiner Segmente. Eine Lösung wäre, die Funktion zu verallgemeinern und `n` als Parameter hinzuzufügen. Dies gäbe dem Nutzer (wer auch immer `circle` aufruft) mehr Kontrolle, aber die Schnittstelle wäre weniger aufgeräumt.\n", + "\n", + "Die **Schnittstelle** (Englisch *Interface*) einer Funktion ist eine Zusammenfassung dessen, wie die Funktion genutzt wird: Was sind ihre Parameter? Was macht die Funktion? Was ist ihr Rückgabewert? etc. Eine Schnittstelle ist \"aufgeräumt\", wenn sie der/m Aufrufenden ermöglicht das zu tun, was er oder sie möchte, ohne sich mit unnötigen Details beschäftigen zu müssen.\n", + "\n", + "In unserem Beispiel gehört `r` zur Schnittstelle, da es den Kreis beschreibt, der gezeichnet werden soll. Dagegen ist `n` weniger passend, denn es gehört zu den Details, wie der Kreis gezeichnet wird. \n", + "\n", + "Anstatt die Schnittstelle zu \"verunreinigen\" ist es besser, einen passenden Wert von `n` in Abhängigkeit vom Umfang des Kreises zu wählen:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def circle(t, r):\n", + " circumference = 2 * math.pi * r\n", + " n = int(circumference / 3) + 3\n", + " length = circumference / n\n", + " polygon(t, n, length)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Jetzt ist die Anzahl der Liniensegmente eine ganze Zahl, nahe bei Umfang/3, so dass die Länge jedes Segments ungefähr 3 beträgt. Das ist klein genug, damit der Kreis gut aussieht, aber auch groß genug, um eine effiziente Zeichnung zu ermöglichen und passt für jede Größe von Kreis. \n", + "\n", + "Wir fügen 3 hinzu, damit das Vieleck mindestens drei Seiten hat.\n", + "\n", + "### 4.7 Refactoring\n", + "\n", + "Als wir `circle` geschrieben haben, konnten wir `polygon` wiederverwenden, denn ein regelmäßiges Vieleck mit vielen Seiten ist eine gute Näherung für einen Kreis. Aber mit der Funktion `arc` klappt das nicht so einfach - wir können kein Vieleck und keinen Kreis nutzen, um einen Kreisbogen zu zeichnen.\n", + "\n", + "Eine Alternative ist, mit einer Kopie von `polygon` zu beginnen und diese zu `arc` zu verändern. Das Ergebnis könnte so aussehen:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def arc(t, r, angle):\n", + " arc_length = 2 * math.pi * r * angle / 360\n", + " n = int(arc_length / 3) + 1\n", + " step_length = arc_length / n\n", + " step_angle = angle / n\n", + " \n", + " for i in range(n):\n", + " t.fd(step_length)\n", + " t.lt(step_angle)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Die zweite Hälfte dieser Funktion sieht aus wie `polygon`, aber wir können `polygon` nicht einfach wiederverwenden ohne die Schnittstelle zu verändern. Wir könnten `polygon` generalisieren, so dass es einen Winkel als ein weiteres Argument erwartet, aber dann wäre `polygon` kein angemessener Name mehr. Nennen wir diese verallgemeinerte Funktion also stattdessen `polyline`: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def polyline(t, n, length, angle):\n", + " for i in range(n):\n", + " t.fd(length)\n", + " t.lt(angle)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Jetzt können wir die Funktionen `polygon` und `arc` umschreiben, so dass sie die Funktion `polyline` nutzen:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def polygon(t, n, length):\n", + " angle = 360.0 / n\n", + " polyline(t, n, length, angle)\n", + "\n", + "def arc(t, r, angle):\n", + " arc_length = 2 * math.pi * r * angle / 360\n", + " n = int(arc_length / 3) + 1\n", + " step_length = arc_length / n\n", + " step_angle = float(angle) / n\n", + " polyline(t, n, step_length, step_angle)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Und schließlich können wir die Funktion `circle` umschreiben, so dass sie die Funktion `arc` nutzt:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def circle(t, r):\n", + " arc(t, r, 360)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dieser Prozess der Umstrukturierung eines Programms und der Verbesserung von Schnittstellen, um Code wiederverwenden zu können, nennt sich **Refactoring**. In unserem Fall haben wir festgestellt, dass `arc` und `polygon` ähnlichen Code enthalten haben, daher haben wir diesen Code umgestaltet und in die Funktion `polyline` \"ausfaktorisiert\" (es gibt leider keine gute Deutsche Übersetzung für dieses Vorgehen).\n", + "\n", + "Hätten wir vorausschauend geplant, hätten wir vielleicht `polyline` zuerst geschrieben und diese Umstrukturierung vermieden. Oft wissen wir aber am Anfang eines Projektes noch nicht genug, um schon alle Schnittstellen entwerfen zu können. Sobald wir anfangen zu programmieren, verstehen wir das Problem besser. Manchmal ist Refactoring ein Anzeichen dafür, dass wir etwas gelernt haben.\n" ] }, { @@ -227,7 +511,7 @@ "source": [ "\n", "\n", - "Herzlichen Glückwunsch! Sie haben das 3. Kapitel geschafft. Weiter geht es in [5: Bedingungen und Rekursion](seminar05.ipynb)." + "Herzlichen Glückwunsch! Sie haben das 4. Kapitel geschafft. Weiter geht es in [5: Bedingungen und Rekursion](seminar05.ipynb)." ] } ], -- GitLab