Newer
Older
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Seminar Problemorientierte Programmierung\n",
"\n",
"## Ihre Lernziele\n",
"\n",
"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:\n",
"\n",
"- \n",
"- \n",
"- \n",
"\n",
"\n",
"## Exkurs: Was mir an Python gefällt\n",
"\n",
"In dieser Rubrik, die immer am Anfang eines Kapitels steht, möchte ich Ihnen zeigen, wofür ich Python nutze und warum ich es mag. Sie werden vielleicht noch nicht verstehen, was ich genau mache, aber Sie sehen damit schon einmal die Möglichkeiten von Python und können später darauf zurückgreifen. Da dies auch ein Exkurs ist, können Sie diese Rubrik gerne auch erst einmal überspringen.\n",
"\n",
"Man kann schnell und einfach ein Programm aufschreiben und testen. Man muss es weder kompilieren, noch viel \"unnötige\" Syntax kennen:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def factorial(n):\n",
" if n < 2:\n",
" return 1\n",
" else:\n",
" return n * factorial(n - 1)\n",
"\n",
"print(factorial(5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4: Fallstudie: Schnittstellenentwurf\n",
"\n",
"In diesem Kapitel lernen wir anhand einer Fallstudie, wie wir Funktionen entwerfen können, die gut zusammenarbeiten.\n",
"\n",
"Wir lernen außerdem das `turtle`-Modul kennen, mit dessen Hilfe wir Graphiken erzeugen können. \n",
"\n",
"### 4.1 Das `turtle`-Modul\n",
"\n",
"Führen Sie den folgenden Code aus, um zu testen, ob das `turtle`-Modul installiert ist:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import turtle\n",
"bob = turtle.Turtle()\n",
"turtle.mainloop()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"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 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):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import turtle\n",
"bob = turtle.Turtle()\n",
"print(bob)\n",
"turtle.mainloop()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Das `turtle`-Modul (mit kleinem `t`) stellt eine Funktion `Turtle` (mit großem `T`) bereit, die ein Turtle-Objekt erzeugt - dieses weisen wir einer Variable mit dem Namen `bob` zu. Wenn wir `bob` mit `print` ausgeben, erhalten wir eine Ausgabe ähnlich\n",
"\n",
"```\n",
"<turtle.Turtle object at 0xb7bfbf4c>\n",
"```\n",
"\n",
"Das bedeutet, dass `bob` auf ein Objekt vom Typ `Turtle` verweist, wie es im `turtle`-Modul definiert wurde.\n",
"\n",
"Der Aufruf von `mainloop` weist das Fenster an, auf Nutzeraktivität zu warten. In diesem Fall kann man als Nutzerin allerdings kaum mehr tun, als das Fenster zu schließen.\n",
"\n",
"Sobald wir eine Schildkröte erzeugt haben, können wir eine **Methode** aufrufen, um die Schildkröte über das Fenster zu bewegen. Eine Methode ist wie eine Funktion, aber die Syntax ist etwas anders. Z.B. können wir die Schildkröte mit dem Aufruf von \n",
"\n",
"```python\n",
"bob.fd(100)\n",
"```\n",
"\n",
"nach vorne bewegen. Die Methode `fd` gehört zu dem Turtle-Objekt welches wir `bob` nennen. Wenn wir die Methode aufrufen, bitten wir `bob` nach vorne zu gehen (**f**orwar**d**).\n",
"\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 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",
"Fügen Sie die folgenden Zeilen zu Ihrem Programm hinzu, um einen rechten Winkel zu zeichnen (nachdem Sie `bob` erzeugt haben und bevor Sie `mainloop` aufrufen):\n",
"\n",
"```python\n",
"bob.fd(100)\n",
"bob.lt(90)\n",
"bob.fd(100)\n",
"```\n",
"\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",
"\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)"
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 4.2 Einfache Wiederholung\n",
"\n",
"Vielleicht haben Sie folgendes geschrieben:\n",
"\n",
"```python\n",
"bob.fd(100)\n",
"bob.lt(90)\n",
"\n",
"bob.fd(100)\n",
"bob.lt(90)\n",
"\n",
"bob.fd(100)\n",
"bob.lt(90)\n",
"\n",
"bob.fd(100)\n",
"```\n",
"\n",
"Wir können das gleiche deutlich knapper mit einer `for`-Schleife formulieren. Testen Sie folgendes: "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for i in range(4):\n",
" print('Hello!')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Sie sollten folgende Ausgabe sehen:\n",
"\n",
"```\n",
"Hello!\n",
"Hello!\n",
"Hello!\n",
"Hello!\n",
"```\n",
"\n",
"Das ist die einfachste Art und Weise, eine `for`-Schleife zu verwenden. Wir werden uns das später ausführlicher anschauen, aber das sollte reichen, damit Sie ihr Programm zum Zeichnen eines Quadrats vereinfachen können. Bitte fahren Sie erst fort, wenn Sie ihr Programm mit Hilfe der `for`-Schleife vereinfacht haben.\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)\n",
"\n",
"Folgende `for`-Schleife zeichnet ein Quadrat:\n",
"\n",
"```python\n",
"for i in range(4):\n",
" bob.fd(100)\n",
" bob.lt(90)\n",
"```\n",
"\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",
"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"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 4.3 Übungen\n",
"\n",
"Es folgen einige Übungen zur \"Schildkrötenwelt\". Sie sollen Ihnen Spaß bereiten, aber die Übungen haben auch eine Bedeutung. Überlegen Sie, was das sein könnte, während Sie daran arbeiten.\n",
"\n",
"1. Schreiben Sie eine Funktion `square`, die einen Parameter namens `t` erwartet, welcher eine Schildkröte ist. Die Funktion soll die Schildkröte nutzen, um ein Quadrat zu zeichnen.\n",
"2. Schreiben Sie einen Funktionsaufruf, der `bob` als Argument an `square` übergibt und rufen Sie ihr Programm wieder auf.\n",
"3. Ergänzen Sie die Funktion `square` um einen weiteren parameter namens `length`. Ändern Sie den Rumpf der Funktion, so dass dass die Seitenlänge des gezeichneten Quadrats `length` entspricht. Passen Sie dann den Funktionsaufruf an, so dass ein weiteres Argument als Länge übergeben wird. Starten Sie ihr Programm noch einmal und testen Sie es mit verschiedenen Werten für die Länge.\n",
"4. Kopieren Sie die Definition der Funktion `square` und benennen Sie die Kopie in `polygon` um. Fügen Sie einen weiteren Parameter namens `n` hinzu und ändern Sie den Funktionsrumpf, so dass ein n-seitiges [reguläres Vieleck](https://de.wikipedia.org/wiki/Regelm%C3%A4%C3%9Figes_Polygon) (Polygon) gezeichnet wird. *Tipp: Die Außenwinkel eines n-seitigen Vielecks betragen 360/n Grad.* \n",
"5. Schreiben Sie eine Funktion `circle` die eine Schildkröte `t` und einen Radius `r` als Parameter erwartet und annähernd einen Kreis zeichnet, indem die Funktion `polygon` mit einer geeigneten Seitenlänge und Anzahl von Seiten aufruft. Testen Sie ihre Funktion mit verschiedenen Werten für `r`.\n",
"\n",
" *Tipp: Finden Sie den Umfang des Kreises heraus und stellen Sie sicher, dass `length * n` gleich dem Umfang ist.*\n",
" \n",
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
"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"
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 4.8 Ein Entwicklungsplan\n",
"\n",
"Ein **Entwicklungsplan** ist ein Prozess zum Schreiben von Programmen. Der Prozess, den wir in dieser Fallstudie verwendet haben ist \"Verkapselung und Verallgemeinerung\". Die Schritte dieses Prozesses sind:\n",
"\n",
"1. Wir schreiben ein kleines Programm ohne Funktionsdefinitionen.\n",
"2. Sobald das Programm funktioniert, identifizieren wir ein schlüssiges und zusammengehöriges Stück Code, verkapseln es in einer Funktion und geben ihm einen Namen.\n",
"3. Wir verallgemeinern die Funktion durch Hinzufügen geeigneter Parameter.\n",
"4. Wir wiederholen die Schritte 1 bis 3 bis wir eine Menge funktionierender Funktionen haben. Dabei kopieren wir Code und fügen ihn an der richtigen Stelle ein, um nochmaliges Eintippen zu vermeiden (und das damit ggf. verbundene Debugging).\n",
"5. Wir suchen nach Möglichkeiten, das Programm durch Refactoring zu verbessern. Wenn wir beispielsweise ähnlichen Code an verschiedenen Stellen haben, können wir überlegen, ob wir ihn nicht besser in eine geeignete allgemeine Funktion \"faktorisieren\". \n",
"\n",
"Dieser Prozess hat einige Nachteile (wir schauen uns Alternativen später an), aber er ist praktisch, wenn wir nicht vorab wissen, wie wir das Programm in Funktionen aufteilen könnten. Dieser Ansatz ermöglicht uns den Entwurf des Programms während wir es schreiben.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 4.9 Docstring\n",
"\n",
"Ein **Docstring** ist eine Zeichenkette am Anfang einer Funktion die die Schnittstelle der Funktion erklärt (\"doc\" ist kurz für \"documentation\", also Dokumentation). Hier ist ein Beispiel:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def polyline(t, n, length, angle):\n",
" \"\"\"Draws n line segments with the given length and\n",
" angle (in degrees) between them. t is a turtle.\n",
" \"\"\" \n",
" for i in range(n):\n",
" t.fd(length)\n",
" t.lt(angle)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Es gilt die Konvention, dass Docstrings in dreifachen Anführungszeichen gesetzt werden. Mit Hilfe der dreifachen Anführungszeichen kann der Text über mehr als eine Zeile hinwegreichen.\n",
"\n",
"Der Docstring im Beispiel ist knapp, enthält aber die wesentlichen Informationen, die jemand benötigt, der die Funktion nutzen möchte. Es wird prägnant erklärt, was die Funktion tut (ohne groß zu beschreiben, wie sie das tut). Es wird auch erklärt, welche Auswirkung jeder Parameter auf das Verhalten der Funktion hat und von welchem Typ jeder Parameter sein sollte (falls es nicht offensichtlich ist). \n",
"\n",
"Das Schreiben dieser Art von Dokumentation ist ein wichtiger Teil des Schnittstellenentwurfs. Eine gut konzipierte Schnittstelle sollte einfach zu erklären sein. Falls es Ihnen schwerfällt, Ihre Funktionen zu erklären, dann könnte es vielleicht helfen, die Schnittstelle zu verbessern. \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 4.10 Debugging\n",
"\n",
"Eine Schnittstelle ist wie ein Vertrag zwischen der Funktion und den Aufrufenden. Die Aufrufenden stimmen zu, bestimmte Parameter bereitzustellen und die Funktion stimmt zu, eine bestimmte Aufgabe zu erledigen.\n",
"\n",
"Beispielsweise benötigt die Funktion `polyline` vier Parameter:\n",
"- `t` muss eine Schildkröte (Typ `Turtle`) sein,\n",
"- `n` muss eine ganze Zahl sein,\n",
"- `length` muss eine positive Zahl sein,\n",
"- `angle` muss eine Zahl sein, die einen Winkel in Grad darstellt.\n",
"\n",
"Diese Anforderungen nennen wir auch **Vorbedingungen**, denn es wird vorausgesetzt, dass sie erfüllt sind, bevor die Funktion ausgeführt wird. Umgekehrt heißen Bedingungen am Ende einer Funktion **Nachbedingungen**. Nachbedingungen schließen die beabsichtigte Wirkung der Funktion (wie z.B. ein Linienstück zeichnen) und etwaige Seiteneffekte (wie z.B. die Schildkröte zu bewegen oder andere Änderungen vorzunehmen) ein. \n",
"\n",
"Die Erfüllung der Vorbedingungen ist die Aufgabe der Aufrufenden. Wenn die Aufrufenden eine (ordentlich dokumentierte!) Vorbedingung verletzen und die Funktion arbeitet nicht richtig, dann liegt der Fehler bei den Aufrufenden, nicht bei der Funktion.\n",
"\n",
"Wenn die Vorbedingungen erfüllt sind und die Nachbedingungen sind es nicht, dann liegt der Fehler bei der Funktion. Wenn ihre Vor- und Nachbedingungen klar und deutlich sind, dann kann das sehr beim Debugging helfen.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 4.11 Glossar\n",
"\n",
"Legen wir uns eine Liste mit den wichtigsten Begriffen an, die wir im Kapitel 4 gelernt haben:\n",
"\n",
"- Methode: Eine Funktion, die zu einem Objekt gehört und mit Hilfe der Punktnotation aufgerufen wird.\n",
"- Schleife: \n",
"- Verkapselung: \n",
"- Verallgemeinerung:\n",
"- Schlüsselwortargument:\n",
"- Schnittstelle:\n",
"- Refactoring: Der Prozess, ein funktionierendes Programm zu verändern, um die Schnittstellen der Funktionen und andere Aspekte des Codes zu verbessern.\n",
"- Entwicklungsplan:\n",
"- Docstring:\n",
"- Vorbedingung:\n",
"- Nachbedingung:\n",
"\n",
"Ergänzen Sie die Liste in eigenen Worten. Das ist eine gute Erinnerungs- und Übungsmöglichkeit."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 4.12 Übung\n",
"\n",
"#### Aufgabe 1\n",
"\n",
"Laden Sie den Code für dieses Kapitel hier herunter: http://thinkpython2.com/code/polygon.py\n",
"\n",
"*Für die folgenden Aufgaben benötigen Sie einen Editor, um den Code zu bearbeiten und Sie sollten mit der Ausführung von Python-Code ausserhalb von Jupyter vertraut sein. Alternativ können Sie versuchen, den Python-Code in ein leeres Jupyter-Notebook einzufügen und die Aufgaben dort zu lösen.*\n",
"\n",
"1. Zeichnen Sie ein Stapeldiagramm, welches den Zustand des Programms zeigt, während die Anweisung `circle(bob, radius)` ausgeführt wid. Sie können das manuell herausfinden, oder `print`-Anweisungen zum Code hinzufügen.\n",
"2. Die Version der Funktion `arc` in [Abschnitt 4.7](#4.7-Refactoring) ist nicht besonders präzise, denn die lineare Näherung eines Kreises ist immer ausserhalb des wahren Kreises. Daher steht die Schildkröte am Ende immer ein paar Pixel vom richtigen Ziel entfernt. Die Lösung im Python-Code zeigt eine Möglichkeit, die Auswirkung dieses Fehlers zu reduzieren. Lesen Sie sich den Code durch und schauen Sie, ob das für Sie einen Sinn ergibt. Vielleicht hilft es Ihnen weiter, ein Diagramm zu zeichnen.\n",
"\n",
"#### Aufgabe 2\n",
"\n",
"[Schildkröten-Blumen](http://greenteapress.com/thinkpython2/html/thinkpython2003.png)\n",
"\n",
"Schreiben Sie eine möglichst allgemeine Menge an Funktionen zum Zeichnen von solchen Blumen.\n",
"\n",
"*(Die [Lösung](http://thinkpython2.com/code/flower.py) benötigt auch [polygon.py](http://thinkpython2.com/code/polygon.py).)*\n",
"\n",
"#### Aufgabe 3\n",
"\n",
"[Schildkröten-Kuchen](http://greenteapress.com/thinkpython2/html/thinkpython2004.png)\n",
"\n",
"Schreiben Sie eine möglichst allgemeine Menge an Funktionen zum Zeichnen von solchen Figuren.\n",
"\n",
"*([Lösung](http://thinkpython2.com/code/pie.py))*\n",
"\n",
"#### Aufgabe 4\n",
"\n",
"*(Hinweis: Dies ist eher eine Fleiß- und Zusatzaufgabe.)*\n",
"\n",
"Die Buchstaben des Alphabets sind aus ein paar grundlegenden Elementen zusammengesetzt, wie z.B. vertikalen und horizontalen Linien und einigen Kurven. Entwerfen Sie ein Alphabet, welches mit einer kleinen Anzahl einfacher Elemente gezeichnet werden kann und schreiben Sie dann Funktionen zum Zeichnen der Buchstaben. \n",
"\n",
"Sie sollten eine Funktion für jeden Buchstaben schreiben, die Sie `draw_a`, `draw_b`, etc. nennen und diese Funktionen in eine Datei namens `letters.py` packen. Sie können [hier](http://thinkpython2.com/code/typewriter.py) eine \"Schildkrötenschreibmaschine\" herunterladen und damit ihren Code testen.\n",
"\n",
"Sie finden eine Lösung für diese Aufgabe hier: http://thinkpython2.com/code/letters.py; Diese benötigt auch http://thinkpython2.com/code/polygon.py.\n",
"\n",
"#### Aufgabe 5\n",
"\n",
"Lesen Sie etwas zu Spiralen auf Wikipedia: https://de.wikipedia.org/wiki/Spirale. Schreiben Sie dann ein Programm, welches eine [Archimedische Spirale](https://de.wikipedia.org/wiki/Archimedische_Spirale) (oder eine andere) zeichnet.\n",
"\n",
"Eine Lösung finden Sie hier: http://thinkpython2.com/code/spiral.py. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" 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",
"metadata": {},
"source": [
"\n",
"\n",
"Herzlichen Glückwunsch! Sie haben das 4. Kapitel geschafft. Weiter geht es in [5: Bedingungen und Rekursion](seminar05.ipynb)."