Level 05: Tupel

Level 5: Bitte zwei davon – Tupel #

Nun hast Du schon einiges über Listen gelernt. Listen in Python lassen sich leicht anlegen, mit der kurzen Schreibweise meine_liste = [], die auch gleich Elemente enthalten kann: meine_liste = [1, 2, 3]. Daneben lässt sich eine Liste auch explizit als Liste anlegen: meine_liste = list(). Allen Listen kann ich mit meine_liste.append(4) Elemente hinzufügen (hier die Zahl vier), oder über die Indexschreibweise auf einzelne Elemente zugreifen: meine_liste[0] würde hier 1 zurückgeben.

Die Liste ist eine so genannte Datenstruktur. Datenstrukturen speichern Daten für uns, und zwar auf eine bestimmte Weise. Du hast bereits eine andere Datenstruktur kennengelernt, die wie die Liste in Python eingebaut ist: die Menge (als set()). In einer Menge kann ein Element nur ein einziges Mal vorkommen, in einer Liste könnten wir gleiche Elemente auch sehr oft abspeichern…

Mehr als nur eine Sache liefern… #

Neben Listen und Mengen kennt Python noch eine weitere Datenstruktur: das Tupel. Zwar kann ein Tupel weniger als eine Liste, doch ist es trotzdem manchmal sehr nützlich. Tippe einmal folgendes Programm ab, überlege, was geschieht, und lass’ es laufen:

def straße_und_hausnummer(eingabe):
    straße, nummer = eingabe.rsplit(maxsplit=1)
    return (straße, nummer)

deine_eingabe = input('Wo wohnst Du? (Straße / Hausnummer) ')
deine_straße, deine_nummer = straße_und_hausnummer(deine_eingabe)
print('Du wohnst also in der', deine_straße, 'und dort im Haus Nr.', deine_nummer)

Wenn Du eine Adresse richtig eingibst, sollte in etwa so etwas dabei herauskommen:

Wo wohnst Du? (Straße / Hausnummer) Gavagaistraße 42
Du wohnst also in der Gavagaistraße und dort im Haus Nr. 42

Was ist passiert? #

Tupel können viele Werte übergeben, ohne dass ihnen jemand dazwischen kommen kann #

Den magischen Einzeiler der Funktion straße_und_hausnummer überspringen wir fürs erste – den sehen wir uns später an. Doch was gibt denn die Funktion zurück? Hier kommt nun das Tupel ins Spiel! Probiere selbst in der REPL aus, wie es sich zusammenbasteln und nutzen lässt:

>>> q1 = (2, 4)
>>> q1
(2, 4)
>>> type(q1)
<class 'tuple'>
>>> q1[0]
2
>>> for i in q1:
        print(i)
 
2
4

Ein Tupel kannst du also recht einfach erzeugen, indem du irgendwelche Werte (oder Variablen) – durch Komma getrennt – in Klammern schreibst. (Die Klammern könntest du sogar weglassen – probier es einmal aus!)

Außerdem kannst Du mit der schon von den Listen bekannten Schreibweise q1[0] auf einzelne Indizes zugreifen, und auch die for-Wiederholung kann etwas mit unserem Tupel anfangen. Sehr schön! Geht da noch mehr?

>>> q1.append(6)
Traceback (most recent call last):
  File "/usr/lib/python3.9/idlelib/run.py", line 559, in runcode
    exec(code, self.locals)
  File "<pyshell#7>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'append'
>>> 

Was ist das? Beim Versuch, dem Tupel ein neues Element hinzuzufügen, wird eine Fehlermeldung ausgegeben – das geht offensichtlich nicht!

Mutabel oder nicht?

Hinweis zur Einordnung

Hier kommt der wesentliche Unterschied zu Listen in Python. Listen kannst Du verändern – sie sind mutabel. Dahingegen sind Tupel unveränderlich, das nennt man immutabel.

Wenn Listen nun alles können, was Tupel auch können – warum nimmt man dann nicht immer Listen?

Dafür gibt es mehrere Gründe. Einer ist, dass Tupel – weil sie weniger können – weniger Aufwand beim Erstellen und Speichern benötigen. Sie lassen sich daher oft schneller verarbeiten. Außerdem kann es gewünscht sein, dass ein Wert unveränderlich ist! (Dann kannst du dir sicher sein, dass auch (fast) immer die Werte herauskommen, die du reingesteckt hast…)

Darf ich vorstellen? split(), eine Zeichenkettenmethode, die spaltet! #

Jetzt fehlt nur noch der magische Einzeiler…

Um genau zu verstehen, was in der Funktion straße_und_hausnummer geschieht, solltest du dich mit der split()-Methode vertraut machen, die sich an Zeichenketten aufrufen lässt. Findest Du heraus, was jeweils passiert?

>>> zeichenkette = 'Hätte, hätte, Fahradkette'
>>> zeichenkette.split()
['Hätte,', 'hätte,', 'Fahradkette']
>>> zeichenkette.split(',')
['Hätte', ' hätte', ' Fahradkette']
>>> zeichenkette.split(maxsplit = 1)
['Hätte,', 'hätte, Fahradkette']

Die split()-Methode zerteilt eine Zeichenkette ohne weitere Argumente an Leerstellen (sogenannter Whitespace, also Leerzeichen, Tabulatoren oder Zeilenumbrüchen). Du kannst allerdings selbst eine Zeichenkette übergeben, an der dann geteilt wird, und zusätzlich ein Argument, das bestimmt, wie oft höchstens geteilt werden darf. Zurückgegeben wird immer eine Liste, in der die einzelnen Teile der ursprünglichen Zeichenkette enthalten sind.

Die Methode rsplit() funktioniert ganz genau so, mit dem einzigen Unterschied, dass die Zeichenkette von hinten nach den Stellen durchsucht wird, an denen geteilt werden soll.

Lass dir helfen!

Hinweis zur Einordnung

Wenn du’s ganz genau wissen möchtest: tippe doch einmal help(str.split) in die REPL, und suche so nach der split-Methode.

Help on method_descriptor:

split(self, /, sep=None, maxsplit=-1)
    Return a list of the substrings in the string, using sep as the separator string.
    
      sep
        The separator used to split the string.
    
        When set to None (the default value), will split on any whitespace
        character (including \n \r \t \f and spaces) and will discard
        empty strings from the result.
      maxsplit
        Maximum number of splits (starting from the left).
        -1 (the default value) means no limit.
    
    Note, str.split() is mainly useful for data that has been intentionally
    delimited.  With natural text that includes punctuation, consider using
    the regular expression module.

Gut, oder? Das ist übrigens die in Python eingebaute Hilfe! Oft ist sie hilfreich, wenn du eine Sache, die du schon weißt, noch einmal (genau) nachlesen möchtest. Wenn du allerdings ein wenig Zeit hast, findest du dort auch Lesestoff für Jahre…

Kurz und klar! #

Jetzt sollte auch klar sein, was mit deiner Eingabe passiert, wenn an ihr rsplit(maxsplit=1) aufgerufen wird: sie wird zerteilt, und zwar am letzten Leerzeichen – das trennt bei einer Adresse üblicherweise den Straßennamen von der Hausnummer!

Die Zeile

straße, nummer = eingabe.rsplit(maxsplit=1)

weist nun das erste Element (Index 0) dieser Liste der Variable straße zu, und das zweite (Index 1) der Variable nummer. Diesen Vorgang nennt man auch unpacking, und es funktioniert mit Tupeln wie Listen. Das ist sehr praktisch, denn was wäre die Alternative? Statt einer Zeile bräuchten wir vielleicht drei, etwa so:

adresse_als_liste = eingabe.rsplit(maxsplit=1)
straße = adresse_als_liste[0]
nummer = adresse_als_liste[1]

Und da ist der Einzeiler doch wirklich verständlicher, oder?

Mutabel oder immutabel? Jetzt wird es doch noch verwirrend… #

Spiele einmal folgendes Beispiel in der REPL durch:

>>> a = 5
>>> b = 7
>>> tup = a, b
>>> tup
(5, 7)
>>> a = 3
>>> a
3
>>> tup
(5, 7)
>>> c = [3]
>>> d = [4]
>>> tup2 = c, d
>>> tup2
([3], [4])
>>> c[0] = 100
>>> 

Welche Ausgabe erwartest du nun, wenn du tup2 anzeigen lässt?

>>> tup2
([100], [4])
>>> c
[100]

Hättest du das gedacht? Obwohl doch Tupel eigentlich immutabel sind (d.h.: sich nicht verändern können), hat sich hier wohl doch etwas verändert… doch warum das?

Wenn du einfache Werte wie z.B. Zahlen oder Zeichenketten in ein Tupel steckst, werden die tatsächlichen Werte übergeben – diese lassen sich dann nicht mehr verändern. Wenn du allerdings kompliziertere Datenstrukturen (zu denen z.B. eine Liste gehört) in ein Tupel hineinpackst, wird nicht die ganze Liste dort abgespeichert, sondern nur so etwas wie ein Hinweis auf die Liste (Programmierer sprechen von einer Referenz auf die Liste). Dieser Hinweis (das heißt: die Referenz) kann sich nicht mehr ändern, weil das Tupel ja immutabel ist. Die darunter liegende Liste ist allerdings mutabel und kann noch verändert werden! Das ist so ähnlich wie in einer Straße: die Häuser können die selben bleiben, doch die Bewohner der Häuser können aus- und einziehen… Das Tupel behält also immer den selben Hinweis auf die selbe Liste, doch die Liste kann sich nach wie vor ändern.

Noch ausführlichere Informationen findest du im Rheinwerk-OpenBook dazu.

Übung: #

Erinnerst du dich noch an Henriks Flaggenquiz? Der Code dazu sah ja so aus:

def hole_antwort() -> str:
    antwort = input('Gib ein: a, b, c oder d: ')
    while not antwort in ['a', 'b', 'c', 'd']:
        antwort = input('Nicht verstanden – a, b, c oder d? ')
    return antwort

def trenner_ausgeben():
    print('-------------------------------')
    print('--Und nun zur nächsten Frage!--')
    print('-------------------------------')
    print()

def werte_aus(gegebene_antwort: str, richtige_antwort: str) -> int:
    uebereinstimmung = gegebene_antwort == richtige_antwort
    punkt = 0
    if uebereinstimmung:
        print('Richtig!')
        punkt = 1
    else:
        print('Leider falsch!')
    return punkt

if __name__ == '__main__':
    punkte = 0
    print('Willkommen zum Flaggenquiz!')
    print('Wo findest du die älteste, heute noch in Gebrauch befindliche Trikolore? ')
    print('a) Deutschland b) Frankreich c) Italien d) Niederlande')
    punkte += werte_aus(hole_antwort(), 'd')
    trenner_ausgeben()
    print('Off-Topic-Frage: Welcher Programmiersprachenentwickler kommt')
    print('ebenfalls aus den Niederlanden?')
    print('a) Dennis Ritchie b) Guido van Rossum c) Graydon Hoare d) Bjarne Stroustrup')
    punkte += werte_aus(hole_antwort(), 'b')
    trenner_ausgeben()
    print('Die Flagge welchen Landes enthält gleichzeitig die Umrisse des Landes?')
    print('a) Frankreich b) Niederlande c) Zypern d) Italien')
    punkte += werte_aus(hole_antwort(), 'c')
    print('Du hast ' + str(punkte) + ' Punkt(e) erspielt.')

Henrik: Mein Laptop ist in den Fluss gefallen, obwohl ich doch gerade diese super Funktion programmiert hatte… Mein Programm war so viel übersichtlicher! Ich weiß noch, dass ich die Fragen zusammen mit den Antworten in eine Liste geschrieben hatte, ich glaube so:

fragen = [ ('Wo findest du die älteste, heute noch in Gebrauch befindliche Trikolore? '
            'a) Deutschland b) Frankreich c) Italien d) Niederlande', 'd'),
           ('Off-Topic-Frage: Welcher Programmiersprachenentwickler kommt '
            'ebenfalls aus den Niederlanden? '
            'a) Dennis Ritchie b) Guido van Rossum c) Graydon Hoare d) Bjarne Stroustrup', 'b'),
           ('Die Flagge welchen Landes enthält gleichzeitig die Umrisse des Landes? '
            'a) Frankreich b) Niederlande c) Zypern d) Italien', 'c') ]

Bekommst du mein Programm wieder hin, so dass das Quiz wieder läuft, und meine neue Fragenliste verwendet?

Tipp 1

Nur falls du dich wunderst: lange Zeichenketten lassen sich in Python in mehrere Zeilen schreiben, indem in der nächsten Zeile die Zeichenkette mit der selben Einrückungstiefe und neuen Anführungszeichen einfach fortgesetzt wird. So lassen sich lange Zeichenketten lesbar im Code unterbringen.

Auch Listen lassen sich umbrechen – das geht nach jedem Komma.

Tipp 2

Auf diese Liste von Tupeln lässt sich mit Indizes zugreifen, z.B. so:

>>> fragen[1][0]
'Off-Topic-Frage: Welcher Programmiersprachenentwickler kommt ebenfalls aus den Niederlanden? a) Dennis Ritchie b) Guido van Rossum c) Graydon Hoare d) Bjarne Stroustrup'
Lösungsvorschlag
fragen = [ ('Wo findest du die älteste, heute noch in Gebrauch befindliche Trikolore? '
            'a) Deutschland b) Frankreich c) Italien d) Niederlande', 'd'),
           ('Off-Topic-Frage: Welcher Programmiersprachenentwickler kommt '
            'ebenfalls aus den Niederlanden? '
            'a) Dennis Ritchie b) Guido van Rossum c) Graydon Hoare d) Bjarne Stroustrup', 'b'),
           ('Die Flagge welchen Landes enthält gleichzeitig die Umrisse des Landes? '
            'a) Frankreich b) Niederlande c) Zypern d) Italien', 'c') ]

def hole_antwort() -> str:
    antwort = input('Gib ein: a, b, c oder d: ')
    while not antwort in ['a', 'b', 'c', 'd']:
        antwort = input('Nicht verstanden – a, b, c oder d? ')
    return antwort

def trenner_ausgeben():
    print('-------------------------------')
    print('--Und nun zur nächsten Frage!--')
    print('-------------------------------')
    print()

def werte_aus(gegebene_antwort: str, richtige_antwort: str) -> int:
    uebereinstimmung = gegebene_antwort == richtige_antwort
    punkt = 0
    if uebereinstimmung:
        print('Richtig!')
        punkt = 1
    else:
        print('Leider falsch!')
    return punkt

if __name__ == '__main__':
    punkte = 0
    print('Willkommen zum Flaggenquiz!')
    for frage in fragen:
        trenner_ausgeben()
        print(frage[0])
        punkte += werte_aus(hole_antwort(), frage[1])
    print('Du hast ' + str(punkte) + ' Punkt(e) erspielt.')

Zurück Weiter