Dictionaries
Einführung
In den vorigen Kapiteln haben wir die sequentiellen Datentypen wie Listen, Strings und Tupel eingeführt. Nun wollen wir eine weitere Kategorie von Datentypen genauer untersuchen, die "Mapping". In dieser Kategorie gibt es allerdings zur Zeit nur einen implementierten Typ, das Dictionary. Beim Dictionary handelt es sich um ein assoziatives Feld. Assoziative Felder werden in verschiedenen Programmiersprachen mit verschiedenen Namen versehen. So spricht man in Java und C++ von einer Map, in Perl und Ruby von einem Hash und wie in Python bezeichnen auch Smalltalk, Objective-C und C# assoziative Arrays als Dictionaries.
Ein Dictionary besteht aus Schlüssel-Objekt-Paaren. Zu einem bestimmten Schlüssel gehört immer ein Objekt. Man kann also die Schlüssel auf die Objekte "abbilden", daher der Kategorienname Mapping.
Dictionaries gehören zu den wichtigsten Datentypen von Python. Kaum ein Programm oder Skript kommt ohne Dictionaries aus. Wie Listen können Dictionaries leicht verändert werden, außerdem können sie beliebig wachsen und schrumpfen während der Laufzeit.
In diesem Kapitel geht es aber nicht nur um Dictionaries sondern am Ende beschäftigen wir uns auch noch mit dem Zusammenhang zwischen Listen und Dictionaries, d.h. wir zeigen wie man aus Dictionaries Listen erzeugt und umgekehrt, wie man aus bestimmten Listen Dictionaries erzeugen kann.
Beispiele für Dictionaries
Unser erstes Beispiel ist ein Dictionary mit deutschen Städten und Einwohnerzahlen, die im Jahre 2019 mehr als eine halbe Million Einwohner hatten. Wir sehen, dass Dictionaries mit geschweiften Klammern geschrieben werden. Sie enthalten Schlüssel-Werte-Paare, die mit Kommata getrennt sind. Ein Schlüssel (englisch key) und sein zugehöriger Wert werden jeweils durch einen Doppelpunkt getrennt.
städte_einwohner = {"Berlin": 3_669_491,
"Hamburg": 1_847_253,
"München": 1_484_226,
"Köln": 1_087_863,
"Frankfurt am Main": 763_380,
"Stuttgart": 635_911,
"Düsseldorf": 621_877,
"Leipzig": 593_145,
"Dortmund": 588_250,
"Essen": 582_760,
"Bremen": 567_559,
"Dresden": 556_780,
"Hannover": 536_925,
"Nürnberg": 518_370}
Wir können auf den Wert für einen bestimmten Schlüssel zugreifen, indem wir diesen Schlüssel in Klammern nach dem Namen des Wörterbuchs angeben:
städte_einwohner["Stuttgart"]
Was passiert, wenn wir versuchen, auf einen Schlüssel zuzugreifen, d. h. eine Stadt in unserem Beispiel, die nicht im Dictionary enthalten ist? Wir erheben einen KeyError:
städte_einwohner["Konstanz"]
Eine häufig gestellte Frage ist, ob Dictionary-Objekte geordnet sind. Die Unsicherheit ergibt sich aus der Tatsache, dass Dictionaries in Versionen vor Python 3.7 nicht sortiert waren. In Python 3.7 und alle späteren Versionen werden Dictionaries nach der Reihenfolge der Item-Eintragung sortiert. In unserem Beispiel bedeutet dies, das unser städte_einwohner
-Dictionary die bei der Definition benutzte Reihenfolge beibehält. Dies sieht man, wenn man sich das Dictionary ausgeben lässt:
städte_einwohner
Die Tatsache, dass sie geordnet sind, bedeutet aber nicht, dass es eine Möglichkeit gibt, das n-te Element eines Wörterbuchs direkt aufzurufen. Versucht man, auf ein Wörterbuch mit einer Zahl zuzugreifen - wie wir dies mit Listen tun - führt dies zu einer Ausnahme:
städte_einwohner[0]
Wir hatten gesehen, dass wir auch eine Ausnahme erheben, wenn wir einen nicht existierenden Städenamen verwenden. Es ist sehr einfach weitere Einträge einem Dictionary einzufügen. Wir zeigen dies am Beispiel der Stadt Konstanz:
städte_einwohner["Konstanz"] = 84911
Wir können sehen, dass Konstanz ans Ende der Liste eingefügt worden ist:
städte_einwohner
Dies ist also eine Möglichkeit ein Dictionary während des Programmlaufes zu erweitern. Denkbar ist dann auch mit einem leeren Dicitonary zu beginnen. Ein leeres Dictionary wird mit einem geschweiften Klammernpaar {}
oder dem Aufruf dict()
erzeugt:
food = {}
Alternativ kann man food
auch so erzeugen:
food = dict()
Zu Ehren des Schutzheiligen von Python "Monty Python" erweitern wir nun dieses Dictionary um spezielle pythonische kulinarische Besonderheiten. Was wäre Python ohne "ham", "eggs", "cheese" und "spam"? Wir erweitern unser Dictionary nun inkrementell um diese Delikatessen:
food["ham"] = "yes"
food["eggs"] = "yes"
food["cheese"] = "yes"
food
Haben wir da nicht noch etwas vergessen? Leider, denn wer mag schon Spam. Bisher haben wir als Wert "yes" verwendet. Bei "spam" werden wir nun "no" verwenden. Bitte beachte auch, dass die Werte in einem Dictionary nicht eindeutig sein müssen, weshalb wir mehrmals "yes" verwenden konnten. Die Schlüssel sind jedoch eindeutig.
food["spam"] = "no"
food
Im nächsten Beispiel kreieren wir ein einfaches Deutsch-Englisches Wörterbuch als Dictionary:
en_de = {"red" : "rot", "green" : "grün", "blue" : "blau", "yellow":"gelb"}
print(en_de)
Wie wäre es mit einem weiteren Dictionary, eines was von Deutsch nach Französisch übersetzt? Damit sind wir dann aber auch in der Lage von Englisch nach Französisch zu übersetzen, indem wir die beiden Dictionaries hintereinander schalten, wie wir im folgenden Beispiel demonstrieren. Mit Hilfe dieses Dictionarys können wir nun von Englisch nach Französisch übersetzen, obwohl wir gar kein Englisch-Französisch-Wörterbuch haben. So gibt uns de_fr[en_de["red"]] das französische Wort für "red", also "rouge". Dies geschieht in zwei Schritten: Erst wird von Python der Ausdruck en_de["red"] ausgewertet. Das Ergebnis der Auswertung "rot" wird dann im de_fr-Dictionary "nachgeschaut", d.h. de_fr["rot"] liefert nun "rouge" zurück:
en_de = {"red" : "rot", "green" : "grün", "blue" : "blau", "yellow":"gelb"}
print(en_de)
print(en_de["red"])
de_fr = {"rot": "rouge", "grün": "vert", "blau": "bleu", "gelb":"jaune"}
print("The French word for red is: " + de_fr[en_de["red"]])
In einem Dictionary können beliebige Typen als Werte verwendet werden. Bei den Schlüsseln gilt jedoch die Einschränkung, dass nur Instanzen unveränderlicher (immutable) Datentypen verwendet werden können, also z.B. keine Listen und keine Dictionaries. Ansonsten erhält man eine Fehlermeldung:
dic = { [1,2,3]:"abc"}
Tupel als Schlüssel sind in Ordnung, wie wir im folgenden Beispiel sehen:
dic = { (1,2,3): "abc", 3.1415: "abc"}
dic
Nun wollen wir unsere Beispiele mit Wörterbüchern für natürliche Sprachen noch ein wenig aufpeppen. Dazu definieren wir eine Dictionary von Dictionaries:
en_de = {"red" : "rot", "green" : "grün", "blue" : "blau", "yellow":"gelb"}
de_fr = {"rot" : "rouge", "grün" : "vert", "blau" : "bleu", "gelb":"jaune"}
dictionaries = {"en_de" : en_de, "de_fr" : de_fr }
print(dictionaries["de_fr"]["blau"])
Operatoren auf Dictionaries
Operator |
Erklärung |
---|---|
len(d) |
liefert die Anzahl aller im
Dictionary enthaltenen Elemente, d.h. Schlüssel-Werte-Paare |
del d[k] |
Löschen des Schlüssels
k zusammen mit seinem Wert |
k in d |
True, wenn es im
Dictionary d einen Schlüssel k gibt |
k not in d |
True, wenn es im Dictionary d
keinen Schlüssel k gibt |
morse = {
"A" : ".-",
"B" : "-...",
"C" : "-.-.",
"D" : "-..",
"E" : ".",
"F" : "..-.",
"G" : "--.",
"H" : "....",
"I" : "..",
"J" : ".---",
"K" : "-.-",
"L" : ".-..",
"M" : "--",
"N" : "-.",
"O" : "---",
"P" : ".--.",
"Q" : "--.-",
"R" : ".-.",
"S" : "...",
"T" : "-",
"U" : "..-",
"V" : "...-",
"W" : ".--",
"X" : "-..-",
"Y" : "-.--",
"Z" : "--..",
"0" : "-----",
"1" : ".----",
"2" : "..---",
"3" : "...--",
"4" : "....-",
"5" : ".....",
"6" : "-....",
"7" : "--...",
"8" : "---..",
"9" : "----.",
"." : ".-.-.-",
"," : "--..--"
}
Sie können obiges Dictionary als morsecode.py abspeichern, um die folgenden Beispiele leichter nachvollziehen zu können. Zuerst müssen wir das Dictionary wie folgt importieren:
from morsecode import morse
Die Anzahl der verschiedenen Zeichen für die unser Dictionary eine Abbildung in ein Morsezeichen besitzt, können wir mittels len() bestimmen:
len(morse)
Unser Dictionary enthält nur Großbuchstaben. "a" in morse liefert deshalb zum Beispiel False zurück:
"a" in morse
"A" in morse
"a" not in morse
pop() und popitem()
pop
Listen können als Stack gesehen und benutzt werden. Dabei wird die pop()-Methode benutzt um ein Element vom Stack zu nehmen. So weit so gut, wenn es um Listen geht, aber macht die pop()-Methode Sinn für Dictionaries. Schließlich ist der dict-Typ kein sequentieller Datentyp, hat also keine Anordnung und keine Indizierung. Aber es gibt dennoch eine pop-Methode für Instanzen des Typs dict. Allerdings ist pop() anders definiert. Falls D ein Dictionary bezeichnet, dann entfernt D.pop(k) den Index k zusammen mit seinem Wert aus dem Dictionary, außerdem liefert D.pop(k) den Wert von D[k] als Rückgabewert zurück. Falls der Schlüssel nicht in D gefunden wird, wird der KeyError generiert:
capitals = {"Österreich":"Wien", "Deutschland":"Berlin", "Niederlande":"Amsterdam"}
capital = capitals.pop("Deutschland")
print(capital)
print(capitals)
capital = capitals.pop("Schweiz")
Der Versuch die Hauptstadt der Schweiz zu ermitteln führte im vorigen Beispiel zum Ausnahmefehler KeyError, weil es diesen Eintrag im Dictionary nicht gibt. Um solche Fehler zu vermeiden, gibt es einen eleganten Weg in Python. Die Methode pop() hat dazu einen zweiten optionalen Parameter, mit dem man einen Default-Wert für diesen Fall mitgeben kann:
capital = capitals.pop("Schweiz","unbekannt")
print(capital)
capitals.pop("Niederlande","unbekannt")
capitals.pop("Niederlande","unbekannt")
capitals = {"Hessen":"Wiesbaden", "Saarland":"Saarbrücken", "Baden-Württemberg":"Stuttgart", "Rheinland-Pfalz":"Mainz", "Nordrhein-Westfalen":"Düsseldorf"}
(land, capital) = capitals.popitem()
print(land, capital)
pair = capitals.popitem()
print(pair)
print(capitals)
woerter = {"house" : "Haus", "cat":"Katze"}
woerter["car"]
Man kann dies wie folgt absichern:
if "car" in woerter: print(woerter["car"])
if "cat" in woerter: print(woerter["cat"])
Eine weitere Methode auf die Werte über die Schlüssel zuzugreifen ist mit der Methode get() gegeben. Auch ihr kann als zweiter Parameter ein Default-Wert mitgegeben werden:
capitals = {"Sachsen":"Dresden", "Niedersachsen":"Hannover", "Brandenburg":"Potsdam"}
capital = capitals["Sachsen"]
print(capital)
capital = capitals["Thüringen"]
capital = capitals.get("Sachsen")
print(capital)
capital = capitals.get("Thüringen")
print(capital)
capital = capitals.get("Thüringen","Erfurt")
print(capital)
w = woerter.copy()
woerter["cat"]="chat"
print(w)
print(woerter)
Bei diesem Kopieren handelt es sich um eine flache (shallow) Kopie. Wenn es sich bei einem Wert um einen komplexen Datentyp, wie z.B. eine Liste oder ein anderes Dictionary handelt, wirken sich Änderungen innerhalb eines solchen Wertes sowohl auf die Kopie als auch auf das Original aus.
# -- coding: utf-8 --
trainings = { "course1":{"title":"Python Training Course for Beginners",
"location":"Frankfurt",
"trainer":"Steve G. Snake"},
"course2":{"title":"Intermediate Python Training",
"location":"Berlin",
"trainer":"Ella M. Charming"},
"course3":{"title":"Python Text Processing Course",
"location":"München",
"trainer":"Monica A. Snowdon"}
}
trainings2 = trainings.copy()
trainings["course2"]["title"] = "Perl Training Course for Beginners"
print(trainings2)
Wenn wir uns die Ausgaben anschauen, sehen wir, dass der Wert von "title" von "course2" sich sowohl in trainings als auch in trainings2 geändert hat.
Alles funktioniert so, wie man sich eine Kopie vorstellt, wenn man einem Schlüssel einen komplett neuen Wert, also ein neues Objekt zuordnet:
trainings = { "course1":{"title":"Python Training Course for Beginners",
"location":"Frankfurt",
"trainer":"Steve G. Snake"},
"course2":{"title":"Intermediate Python Training",
"location":"Berlin",
"trainer":"Ella M. Charming"},
"course3":{"title":"Python Text Processing Course",
"location":"München",
"trainer":"Monica A. Snowdon"}
}
trainings2 = trainings.copy()
trainings["course2"] = {"title":"Perl Seminar for Beginners",
"location":"Ulm",
"trainer":"James D. Morgan"}
print(trainings2["course2"])
Für diejenigen, die mehr über die tieferen Gründe für dieses Verhalten erfahren wollen, empfehlen wir unser Kapitel über "Flaches und tiefes Kopieren".
Der Inhalt eines Dictionary kann mittels der Methode clear() geleert werden. Das Dictionary wird dabei nicht gelöscht sondern wirklich nur geleert:
trainings.clear()
print(trainings)
w={"house":"Haus","cat":"Katze","red":"rot"}
w1 = {"red":"rouge","blau":"bleu"}
w.update(w1)
print(w)
d = {"a":123, "b":34, "c":304, "d":99}
for key in d:
print(key)
Man kann aber auch die Methode keys() benutzen, die einem speziell die Schlüssel liefert:
for key in d.keys():
print(key)
Mit der Methode values() iteriert man direkt über die Werte:
for value in d.values():
print(value)
Wenn man nur auf die Ergebnisse schaut, ist das äquivalent zu der folgenden Schleife:
for key in d:
print(d[key])
Was die Implementierung und die Effizienz betrifft, ist die letzte Methode jedoch weniger effizient. Im Folgenden zeigen wir, dass der erste Weg deutlich schneller ist. Um das Folgende zu verstehen, sollte man sich mit %%timeit von ipython auskennen:
%%timeit d = {"a":123, "b":34, "c":304, "d":99}
for key in d.keys():
x=d[key]
x
%%timeit d = {"a":123, "b":34, "c":304, "d":99}
for value in d.values():
x=value
x
Zusammenhang zwischen Listen und Dictionaries
Wenn man eine Weile mit Listen und Dictionaries unter Python gearbeitet hat, kommt nahezu zwangsläufig der Punkt, dass man Listen in Dictionaries oder umgekehrt Dictionaries in Listen wandeln will bzw. muss. Es wäre nicht allzu schwer, direkt mit den bisher bekannten Funktionen sich neue Funktionen zu schreiben, die genau dies bewerkstelligen. Aber Python wäre nicht Python, wenn es hierfür nicht auch Hilfen zur Verfügung stellen würde.
Es bietet sich an, ein Dictionary der Art
{"list":"Liste", "dictionary":"Wörterbuch", "function":"Funktion"}als Liste von 2-Tupel darzustellen.
[("list","Liste"), ("dictionary","Wörterbuch"), ("function","Funktion")]
colours = {"red":"rot", "green":"grün", "blue":"blau", "yellow":"gelb"}
Nehmen wir nun an, dass wir daraus gerne die Menge aller Schlüssel, also alle englischen Farbadjektive extrahieren wollen. Dazu bietet die Klasse dict die Methode "keys" an. Zu dieser Methode sagt uns die help-Funktion help(dict.keys):
help(dict.keys)
Zu Deutsch bedeutet dies, dass D.Keys für ein Dictionary D ein Mengen-ähnliches Objekt zurückliefert, was uns eine "view" (Sicht) auf die Schlüssel (keys) von D liefert. Eine "view" ist ein spezielles Python-Objekt, was in diesem Fall, wie die Hilfe sagt, ein einer Menge ähnelndes Objekt erzeugt. Dieses Objekt ist aber ein Iterator, der seine Elemente aus dem Dictionary generiert, ohne dass eine Menge generiert wird. Es erfolgt also keine Kopie.
english_colours = colours.keys()
english_colours
Im folgenden Beispiel können wir sehen, dass sich diese View auch "ändert", wenn das Dictionary geändert wurde:
colours["black"] = "schwarz"
english_colours
Möchte man aus dem dict_keys-Objekt eine "echte" Menge oder eine Liste erzeugen, so kann man dies mit den globalen Funktionen set und list bewerkstelligen:
english_colours_set = set(english_colours)
colours["white"] = "weiß"
english_colours
english_colours_set
english_colours_list = list(english_colours)
english_colours_list
Analog dazu kann man sich auch eine view für die Werte eines Dictionaries (dict_values) mittels der Methode values erzeugen:
german_colours = colours.values()
german_colours
Außerdem gibt es noch die methode items(), mit der man sich eine View der Schlüssel-Werte-Paare erzeugen kann:
pairs = colours.items()
pairs
set(pairs)
list(pairs)
Semantisch gesehen beinhaltet eine Liste/View, die mittels der Methode items() aus einem Dictionary erzeugt wurde, den kompletten Informationsgehalt des ursprünglichen Dictionary. Allerdings gibt es keine Zugriffsfunktionen über die Schlüssel (Keys), d.h. die erste Komponente der 2-Tupel und der Werte (Values), also der zweiten Komponente der 2-Tupel.
Listen in Dictionaries wandeln
Auch wenn wir uns im folgenden ein wenig mit Essen beschäftigen, bleibt dies ein Python-Kurs und wird kein Kochkurs. Nehmen wir an, dass wir zwei Listen haben, von denen eine die Keys und die andere die Werte enthält:
gerichte = ["Pizza", "Sauerkraut", "Paella", "Hamburger"]
laender = ["Italien", "Deutschland", "Spanien", "USA"]
Nun wollen wir ein Dictionary erzeugen, dass einem Gericht ein Land zuordnet. (Sorry, dass wir hier die Vorurteile bedienen.) Dazu benötigen wir die Funktion zip(). Der Name zip wurde gewählt, weil zip "Reißverschluss" im Englischen bedeutet. Wie ein Reißverschluss nimmt zip zwei oder mehr Listen - genauer gesagt iterierbare Objekte - als Argumente und liefert einen Iterator zurück, der die i-ten Komponenten der Argumente zu Tupeln zusammenfasst.
In unserem kulinarischen Beispiel sieht das wie folgt aus:
gericht_land_iterator = zip(laender, gerichte)
gericht_land_iterator
land_gericht = list(gericht_land_iterator)
land_gericht
In der Variablen land_gericht haben wir nun eine Liste, in der die Elemente 2-Tupel sind mit den Schlüsseln und den Werten. Logisch gesehen sind wir also einem Dictionary schon sehr nahe. Was wir aber eigentlich wollen, ist ein richtiges Python-Dictionary. Glücklicherweise gibt es auch hierfür eine Funktion. dict() nimmt als Argument eine Liste der obigen Form und wandelt sie in ein Dictionary um:
land_gericht_dict = dict(land_gericht)
print(land_gericht_dict)
Allerdings ist das obige Vorgehen äußerst ineffizient, da wir zuerst eine Liste mit Zweiertupel erzeugen und diese dann in ein Dictionary wandeln. Man kann "dict" auch direkt auf das Ergebnis von zip aufrufen:
gerichte = ["Pizza", "Sauerkraut", "Paella", "Hamburger"]
laender = ["Italien", "Deutschland", "Spanien", "USA"]
dict(zip(laender, gerichte))
Eine Frage stellt sich noch bezüglich der Funktion zip(). Was passiert, wenn eine der beiden Listen mehr Elemente als die andere hat? Ganz einfach, die überzähligen Elemente werden nicht verarbeitet, d.h. sie kommen nicht in die Ergebnisliste.
laender = ["Italien","Deutschland","Spanien","USA","Schweiz"]
gerichte = ["Pizza", "Sauerkraut", "Paella", "Hamburger"]
land_gericht = zip(laender,gerichte)
print(land_gericht)
Die brennende Frage, was wohl das Nationalgericht der Schweiz ist, bleibt, zumindest was diesen Kurs betrifft unbeantwortet!
Alles in einem Schritt
Normalerweise empfehlen wir nicht zu viele Schritte in eine Anweisung oder einen Ausdruck zu stecken, auch wenn dies eindrucksvoll ausschauen mag und der Code dadurch sehr klein und kompakt wird. Es ist häufig besser Zwischenschritte mit sprechenden Variablen einzubauen. Dadurch wird die Lesbarkeit und Verständlichkeit eines Programmes erhöht.
Unser vorheriges kulinarisches Dictionary könnten wir in einem Schritt schreiben:
country_specialities_dict = dict(zip(["pizza", "sauerkraut", "paella", "hamburger"], ["Italy", "Germany", "Spain", "USA"," Switzerland"]))
print(country_specialities_dict)
Andererseits sind die Zwischenschritte im folgenden Code möglicherweise des Guten zu viel:
dishes = ["pizza", "sauerkraut", "paella", "hamburger"]
countries = ["Italy", "Germany", "Spain", "USA"]
country_specialities_zip = zip(dishes,countries)
country_specialities_list = list(country_specialities_zip)
country_specialities_dict = dict(country_specialities_list)
print(country_specialities_dict)
Wir erhalten das Gleiche Ergebnis, wie im Einzeiler.
Lauernde Gefahr
Insbesondere für diejenigen, die von Python 2.x auf Python 3.x wechseln lauert im obigen Vorgehen eine große Gefahr: zip() lieferte in Python2 eine Liste zurück. In Python 3.x hingegen liefert zip nun einen Iterator zurück. Man sollte sich immer vergegenwärtigen, dass sich viele Iteratoren bei der Benutzung "verbrauchen", das bedeutet, dass man sie nur einmal durchlaufen kann.
Wir demonstrieren dies im folgenden kleinen Beispiel:
l1 = ["a","b","c"]
l2 = [1,2,3]
c = zip(l1, l2)
for i in c:
print(i)
for i in c:
print(i)
Diesen Effekt kann man auch beobachten, wenn man list() benutzt:
l1 = ["a","b","c"]
l2 = [1,2,3]
c = zip(l1,l2)
z1 = list(c)
z2 = list(c)
print(z1)
print(z2)
Als Übung können Sie überlegen, was im folgenden Skript faul ist:
dishes = ["pizza", "sauerkraut", "paella", "hamburger"]
countries = ["Italy", "Germany", "Spain", "USA"]
country_specialities_zip = zip(dishes,countries)
print(list(country_specialities_zip))
country_specialities_list = list(country_specialities_zip)
country_specialities_dict = dict(country_specialities_list)
print(country_specialities_dict)
Wenn man das Skript startet, sieht man, dass das Dictionary, was wir erzeugen wollen, leer ist.