Den Begriff Tupel sollten Sie aus der Mathematik kennen. Dort bezeichnet er eine (endlich lange) Liste von nicht notwendigerweise voneinander verschiedenen Objekten. Da wir bereits Listen in Python haben, benötigen wir keine Tupel, richtig? Falsch! Denn im Gegensatz zu Listen können bei einem Tupel die einzelnen Werte zur Laufzeit des Programms nicht verändert werden. Und diese Eigenschaft macht tatsächlich einen großen Unterschied.
\index{Tupel} \index{Datentyp!Tupel} \index{Folge}
Ein Tupel ist eine Folge von Werten. Die in einem Tupel gespeicherten Werte können von beliebigem Typ sein und sie werden durch Ganzzahlen indiziert. Alles genau wie bei Listen, nur eben mit dem wichtigen Unterschied, dass Tupel unveränderlich sind. Tupel sind außerdem vergleichbar und können gehasht werden, sodass wir Listen von ihnen sortieren und Tupel als Schlüsselwerte in Python-Dictionarys verwenden können.
\index{Veränderbarkeit} \index{vergleichbar} \index{Unveränderlichkeit}
Syntaktisch ist ein Tupel eine durch Kommata getrennte Liste von Werten:
>>> t = 'a', 'b', 'c', 'd', 'e'
Obwohl es nicht notwendig ist, ist es üblich, Tupel in Klammern einzuschließen, damit wir Tupel schnell identifizieren können, wenn wir uns Python-Code ansehen:
\index{Klammern!Tupeln innerhalb}
>>> t = ('a', 'b', 'c', 'd', 'e')
Um ein Tupel mit einem einzelnen Element zu erstellen, müssen wir das abschließende Komma einfügen:
\index{Singleton} \index{Tupel!Singleton}
>>> t1 = (1,)
>>> type(t1)
<type 'tuple'>
Ohne das Komma würde die (1)
für den Python-Interpreter wie eine geklammerte Ganzzahl aussehen und damit wie ein ganz normaler Ausdruck. Erst das Komma vor der Klammer macht deutlich, dass hier etwas anderes, nämlich ein Eintupel gemeint ist.
Eine weitere Möglichkeit, ein Tupel zu konstruieren, ist die eingebaute Funktion tuple
. Ohne Argument erzeugt sie ein leeres Tupel:
\index{tuple (Funktion)} \index{Funktion!tuple}
>>> t = tuple()
>>> print(t)
()
Wenn das Argument eine Sequenz (String, Liste oder Tupel) ist, ist das Ergebnis des Aufrufs von tuple
ein Tupel mit den Elementen der Sequenz:
>>> t = tuple('lupins')
>>> print(t)
('l', 'u', 'p', 'i', 'n', 's')
Da tuple
der Name eines Konstruktors ist, sollten wir es vermeiden, ihn als Variablennamen zu verwenden.
Die meisten Listenoperatoren funktionieren auch auf Tupeln. Der Indexoperator indiziert ein Element:
\index{Indexoperator}\index{Zugriff, indexbasiert} \index{Operator!indexbasierter Zugriff}
>>> t = ('a', 'b', 'c', 'd', 'e')
>>> print(t[0])
'a'
Und der Slice-Operator selektiert einen Bereich von Elementen.
\index{slice-Operator} \index{Operator!slice} \index{Tupel!slice} \index{slice!Tupel}
>>> print(t[1:3])
('b', 'c')
Wenn wir jedoch versuchen, eines der Elemente des Tupels zu ändern, erhalten wir einen Fehler:
\index{Ausnahme!TypeError} \index{TypeError} \index{Elementzuweisung} \index{Zuweisung!Element}
>>> t[0] = 'A'
TypeError: object doesn't support item assignment
Wir können die Elemente eines Tupels nicht ändern, aber wir können ein Tupel durch ein anderes ersetzen:
>>> t = ('A',) + t[1:]
>>> print(t)
('A', 'b', 'c', 'd', 'e')
\index{Vergleich!Tupel} \index{Tupel!Vergleich} \index{sort (Methode)} \index{Methode!sort}
Die Vergleichsoperatoren funktionieren mit Tupeln und anderen Sequenzen. Python beginnt mit dem Vergleich des jeweils ersten Elements aus jeder Sequenz. Wenn beide gleich sind, geht es weiter zum nächsten Element und so weiter, bis zwei Elemente gefunden wurden, die sich unterscheiden. Diese werden entsprechend dem verwendeten Operator verglichen. Damit endet der Vergleich der Tupel. Alle danach kommenden Elemente werden also nicht mehr berücksichtigt.
>>> (0, 1, 2) < (0, 3, 4)
True
>>> (0, 1, 2000000) < (0, 3, 4)
True
Die Funktion sort
arbeitet auf die gleiche Weise. Sie sortiert primär nach dem ersten Element, aber im Falle eines Gleichstandes nach dem zweiten Element und so weiter.
Diese Funktion eignet sich für ein Muster namens DSU:
Decorate : „Dekorieren“ einer Sequenz, indem eine Liste von Tupeln mit einem oder mehreren Sortierschlüsseln den Elementen der Sequenz vorangestellt werden.
Sort
: Sortieren der Liste der Tupel mit dem in Python eingebauten sort
.
Undecorate : Extrahieren der sortierten Elemente der Sequenz.
\index{DSU-Muster} \index{Muster!DSU} \index{Muster!Decorate-Sort-Undecorate} \index{Romeo and Juliet}
Angenommen, wir haben eine Liste von Wörtern und möchten diese nach den Wortlängen absteigend sortieren:
\VerbatimInput{../code3/soft.py}
Die erste Schleife baut eine Liste von Tupeln auf, wobei jedes Tupel ein Wort ist, dem seine Länge vorangestellt ist.
sort
vergleicht das erste Element (die Länge) zuerst und berücksichtigt nur das zweite Element, um Gleichstände aufzulösen. Das Schlüsselwortargument reverse=True
sagt sort
, dass es in absteigender Reihenfolge vorgehen soll.
\index{Schlüsselwortargument} \index{Argument!Schlüsselwort} \index{Traversieren}
Die zweite Schleife durchläuft die Liste aller Tupel und baut eine Liste der Wörter nach absteigender Wortlänge auf. Gleichlange Wörter werden in umgekehrter alphabetischer Reihenfolge sortiert, sodass „war“ in der folgenden Liste vor „und“ erscheint.
Die Ausgabe des Programms ist wie folgt:
['Nachtigall', 'Lerche', 'nicht', 'war', 'und', 'die', 'die', 'Es']
Natürlich verliert die Zeile viel von ihrer poetischen Wirkung, wenn man sie in eine Python-Liste verwandelt und in absteigender Reihenfolge der Wortlänge sortiert.
\index{Tupel!Zuweisung} \index{Zuweisung!Tupel} \index{Swap (Muster)} \index{Muster!Swap}
Eine der einzigartigen syntaktischen Eigenschaften der Sprache Python ist die Fähigkeit, ein Tupel auf der linken Seite einer Zuweisung haben zu können. Dadurch können wir mehr als eine Variable auf einmal zuweisen, wenn die linke Seite eine Sequenz ist.
In diesem Beispiel haben wir eine zweielementige Liste (die eine Sequenz ist) und weisen das erste und zweite Element der Sequenz den Variablen x
und y
in einer einzigen Anweisung zu.
>>> m = [ 'have', 'fun' ]
>>> x, y = m
>>> x
'have'
>>> y
'fun'
>>>
Es ist keine Magie, Python übersetzt die Tupel-Zuweisungssyntax in etwa so, dass sie wie folgt aussieht:^[Python übersetzt die Syntax nicht wörtlich. Wenn wir dies zum Beispiel mit einem Dictionary versuchen, wird es nicht so funktionieren, wie wir es vielleicht erwarten.]
>>> m = [ 'have', 'fun' ]
>>> x = m[0]
>>> y = m[1]
>>> x
'have'
>>> y
'fun'
>>>
Stilistisch gesehen lassen wir die Klammern weg, wenn wir ein Tupel auf der linken Seite der Zuweisungsanweisung verwenden, aber das Folgende ist eine ebenso gültige Schreibweise:
>>> m = [ 'have', 'fun' ]
>>> (x, y) = m
>>> x
'have'
>>> y
'fun'
>>>
Eine besonders clevere Anwendung der Tupel-Zuweisung erlaubt es uns, die Werte zweier Variablen in einer einzigen Anweisung zu tauschen:
>>> a, b = b, a
Beide Seiten dieser Anweisung sind Tupel, aber die linke Seite ist ein Tupel von Variablen; die rechte Seite ist ein Tupel von Ausdrücken. Jeder Wert auf der rechten Seite wird der entsprechenden Variablen auf der linken Seite zugewiesen. Alle Ausdrücke auf der rechten Seite werden vor ihrer Zuweisungen ausgewertet.
Die Anzahl der Variablen auf der linken Seite und die Anzahl der Werte auf der rechten Seite müssen zueinander passen:
\index{Ausnahme!ValueError} \index{ValueError}
>>> a, b = 1, 2, 3
ValueError: too many values to unpack
Wir sollten noch ein wenig näher darauf eingehen, was „zueinander passen“ hier bedeutet. a
und b
sind in der obigen Zuweisung einfach nur Namen. Python, als dynamisch typisierte Programmiersprache, wird zur Laufzeit entscheiden, welche Werte a
und b
bekommen sollen. Nun könnte einer der beiden Namen für eine Liste stehen, in der mehrere Werte untergebracht sind. Dann würde die Zuweisung oben wieder „passen“. Nur kann Python hier nicht wissen, ob a
oder b
diese Liste sein soll.
Um das Beispiel oben doch funktionstüchtig zu machen, müssen wir die Information hinzufügen, welcher Name für eine Sequenz von Werten stehen soll. Das tun wir mit einem vorangestellten Sternchen *
(englisch Asterisk):
>>> a, *b = (1,2,3)
>>> b
[2, 3]
Es gibt noch weitere Stellen in Python, an denen man mit einem vorangestellten Asterisk ausdrückt, dass mehrere einzelne Werte zusammengefasst oder aber eine Sequenz zu einzelnen Werten entpackt wird. Dieses Konzept nennt man dementsprechend Packing bzw. Unpacking. Funktionen verwenden es z. B., um eine beliebig lange Liste von Parametern zuzulassen.
Beachten Sie übrigens, dass im Beispiel oben das Resultat in b
eine Liste ist, obwohl auf der linken Seite der Zuweisung ein Tupel steht. Beim Packing werden die einzelenen Elemente immer in eine Liste gepackt, ganz gleich aus welcher Datenstruktur sie entnommen werden.
\index{Packing} \index{Unpacking}
(Un)packing lässt sich nicht nur auf numerische Werte anwenden, sondern auch auf andere Typen wie etwa Zeichenketten. Um z. B. eine E-Mail-Adresse in einen Benutzernamen und eine Domäne aufzuteilen, könnten wir schreiben:
\index{split (Methode)} \index{Methode!split} \index{E-Mail-Adresse}
>>> addr = 'monty@python.org'
>>> uname, domain = addr.split('@')
Der Rückgabewert von split
ist eine Liste mit zwei Elementen; das erste Element wird uname
zugewiesen, das zweite domain
.
>>> print(uname)
monty
>>> print(domain)
python.org
\index{Dictionary} \index{items (Methode)} \index{Methode!items} \index{Key-Value-Paar}\index{Schlüssel-Wert-Paar}
Dictionarys haben eine Methode namens items
, die eine Liste von Tupeln zurückgibt, wobei jedes Tupel ein Schlüssel-Wert-Paar ist:
>>> d = {'b':1, 'a':13, 'c':7}
>>> t = list(d.items())
>>> print(t)
[('b', 1), ('a', 13), ('c', 7)]
Wie wir es vermutlich von einem Dictionary erwarten, sind die Begriffe in keiner bestimmten Reihenfolge angeordnet.
Da die Liste der Tupel jedoch eine Liste ist und Tupel vergleichbar sind, können wir nun die Liste der Tupel sortieren. Das Konvertieren eines Dictionarys in eine Liste von Tupeln ist eine Möglichkeit, den Inhalt eines Dictionarys nach Schlüssel sortiert auszugeben:
>>> d = {'b':1, 'a':13, 'c':7}
>>> t = list(d.items())
>>> t
[('b', 1), ('a', 13), ('c', 7)]
>>> t.sort()
>>> t
[('a', 13), ('b', 1), ('c', 7)]
Die neue Liste wird in aufsteigender alphabetischer Reihenfolge nach dem Schlüsselwert sortiert.
\index{Traversieren!Dictionary} \index{Dictionary!Traversieren}
Durch die Kombination von items
, Tupel-Zuweisung und for
können wir ein schönes Codeschema für das Durchlaufen der Schlüssel und Werte eines Dictionarys in einer einzigen Schleife sehen:
d = {'b':1, 'a':13, 'c':7}
for key, val in list(d.items()):
print(val, key)
Diese Schleife hat zwei Iterationsvariablen, weil items
eine Liste von Tupeln zurückgibt und key, val
eine Tupel- bzw. Unpacking-Zuweisung ist, die nacheinander durch jedes der Schlüssel-Wert-Paare im Dictionary iteriert.
Bei jeder Iteration durch die Schleife werden sowohl key
als auch val
zum nächsten Schlüssel-Wert-Paar im Dictionary vorgerückt (immer noch in Hash-Reihenfolge).
Der Ausgang dieser Schleife ist:
1 b
13 a
7 c
Auch hier gilt wieder die Reihenfolge der Hash-Schlüssel (d. h. letztendlich keine bestimmte Reihenfolge).
Wenn wir diese beiden Techniken kombinieren, können wir den Inhalt eines Dictionarys sortiert nach dem Wert, der in jedem Schlüssel-Wert-Paar gespeichert ist, ausgeben.
Um dies zu tun, erstellen wir zunächst eine Liste von Tupeln, wobei jedes Tupel (value, key)
ist. Die Methode items
würde uns eine Liste von (key, value)
-Tupeln liefern, aber dieses Mal wollen wir nach Wert und nicht nach Schlüssel sortieren. Sobald wir die Liste mit den Wert-Schlüssel-Tupeln aufgebaut haben, ist es eine einfache Sache, die Liste in umgekehrter Reihenfolge zu sortieren und die neue, sortierte Liste auszugeben.
>>> d = {'b':1, 'a':13, 'c':7}
>>> l = list()
>>> for key, val in d.items():
... l.append( (val, key) )
...
>>> l
[(1, 'b'), (13, 'a'), (7, 'c')]
>>> l.sort(reverse=True)
>>> l
[(13, 'a'), (7, 'c'), (1, 'b')]
>>>
Indem wir die Liste der Tupel sorgfältig so konstruieren, dass der Wert das erste Element jedes Tupels ist, können wir die Liste der Tupel sortieren und erhalten den Inhalt unseres Dictionarys nach dem Wert sortiert.
Wenn wir zu unserem laufenden Beispiel des Textes aus Romeo und Juliet Akt 2, Szene 2 zurückkehren, können wir unser Programm erweitern, um diese Technik zu verwenden und die zehn häufigsten Wörter im Text wie folgt auszugeben:
\VerbatimInput{../code3/count3.py} \begin{trinketfiles} ../code3/romeo-full.txt \end{trinketfiles}
Der erste Teil des Programms, der die Datei liest und das Dictionary erstellt, welches jedes Wort auf die Anzahl der Wörter im Dokument abbildet, bleibt unverändert. Aber anstatt einfach counts
auszugeben und das Programm zu beenden, konstruieren wir eine Liste von (val, key)
-Tupeln und sortieren die Liste dann in umgekehrter Reihenfolge.
Da der Wert an erster Stelle steht, wird er für die Vergleiche verwendet. Wenn es mehr als ein Tupel mit demselben Wert gibt, wird das zweite Element (der Schlüssel) betrachtet, sodass Tupel, bei denen der Wert gleich ist, weiter nach der alphabetischen Reihenfolge des Schlüssels sortiert werden.
Am Ende schreiben wir eine for
-Schleife, die in jeder Iteration eine Mehrfachzuweisung durchführt und die zehn häufigsten Wörter ausgibt, indem sie durch einen Ausschnitt der Liste (lst[:10]
) iteriert.
Jetzt sieht die Ausgabe endlich so aus, wie wir es uns für unsere Worthäufigkeitsanalyse wünschen.
61 i
42 and
40 romeo
34 to
34 the
32 thou
32 juliet
30 that
29 my
24 thee
Die Tatsache, dass dieses komplexe Parsen und Analysieren von Daten mit einem leicht verständlichen 19-zeiligen Python-Programm durchgeführt werden kann, ist ein Grund, warum Python eine gute Wahl als Sprache für die Erforschung von Informationen ist.
\index{Tupel!als Schlüssel in Dictionarys} \index{hashbar}
Da Tupel gehasht werden können und Listen nicht, müssen wir ein Tupel als Schlüssel verwenden, wenn wir einen zusammengesetzten Schlüssel zur Verwendung in einem Dictionary erstellen wollen.
Wir würden auf einen zusammengesetzten Schlüssel stoßen, wenn wir ein Telefonverzeichnis erstellen wollten, das von Nachnamen-Vornamen-Paaren auf Telefonnummern abbildet. Unter der Annahme, dass wir die Variablen last
, first
und number
definiert haben, könnten wir eine Dictionaryzuweisung wie folgt schreiben:
directory[last,first] = number
Der Ausdruck in Klammern ist ein Tupel. Wir könnten die Tupel-Zuweisung in einer for
-Schleife verwenden, um dieses Dictionary zu durchlaufen.
\index{Tupel!geklammert}
for last, first in directory:
print(first, last, directory[last,first])
Diese Schleife durchläuft die Schlüssel in directory
, bei denen es sich um Tupel handelt. Sie ordnet die Elemente jedes Tupels last
und first
zu und gibt dann den Namen und die entsprechende Telefonnummer aus.
\index{Folge}
Ich habe mich hierbei auf Listen von Tupeln konzentriert, aber fast alle Beispiele in diesem Kapitel arbeiten auch mit Listen von Listen, Tupeln von Tupeln und Tupeln von Listen. Um die Aufzählung der möglichen Kombinationen zu vermeiden, ist es manchmal einfacher, von „Sequenzen von Sequenzen“ zu sprechen.
In vielen Kontexten können die verschiedenen Arten von Sequenzen (Strings, Listen und Tupel) austauschbar verwendet werden. Wie und warum wählen wir also eine der beiden Arten aus?
\index{Zeichenkette} \index{Liste} \index{Tupel} \index{Veränderbarkeit} \index{Unveränderlichkeit}
Um mit dem Offensichtlichen zu beginnen: Zeichenketten sind begrenzter als andere Sequenzen, da die Elemente Zeichen sein müssen. Außerdem sind sie unveränderlich. Wenn wir die Möglichkeit benötigen, die Zeichen in einer Zeichenfolge zu ändern (im Gegensatz zur Erstellung einer neuen Zeichenfolge), sollten wir stattdessen eine Liste von Zeichen verwenden.
Listen sind üblicher als Tupel, hauptsächlich weil sie veränderbar sind. Aber es gibt ein paar Fälle, in denen wir Tupel bevorzugen könnten:
-
In manchen Kontexten, wie z. B. einer
return
-Anweisung, ist es syntaktisch einfacher, ein Tupel zu erstellen als eine Liste. In anderen Kontexten bevorzugen wir vielleicht eine Liste. -
Wenn wir eine Sequenz als Dictionaryschlüssel verwenden möchten, müssen wir einen unveränderlichen Typ wie ein Tupel oder einen String verwenden.
-
Wenn wir eine Sequenz als Argument an eine Funktion übergeben, verringert die Verwendung von Tupeln das Potenzial für unerwartetes Verhalten aufgrund von Aliasbildung.
Da Tupel unveränderlich sind, bieten sie keine Methoden wie sort
und reverse
, die bestehende Listen verändern. Python bietet jedoch die eingebauten Funktionen sorted
und reversed
, die eine beliebige Sequenz als Parameter nehmen und eine neue Sequenz mit denselben Elementen in einer anderen Reihenfolge zurückgeben.
\index{sorted (Funktion)} \index{Funktion!sorted} \index{reversed (Funktion)} \index{Funktion!reversed}
\index{Debugging} \index{Datenstruktur}
Listen, Dictionarys und Tupel sind allgemein als Datenstrukturen bekannt; in diesem Kapitel beginnen wir, zusammengesetzte Datenstrukturen zu nutzen, wie Listen von Tupeln und Dictionarys, die Tupel als Schlüssel und Listen als Werte enthalten. Zusammengesetzte Datenstrukturen sind nützlich, aber sie sind anfällig für das, was man als Formatfehler bezeichnen könnte; das heißt, Fehler, die entstehen, wenn eine Datenstruktur den falschen Typ, die falsche Größe oder die falsche Zusammensetzung hat, oder wenn wir vielleicht etwas Code schreiben und das Format der Daten vergessen und einen Fehler einführen. Wenn wir zum Beispiel eine Liste mit einer Ganzzahl erwarten und ich dem Programm eine einfache Ganzzahl (nicht in einer Liste) gebe, wird es nicht funktionieren.
vergleichbar : Ein Datentyp, bei dem ein Wert daraufhin überprüft werden kann, ob er größer, kleiner oder gleich einem anderen Wert desselben Typs ist. Typen, die vergleichbar sind, können in eine Liste eingefügt und sortiert werden. \index{vergleichbar}
Datenstruktur : Eine Sammlung zusammengehöriger Werte, oft organisiert in Listen, Dictionarys, Tupeln usw. \index{Datenstruktur}
DSU : Abkürzung für „Decorate-Sort-Undecorate“, ein Muster, bei dem eine Liste von Tupeln erstellt, sortiert und ein Teil des Ergebnisses extrahiert wird. \index{DSU-Muster}
hashbar : Ein Datentyp, der eine Hash-Funktion hat. Unveränderliche Typen wie Ganzzahlen, Fließkommazahlen und Zeichenketten sind hashfähig; veränderliche Typen wie Listen und Dictionarys können nicht gehasht werden. \index{hashbar}
Singleton : Eine Liste (oder andere Sequenz) mit einem einzelnen Element. \index{Singleton}
Tupel : Eine unveränderliche Folge von Elementen. \index{Tupel}
Tupelzuweisung : Eine Zuweisung mit einer Sequenz auf der rechten Seite und einem Tupel von Variablen auf der linken Seite. Die rechte Seite wird ausgewertet und dann werden ihre Elemente den Variablen auf der linken Seite zugewiesen. \index{Tupelzuweisung} \index{Zuweisung!Tupel}
Übung 1: Das vorherige Programm soll auf folgende Weise überarbeitet werden: Die From
-Zeilen sollen gelesen und geparst werden, um daraus die Adressen zu extrahieren. Dabei soll mit Hilfe eines Dictionarys die Anzahl der Nachrichten von jeder Person gezählt werden.
Nachdem alle Daten gelesen wurden, soll die Person mit den meisten Nachrichten ausgegeben werden, indem eine Liste von (count, email)
-Tupel aus dem Dictionary erstellt wird. Dann muss die Liste in umgekehrter Reihenfolge sortiert und schließlich die Person mit den meisten Nachrichten ausgegeben werden.
Gib eine Datei an: mbox-short.txt
cwen@iupui.edu 5
Gib eine Datei an: mbox.txt
zqian@umich.edu 195
Übung 2: Schreiben Sie ein Programm, welches die Häufigkeit von Nachrichten pro Tageszeit (ganze Stunden) ermittelt. Die Stunden sollen aus der From
-Zeile extrahiert werden, indem die Uhrzeit gefunden und diese Zeichenfolge dann anhand des Doppelpunkts in Teile zergliedert wird. Sobald die Nachrichtenhäufigkeit für jede Stunde gesammelt wurde, sollen diese wie weiter unten gezeigt ausgegeben werden (eine pro Zeile, sortiert nach Stunde).
python timeofday.py
Gib eine Datei an: mbox-short.txt
04 3
06 1
07 1
09 2
10 3
11 6
14 1
15 2
16 4
17 2
18 1
19 1
Übung 3: Schreiben Sie ein Programm, das eine Datei liest und die Buchstaben in absteigender Reihenfolge der Häufigkeit ausgibt. Das Programm sollte alle Eingaben in Kleinbuchstaben umwandeln und nur die Buchstaben a-z zählen. Es soll keine Leerzeichen, Ziffern, Interpunktionszeichen oder irgendetwas anderes als die Buchstaben a-z zählen. Dann sollen Textbeispiele in verschiedenen Sprachen als Eingabe dienen, um zu analysieren, wie die Buchstabenhäufigkeit zwischen verschiedenen Sprachen variiert. Vergleichen Sie ihre Ergebnisse mit den Tabellen auf https://wikipedia.org/wiki/Letter_frequencies
\index{Buchstabenhäufigkeit} \index{Häufigkeit!Buchstaben}