Heute besprechen wir die Funktionalität von Dictionarys noch einmal im Detail, anhand der Übungsaufgabe zu Würfelhäufigkeiten.
Die Aufgabenstellung lautete so:
Aufgabe 03-01: Random
Schreiben Sie ein Programm, das einen normalen, fairen Würfel simuliert.
Würfeln sie 10, 100, 100000, 10000000 mal und speichern Sie in Dictionaries
die Häufigkeit der erwürfelten Augenzahlen.
Lassen Sie sich die Ergebnisse der Versuche auf geeignete Weise ausgeben.
Was ist Ihre Erwartung bzgl. der Werte der Dictionaries? Wird diese erfüllt?
Welche Beobachtung machen Sie, wenn Sie die Versuche wiederholen?
Überlegen Sie zunächst, welche Befehle aus den bisherigen Unterrichtseinheiten hier hilfreich sein könnten.
In der Aufgabenstellung wird explizit erwähnt, dass die Ergebnisse in einem Dictionary gespeichert werden sollen. Hier müssen Sie sich überlegen, welche Struktur das Dictionary haben soll: Welche Informationen sind geeignete Schlüssel, und welche Informationen sind die dazugehörigen Werte? Welchen Typ sollten die Schlüssel und die Werte jeweils haben?
Definieren Sie hier ein Beispieldictionary, das ausgedachte Daten enthält und einer sinnvollen Struktur für diese Aufgabe folgt.
ergebnisse = {} # Verändern Sie dieses Dictionary, indem Sie ausgedachte
# Beispieldaten in einer von Ihnen gewählten Struktur einfügen.
In der Aufgabenstellung steht, dass Sie 10 (bzw 100, 100000, 10000000) mal würfeln sollen. Welches Konstrukt, das wir bereits gelernt haben, ist nützlich, um so eine Wiederholung von Programmteilen umzusetzen?
Schreiben Sie hier die Codezeilen, die dafür zuständig sind, 10 mal zu würfeln. Sie müssen das Ergebnis jedes Wurfs noch nicht weiterverarbeiten, das ist erst im nächsten Schritt dran.
# Erzeugen Sie hier mit bekannten Python-Konstrukten 10 (bzw 100, ...) Zufallszahlen.
Wir beginnen jetzt, die Teile zusammenzufügen, die wir eben einzeln geschrieben haben.
In vielen Teillösungen habe ich gesehen, dass Sie die Häufigkeit jeder einzelnen Zahl zunächst in sechs verschiedenen Variablen gespeichert haben und zum Schluss, also nach den 100 Würfelwürfen, die Ergebnisse in ein Dictionary geschrieben haben.
Sie können auch stattdessen das Dictionary mit der Zahl 0 für jedes Würfelergebnis initialisieren und bei jedem Wurf die beobachtete Zahl inkrementieren (um 1 erhöhen).
Welche Möglichkeit gefällt Ihnen besser? Warum?
Um den Umgang mit Dictionarys zu üben, schreiben wir beide Varianten. Ergänzen Sie die folgenden Kästchen mit dem jeweils fehlenden Code.
ergebnisse = {} # Ihre Dictionarystruktur von oben hier einfügen
# Hier Ihren Code für die 100 Wiederholungen des Würfelns einfügen
# Zum Schluss die Ergebnisse ausgeben
ones = 0
twos = 0
threes = 0
fours = 0
fives = 0
sixes = 0
# Hier Ihren Code für die 100 Wiederholungen des Würfelns einfügen
# Ergebnisse in einem Dictionary speichern
# Zum Schluss die Ergebnisse ausgeben
Der Prozess, den wir angewendet haben, um diese Aufgabe nach und nach in kleinen Einzelteilen zu lösen, wird manchmal Teilen und Herrschen genannt. Die Aufgabe als ganzes war unübersichtlich, obwohl wir jeden einzelnen Befehl, den wir schließlich geschrieben haben, kannten (bzw. nachschlagen konnten). Um das Problem handhabbar zu machen, haben wir es in Teilprobleme zerlegt. Jedes einzelne Teilproblem war gar nicht so schwer zu lösen.
Eine gute Beschreibung dieser Vorgehensweise finden Sie hier. In diesem Artikel wird das Lösen von Programmieraufgaben mit Aufgaben aus der echten Welt verglichen: Was gibt es heute zum Abendessen?
Tonight's dinner:
Find a nice recipe.
Make a list of ingredients to buy.
Go shopping for the ingredients that are needed.
Cook the dinner.
Each of these new tasks is a slightly smaller problem to solve than the original task but, together, they constitute a solution to the overall problem. We shall often see this kind of approach in solving computer problems.
Having broken the initial task down one level, we will often find that we need to break these second level tasks down still further:
Tonight's dinner:
Find a nice recipe.
Make a list of ingredients to buy.
Go shopping for the ingredients that are needed.
Cook the dinner.
Find a nice recipe:
Look in a few cookery books for a short list.
Check with everyone which recipe they would prefer.
Make a list of ingredients to buy:
Make a list of ingredients in the recipe.
Check the cupboards for which ingredients are missing.
Go shopping for the ingredients that are needed:
Get the car out.
Drive to Sainsburys.
Buy the ingredients.
Drive back home.
Cook the dinner:
Prepare the ingredients.
Put the oven on to the right temperature.
Follow the recipe.
This process might continue through several stages until the problem has reached the sort of level that we feel we can make a start on.
Ein etwas ausführlicheres Tutorial ist hier zu finden.
Versuchen Sie ab jetzt, Übungsaufgaben zuerst in Teilprobleme zu zerlegen, damit Sie diese dann lösen können. Das hilft Ihnen, sich einen Überblick zu verschaffen. Sie können dann sich selbst und den Dozierenden konkretere Fragen stellen.
Der Inhalt von Variablen ist immer bis zum Ende der Laufzeit verfügbar. Wenn wir Informationen langfristig speichern wollen, machen wir das in Dateien, die im Dateisystem des Computers, auf dem das Programm läuft, abgelegt werden.
Der folgende Code erzeugt eine Datei und schreibt alles, was Sie während der Laufzeit eingeben, in diese Datei:
filename = "secrets.txt"
with open(filename, "w") as outfile:
content = input("Hier können Sie Ihre Geheimnisse eingeben:\n")
# Dieser print-Befehl gibt den Inhalt in die Datei outfile aus.
print(content, file=outfile)
# Dieser print-Befehl zeigt die Ausgabe direkt an.
print("Daten wurden in der Datei " + filename + " gespeichert!")
Den Dateinamen können Sie frei wählen. Wir verwenden meist die Dateiendung .txt
für Textdateien. Wenn wir nur den Namen angeben, wird die Datei im gleichen Verzeichnis erzeugt, in dem unser Pythoncode ausgeführt wird, hier also in dem Verzeichnis, in dem Sie auch das Jupyter Notebook abgelegt haben. Um einen anderen Speicherort zu wählen, geben wir einfach absolute Pfade an, z.B. filename = "D:/Files/secrets.txt"
.
Das Öffnen der Datei ähnelt optisch einer Schleife oder einem if
-Block: Auch hier sind einige Zeilen nach rechts eingerückt. Der Block wird mit der Zeile eröffnet, die die Datei aus dem Dateisystem öffnet:
with open(<filename>, <read-or-write mode>) as <variablename>:
Für alle eingerückten Zeilen ist die geöffnete Datei verfügbar. Sobald wieder Code ohne Einrückung folgt, wird die Datei automatisch vom Interpreter geschlossen.
Um Inhalte in die Datei zu schreiben, können wir print
mit dem zusätzlichen Paramenter file
verwenden. Dabei muss der Wert für file
der Variablenname sein, den wir beim Öffnen der Datei erzeugt haben.
Wir haben oben den Modus "w"
wie write
(schreiben) gewählt. Bei jeder Ausführung des Codes wird die Datei komplett überschrieben. Wenn wir stattdessen "a"
wie append
(anhängen) verwenden, wird neuer Inhalt immer nach dem bisherigen Inhalt platziert, sodass nichts verlorengeht. Probieren Sie es oben aus!
Um eine existierende Datei in unserem Pythoncode zu öffnen, verwenden wir den Modus "r"
wie read
(lesen). Dann können wir z.B. mit einer for
-Schleife durch den Inhalt der Datei iterieren. Dabei wird jede Zeile als String aus der Datei gelesen und kann mit unseren verfügbaren Stringmethoden weiterverarbeitet werden.
Konventionell verwenden wir für Dateien, die geschrieben werden, den Variablennamen outfile
, und für Dateien, die gelesen werden, den Variablennamen infile
.
filename = "secrets.txt"
with open(filename, "r") as infile:
for line in infile:
print(line)
Wenn wir die Datei nicht zeilenweise verarbeiten möchten, sondern alles auf einmal lesen wollen, verwenden wir read()
. Achtung: Bei großen Dateien kann das schnell zu Problemen führen, da der gesamte Inhalt der Datei in den Arbeitsspeicher passen muss.
Bei vielen Dateiformaten hier im Kurs ist das Lesen einzelner Zeilen angebracht.
So wird read()
verwendet:
filename = "secrets.txt"
with open(filename, "r") as infile:
content = infile.read()
print(content)
import datetime
for i in range(5): # 5 Räume
print("YOU ARE IN A MAZE OF TWISTY LITTLE PASSAGES, ALL ALIKE.")
direction = input("> ")
if direction not in ["east", "west", "south", "north"]:
print("[please choose one of the 4 directions east, west, north, south]")
else:
print(datetime.datetime.utcnow()) # aktuelles Datum und Zeit ausgeben
print("You moved " + direction + ".\n")
os
¶Wenn Sie zu Beginn Ihres Programms das Modul os
importieren, können Sie zur Laufzeit mit den Methoden dieses Moduls prüfen, ob eine Datei existiert. Das ermöglicht Ihnen unter anderem, Dateipfade als Usereingabe zu verarbeiten, ohne dass das Programm abstürzt, falls die Datei nicht gefunden wird.
Probieren Sie es aus!
import os
filepath = input("Geben Sie einen Dateipfad ein: ")
if os.path.isfile(filepath):
with open(filepath, "r") as infile:
for line in infile:
print(line)
else:
print("Sorry, couldn't find that file.")
Der Befehl os.path.isfile()
gibt True
zurück, wenn der Pfad auf eine existierende Datei zeigt. Sie können auch os.path.isdir()
verwenden, um zu prüfen, ob der Pfad auf einen existierenden Ordner zeigt.
Es gibt in Python einen speziellen Datentyp namens None
, der leere Objekte bezeichnet, die keinen anderen Datentyp haben.
print(type(None))
# Leere Listen, Strings usw. haben ihren eigenen Datentyp:
print(type([]))
print(type(""))
Oft haben Methoden, die z.B. auf veränderlichen Objekten operieren, den Rückgabetyp None
- es muss immer einen Rückgabetyp geben, und None
bekommen wir, wenn nichts anderes zurückgegeben wird. Wenn Sie also eine Fehlermeldung wie die folgende sehen...
for i in [1,2,3].append(4):
print(i)
... liegt das mit hoher Wahrscheinlichkeit daran, dass das Ergebnis der Methode append()
vom Typ None
ist und nicht - wie im Codebeispiel sinnvoll wäre - eine Liste.
Damit der Code ohne Fehler ausgeführt wird, können wir beispielsweise folgendes schreiben:
for i in [1,2,3] + [4]:
print(i)
None
kann auch als Boolean ausgewertet werden und hat dann den Wert False
. Das ist auch bei leeren Strings, leeren Listen und der Zahl 0
der Fall:
print(bool(None))
print(bool([]))
print(bool(""))
print(bool(0))
print("-----------")
print(bool([5,3,1]))
print(bool("String mit Inhalt"))
print(bool(-10))
Wenn Sie mitten in Ihrem Programm den Wert einer Variable ausgeben lassen und None
angezeigt wird, wissen Sie, dass etwas nicht geklappt hat. Prüfen Sie in so einem Fall alle Stellen, an denen der Wert der Variable verändert wurde, daraufhin, ob eventuell keine Rückgabe geliefert wurde und daher None
der Wert der Variable ist.
Inzwischen haben wir schon eine Menge Programmiergrundlagen behandelt und Sie sind bereit, komplexere Aufgaben zu bearbeiten. Funktionen bieten Ihnen die Möglichkeit, Sinnabschnitte Ihrer Programme vom Rest abzukapseln und so den Überblick zu bewahren.
Stellen Sie sich beispielsweise vor, dass Sie Quadratzahlen berechnen wollen. Bisher haben wir das etwa so gemacht:
input_number = 3 # Variable erzeugen und mit dem Wert 3 belegen
result = input_number * input_number # Quadratzahl berechnen
print(result) # Ergebnis ausgeben
Wenn wir genau eine Quadratzahl berechnen wollen, ist gegen den Code oben nichts einzuwenden. Wenn wir die Berechnung aber mehrmals für unterschiedliche Werte durchführen wollen, lohnt es sich, den Code dafür allgemeiner zu schreiben. Dann können wir die Funktionalität "Quadratzahl berechnen" vom restlichen Code abkapseln und jederzeit aufrufen, wenn wir sie brauchen.
Eine Funktion hat in Python die folgende Form:
def calculate_square(input_number): # Funktionsdefinition eröffnen
result = input_number * input_number # beliebig viele Befehle
return result # Funktionsdefinition beenden
Die Einrückung zeigt, welche Zeilen zur Funktionsdefinition gehören. Der Kopf der Funktion enthält den Namen (hier: calculate_square
) und eine Angabe aller Parameter, die wir in der Funktion brauchen. (Erinnern Sie sich an die Parameter in der Sitzung am 16.10., z.B. s.split(".", 2)
)
In der Beispielfunktion heißt der einzige Parameter input_number
.
Im Körper der Funktion folgen beliebig viele Befehle, in denen das Argument (der konkrete Wert, der als Parameter verwendet wird) verarbeitet wird. Beispielsweise berechnen wir oben das Quadrat der übergebenen Zahl. Das Ergebnis wird hier in der Variable result
gespeichert.
Schließlich gibt die Funktion mit return
einen Wert zurück. Im Beispiel ist das der Wert, der in der Variable result
gespeichert ist. Da return
die Funktion sofort beendet, werden keine eingerückten Zeilen unterhalb von return
mehr ausgeführt.
Wir können uns Funktionen wie eine Art Rezept vorstellen, nach dem bestimmte Aufgaben gelöst werden. Die Funktionsdefinition, die wir oben sehen, braucht noch einen konkreten Eingabewert (ein Argument für den Parameter input_number
), damit ein Ergebnis berechnet werden kann.
Erst, wenn die Funktion im Code mit einem konkreten Argument aufgerufen wird, werden die Befehle innerhalb des Funktionskörpers ausgeführt. Das Ergebnis - der Wert, der in der letzten Zeile der Funktion zurückgegeben wird - kann dann an der Stelle im Code, wo die Funktion aufgerufen wurde, verwendet werden.
Probieren Sie es aus: Führen Sie den Code in der nächsten Zelle aus und beachten Sie, in welcher Reihenfolge die print()
-Befehle ausgeführt werden.
print("1")
def calculate_square(input_number):
print("2")
result = input_number * input_number
return result
print("3")
my_square = calculate_square(4) # das Ergebnis des Funktionsaufrufs calculate_square mit dem Argument 4
# wird in der Variable my_square gespeichert
print("Ergebnis der Berechnung: " + str(my_square))
print("4")
Im Gegensatz zu z.B. Schleifen sind Variablen, die in Funktionskörpern definiert werden, außerhalb der Funktion nicht mehr verfügbar. Probieren Sie es aus: Ergänzen Sie im Beispiel unterhalb des bisherigen Codes einen print()
-Aufruf für result
oder input_number
. Das Programm stürzt ab.
Mithilfe von Funktionen können wir Programmcode, der vorher unübersichtlich war, auslagern. Wir geben Funktionen dafür sprechende Namen und lassen jede Funktion genau eine Aufgabe erfüllen.
Beachten Sie, dass der Interpreter eine Funktion gelesen haben muss, bevor sie ausgeführt werden kann. Eine Funktion muss also immer oberhalb ihres ersten Aufrufs im Hauptprogramm definiert werden.
Mit Funktionen können wir Redundanz (unnötige Wiederholungen) in unserem Code vermeiden. Jedesmal, wenn wir später im Programm eine Quadratzahl berechnen wollen, können wir die eben definierte Funktion verwenden, statt immer wieder die gleichen Codezeilen an verschiedenen Stellen einzufügen. Die Beispielfunktion ist sehr übersichtlich, aber wenn eine Funktion eine komplexere Aufgabe erfüllt - z.B. Daten nach bestimmten Kriterien zu filtern - , wollen wir vermeiden, Codezeilen mehrfach zu schreiben.
Codewiederholungen sind eine typische Fehlerquelle. Außerdem machen Sie unser Programm länger und dadurch unübersichtlicher. Mit Funktionen vermeiden wir solche Wiederholungen.
########### Funktionsdefinition ############
def filter_dictionary_only_keep_nouns(input_dict):
"""
Diese Funktion filtert das eingegebene Dictionary so, dass zum Schluss
ein Dictionary zurückgegeben wird, das nur die Nomen aus dem ursprünglichen
Dictionary enthält
"""
output_dict = {}
# Hier Code einfügen
return output_dict
########### Hauptprogramm ############
words = {"Terminal": "N", "for-Schleife": "N", "einrücken": "V", "mutable": "A", "zurückgeben": "V", "redundant": "A", "Funktion": "N"}
filtered_words = {}
for w in words:
if words[w] == "N":
filtered_words[w] = "N"
print("gefiltertes Dictionary (nur Nomen): {}".format(filtered_words))
Versuchen Sie ab jetzt, Übungsaufgaben durch den sinnvollen Einsatz von Funktionen zu lösen. Dadurch erhalten Sie Zwischenergebnisse, können Programmierfehler besser finden, und bei Änderungen im Code sehen Sie leichter, welche Funktionen angepasst werden müssen und welche gleich bleiben können.
return
-Statements schreiben, die in unterschiedlichen Fällen greifen. Achtung: Die Funktion soll in jeder möglichen Verwendung eine Rückgabe liefern.Sie haben heute gelernt,
os
zu prüfen, ob eine Datei/ein Verzeichnis tatsächlich existiertNone
ist