Programmieren lernen mit Lua: Lektion 05a: Tabellen als Listen und Wörterbücher

Übersicht | Vorherige Lektion | Nächste Lektion




Die fünfte und letzte Lektion unserer kleinen Lua Einführung behandelt das Thema Tabellen. Dieses Thema ist besonders spannend und vielseitig. Tatsächlich wurde die Programmiersprache Lua überhaupt entwickelt, um mit tabellenartigen Daten besonders flexibel umgehen zu können. Wegen des großen Umfangs haben wir die Lektion in zwei Teile, a und b, aufgeteilt.

Tabellen sind das Schweizer Taschenmesser unter den Lua-Datentypen. Die möglichen Anwendungsfälle von Tabellen sind unüberschaubar vielfältig. So können sie verwendet werden, um etwa folgende Dinge zu speichern und in eurem Programm zu repräsentieren:

  • Der Inhalt einer Tasche
  • Alle in einer Physiksimulation umherfliegenden Partikel
  • Alle in einer Pizzeria-Simulation vorhandenen Extra-Beläge
  • Alle Funktionen, die für eine Pizzeria-Simulation benötigt werden
  • Die Eigenschaften eines Bausteins in Minetest
  • Eine Spielerin mit Eigenschaften wie name, punktestand, position etc…
  • Alle Funktionen, die das Verhalten der Spielerin steuern, zum Beispiel kaempfen() oder position_wechseln()

Tabellen als Listen oder Arrays

Starten wir mit dem einfachsten Anwendungsfall. Eine Tabelle kann sich wie eine Liste von Werten verhalten. So können wir den Inhalt einer Tasche wie folgt als Tabelle darstellen:

> tasche = {"buch", "brille", "kekse"}

tasche ist vom Typ table:

> type(tasche)
table

Der Zugriff auf die einzelnen Elemente läuft wie folgt:

> tasche[1]
buch
> tasche[2]
brille
> tasche[3]
kekse

Die Zahlen 3, 4 und 5 nennen wir in diesem Fall auch Indizes, Einzahl Index: Unter dem Index 1 ist der String "buch" gespeichert.

Wenn Du ein nicht existierendes Element abfragst, erhältst Du nil. Wie in dem folgenden Fall – unter dem Index 4 ist nichts gespeichert:

> tasche[4]
nil

Lua verhält sich hier genau so wie bei Bezeichnern, die auf nichts verweisen. Zur Erinnerung: nil steht für „nihil“, das ist lateinisch für „Nichts“. nil ist der universelle Platzhalter für Bezeichner, denen kein Wert zugewiesen ist.

Du kannst nachträglich ein Element hinzufügen:

> tasche[4] = "bleistift"

Jetzt befindet sich an der vierten Position der Liste nicht mehr nil, sondern der String "bleistift":

> tasche[4]
bleistift

Genau so kannst Du ein Element durch ein anderes ersetzen. Nach der Ausführung wurden die Kekse durch einen Apfel ersetzt:

> tasche[3] = "apfel"
Exkurs: Tabellen sind veränderlich

Die letzten Beispiele zeigen: Tabellen sind im Unterschied zu allen anderen Typen in der Programmiersprache Lua veränderlich. An dieser Stelle ist ein kleiner Einschub angesagt, der zeigt, was das bedeutet:

Wenn Du eine Tabelle t anlegst …

> t = {"rhabarber"}
> t[1]
rhabarber

… und einem Bezeichner t2 die Tabelle t zuweist …

> t2 = t

… und dann t2 veränderst …

> t2[1] = "rhododendron"

… ist auch die Tabelle t verändert:

> t[1]
rhododendron

Dieses auf den ersten Blick seltsame Verhalten ist auf den zweiten Blick sehr logisch: t und t2 sind hier Bezeichner, die auf den selben Wert verweisen. Daher werden Veränderungen auf der Tabelle t auch auf t2 wirksam und umgekehrt. Es gibt schlichtweg nur eine Tabelle.


Die Bezeichner t und t2 verweisen beide auf dieselbe Tabelle

Die Anzahl der Elemente

Zurück zu den „listenartigen“ Tabellen: Ein vorangestelltes # liefert die Anzahl der enthaltenen Elemente:

> #tasche
4

Mithilfe dieses Tricks kannst Du Elemente an die jeweils letzte Position anhängen. Wenn tasche 4 Elemente enthält, dann ist #tasche + 1 gleich 5, also die erste freie Position:

> tasche[#tasche + 1] = "block"
> tasche[#tasche + 1] = "mineralwasser"
> #tasche
6
> tasche[5]
block
> tasche[6]
mineralwasser

Es ist übrigens nicht verboten, folgendes zu machen, also den Index 1000 zu benutzen, auch wenn die Indizes von 7 bis 999 frei sind:

> liste[1000] = "portemonnaie"

Auch negativer Indices, hier -1000, sind erlaubt:

> liste[-1000] = "kühlakku"

Allerdings liefert #tasche danach immer noch 6, obwohl jetzt eigentlich 8 Sachen in der tasche sind:

> #tasche
6

Es ist sogar möglich, statt ganzer Zahlen auch Dezimalzahlen, Strings oder gar andere Tabellen als Schlüssel zu verwenden. Die zuletzt genannten „Tricks“ werden in anderen Situationen sehr viel Sinn ergeben. Nicht aber, wenn es um listenartige Tabellen geht.

Listenartige Tabellen haben folgende Eigenschaften:

  • Alle Indizes sind positive ganze Zahlen
  • Der niedrigste Index ist 1
  • Es gibt keine „Lücken“, d.h.: Wenn unter den Indizes 3 und 5 etwas gespeichert ist, dann muss auch unter 4 etwas gespeichert sein.

Übrigens wird sowohl in der englischsprachigen als auch in der deutschen Lua-Literatur gelegentlich von „array-like“ bzw. „array-artigen“ Tabellen u.ä. gesprochen. Wir haben uns hier für den deutschen Begriff „Liste“ entschieden, meinen aber dasselbe.

Den Inhalt von listenartigen Tabellen aufzählen

Zur Erinnerung: In Lektion 03 haben wir gezeigt, wie wir in einer for-Schleife einen Wertebereich durchlaufen. So läuft in der folgenden Schleife der Index von 1 bis 10 und gibt entsprechend die Zahlen von 1 bis 10 aus:

for index = 1, 10 do
  print(index)
end

Wir können mittels einer for-Schleife auch den Inhalt einer listenartigen Tabelle aufzählen. In unserem konkreten Fall der Tasche ersetzten wir

index = 1, 10

durch

index, wert in ipairs(tasche)

Ihr könnt euch vorstellen, dass ipairs(tasche) in jedem Durchgang der for-Schleife einen Eintrag in die Schleife einspeist. Jeder Eintrag besteht aus einem Index (zum Beispiel 1) und einem Wert (zum Beispiel "buch"). In dem oben genannten Fall haben wir dann im Schleifenkörper Zugriff auf Index und Wert über die entsprechenden Bezeichner. Wie wir sie nennen, ist uns überlassen. Wir könnten, wenn das Array zum Beispiel Ufos in einem Weltraumballerspiel enthält, schreiben:

i, ufo in ipairs(alle_ufos)

Im folgenden durchlaufen wir sämtliche Einträge in der Tabelle tasche. Es gilt:

Beim ersten Durchgang der for-Schleife ist index 1 und wert "buch"
Beim zweiten Durchgang ist index 2 und wert "brille"
Beim dritten Durchgang ist index 3 und wert "stift"
Da tasche nur drei Elemente enthält, ist nach dem dritten Durchgang Schluss: Das Programm springt aus der for-Schleife heraus.

Im Block der for-Schleife können wir wie gehabt auf index und wert zugreifen:

tasche = {"buch", "brille", "stift"}

for index, wert in ipairs(tasche) do
  print(index  .. ": " .. wert)
end

Die Ausgabe des Programms sieht so aus:

1: buch
2: brille
3: stift

Das Programm auf repl.it

Aufgabe 1.1: Schreibe ein Programm, bei dem die Nutzerin in einer Schleife gefragt wird, was sie in die Tasche tun möchte. Diese Eingaben sollen in eine Tabelle tasche eingetragen werden. Nach jedem Durchgang der Schleife soll angegeben werden, wie viele Elemente bereits in der Tasche sind:

Was möchtest Du in die Tasche tun?
$ Kartoffeln
Anzahl der Elemente in der Tasche: 1
Was möchtest Du in die Tasche tun?
$ Quark
Anzahl der Elemente in der Tasche: 2

tasche = {}

while true do
  print("Was möchtest Du in die Tasche tun?")
  eingabe = io.read()
  tasche[#tasche+1] = eingabe
  print("Anzahl der Elemente in der Tasche: " .. #tasche)
end

Die Lösung auf repl.it

Aufgabe 1.2: Erweitere die Lösung von Aufgabe 1.1, sodass nach jeder Eingabe nicht mehr die Anzahl der Elemente genannt, sondern alle Elemente aufgezählt werden:

Was möchtest Du in die Tasche tun?
$ Schraubendreher
In der Tasche befinden sich:
Schraubendreher
Was möchtest Du in die Tasche tun?
$ Hammer
In der Tasche befinden sich:
Schraubendreher
Hammer
tasche = {}

while true do

  print("Was möchtest Du in die Tasche tun?")
  eingabe = io.read()
  tasche[#tasche+1] = eingabe

  print("In der Tasche befinden sich:")
  for index, wert in ipairs(tasche) do
    print(wert)
  end
  
end


Aufgabe 1.3: Erweitere die Lösung von Aufgabe 1.2: Es sollen maximal 5 Sachen in die Tasche passen. Die Aufzählung soll erst erfolgen, wenn die Tasche voll ist. Löse die Aufgabe mit einer Abfrage innerhalb der while-Schleife und break.

Was möchtest Du in die Tasche tun?
$ Zitronen
Was möchtest Du in die Tasche tun?
$ Orangen
...
Die Tasche ist nun voll und enthält:
Zitronen
Orangen
...

tasche = {}

while true do

  print("Was möchtest Du in die Tasche tun?")
  eingabe = io.read()
  tasche[#tasche+1] = eingabe

  if #tasche >= 5 then
    break
  end

end

print("Die Tasche ist nun voll und enthält:")
for index, wert in ipairs(tasche) do
  print(wert)
end

Die Lösung auf repl.it

Elemente hinzufügen mit table.insert()

Wir haben bereits gezeigt, wie sich ein Element an das Ende der listenartigen Tabelle anhängen lässt:

tabelle[#tabelle+1] = x

Was ist aber, wenn die Reihenfolge (zum Beispiel bei einem Ranking von Lieblingsserien) eine Rolle spielt, und wir an einer anderen Stelle, am Anfang oder mitten drin, ein Element einfügen wollen?

Wir könnten das von Hand machen, indem wir alle Elemente, die nach dem Einfügepunkt kommen, um einen Schritt nach hinten verschieben:

lieblingsserien = {"Fargo", "The Prisoner", "Better call Saul"}

Ein Einfügen an der ersten Position würde dann so aussehen:

lieblingsserien[4] = lieblingsserien[3]
lieblingsserien[3] = lieblingsserien[3]
lieblingsserien[2] = lieblingsserien[1]
lieblingsserien[1] = "WandaVision"

Das ist ziemlich mühsam! Zum Glück erleichtert uns Lua die Aufgabe mit der Funktion table.insert(). table ist eine Bibliothek mit Funktionen, die sich auf Tabellen anwenden lassen. table.insert() erwartet drei Argumente:

table.insert(tabelle, position, wert)

tabelle    Die Tabelle, in die etwas eingefügt werden soll
position   Die Position der Einfügung
wert       Das, was eingefügt werden soll

Das nächste Beispiel zeigt die Anwendung. In diesen und den folgenden Beispielen werden wir zudem eine selbst definierte Funktion drucke_inhalt() anwenden, die den Inhalt einer Tabelle ausgibt.

function drucke_inhalt(tabelle)
  for index, wert in ipairs(tabelle) do
    print(index .. ": " .. wert)
  end
end

lieblingsserien = {"Fargo", "The Prisoner", "Better call Saul"}
table.insert(lieblingsserien, 1, "WandaVision")
drucke_inhalt(lieblingsserien)

Die Ausgabe sieht so aus:

1: WandaVision
2: Fargo
3: The Prisoner
4: Better call Saul

Das Programm auf repl.it

Elemente entfernen mit table.remove()

Wir können Elemente aus einer Tabelle zu entfernen, indem wir ihnen nil zuweisen. Das bringt bei listenartigen Tabellen aber Probleme mit sich:

aufgaben = {"Abwaschen", "Müll runterbringen", "Regale abstauben", "Saugen"}
aufgaben[2] = nil

Wenn wir jetzt die Tabelle aufgaben mit for und ipairs() durchlaufen, bricht die Aufzählung bereits nach dem ersten Element ab:

1: Abwaschen

Das liegt daran, dass durch das „Löschen“ der zweiten Position die Tabelle nicht mehr lückenlos ist. Die Schleife bricht an der ersten Position ab, die nil ist.

Auch hier könnten wir das Problem durch mühsames, schrittweises verschieben der nachfolgenden Elemente lösen. Auch hier bietet Lua eine vereinfachte Lösung an, und zwar mit der Funktion table.remove(). Sie erwartet zwei Argumente: Die Tabelle, aus der etwas entfernt werden soll und den Index des zu entfernenden Elementes.

aufgaben = {"Abwaschen", "Müll runterbringen", "Regale abstauben", "Saugen"}
table.remove(aufgaben, 2)
drucke_inhalt(aufgaben)

Jetzt sind die Elemente nach der entfernten Position um 1 aufgerückt:

1: Abwaschen
2: Regale abstauben
3: Saugen

Das Programm auf repl.it

Aufgabe 2.1: Du hast eine Tabelle mit den Namen von Geburtstagsgästen:

gaeste = {"Anna", "Peter", "Michael", "Sabine", "Michaela"}

Mit welchen Code fügst Du an der ersten Stelle den Gast „Theodor“ ein?

table.insert(gaeste, 1, "theodor")

Aufgabe 2.2: Michael hat abgesagt, und Du möchtest in von der Liste streichen. Anstatt mühsam von Hand die entsprechende Position (bzw. Index) zu ermitteln, schreibe einen Code, der die Position automatisch ermittelt und dann das entsprechende Element entfernt. Diese Aufgabe ist etwas knifflig, daher einige Tipps:

  • Du benötigst dafür for und ipairs()
  • Mache Dir die Tatsache zunutze, dass ipairs() sowohl den Index, als auch den Wert an die for-Schleife liefert
  • Ist in einem Schleifendurchgang wert == "Michael", kannst Du den entsprechenden Index zur Entfernung des Eintrages verwenden

gaeste = {"Anna", "Peter", "Michael", "Sabine", "Maria"}

for index, wert in ipairs(gaeste) do
  if wert == "Michael" then
    table.remove(gaeste, index)
  end
end

Die Lösung auf repl.it

Die Ausgabe sieht so aus, Michael steht nicht mehr in der Liste:

1: Anna
2: Peter
3: Sabine
4: Maria

Eine Tabelle als Wörterbuch

Im vorherigen Abschnitt haben wir uns mit listenartigen Tabellen beschäftigt. Eine wichtige Eigenschaft listenartiger Tabellen ist, dass die Indizes (allgemeiner: Schlüssel) positive ganze Zahlen sind. Das muß aber nicht sein. Im folgenden Beispiel sind die Schlüssel Strings. Eine Tabelle speichert englischen Übersetzungen einiger deutscher Wörter:

> woerterbuch = {}
> woerterbuch["sonne"] = "sun"
> woerterbuch["stern"] = "star"
> woerterbuch["erde"] = "earth"

Wenn Schlüssel Strings ohne Leerzeichen sind, dann gilt folgende, verkürzte Schreibweise:

> woerterbuch.sonne = "sun"
> woerterbuch.stern = "star"
> woerterbuch.erde = "earth"

Anstatt die Tabelle schrittweise zu befüllen, kannst Du es auch so schreiben – für die angehenden Programmierprofis nebenbei bemerkt: das nennt man die Literal-Schreibweise:

woerterbuch = {
  sonne = "sun",
  stern = "star",
  erde = "earth"
}

Die Abfrage der einzelnen Einträge läuft entsprechend so:

> woerterbuch["sonne"]
sun

Oder, in der verkürzten Schreibweise, so:

> woerterbuch.sonne
sun

Das folgende Programmbeispiel ist ein einfaches Übersetzungsprogramm, bei dem die Nutzerin ein deutsches Wort eingibt, um die englische Übersetzung zu erhalten. Gibt es zu dem eingegebenen deutschen Wort keinen Eintrag, liefert das Programm eine entsprechende Meldung.

woerterbuch = {
  sonne = "sun",
  stern = "star",
  erde = "earth"
}

while true do

  print("Bitte gib ein Wort ein.")
  eingabe = io.read()
  uebersetzung = woerterbuch[eingabe]

  if uebersetzung then
    print(uebersetzung)
  else
    print("Das Wort " .. eingabe .. " ist mir unbekannt")
  end
  
end

Zeile 11: woerterbuch[eingabe] liefert den Eintrag, falls vorhanden, sonst nil. Obacht: Die Kurzschreibweise woerterbuch.eingabe ist hier nicht möglich, weil das würde vom Lua Interpreter als woerterbuch["eingabe"] verarbeitet werden!
Zeile 13: Nur, wenn uebersetzung nicht nil ist …
Zeile 14: … gibt das Programm die uebersetzung aus …
Zeile 16: … andernfalls meldet das Programm, dass das Wort unbekannt ist

Das Programm auf repl.it

Aufgabe 3: Erweitere das letzte Programmbeispiel, sodass es neue Wörter lernen kann. Es soll, wenn ein Wort unbekannt ist, nach einer Übersetzung fragen und diese in die Tabelle woerterbuch eintragen. Teste das Programm auf der Konsole. Es soll folgendes Verhalten zeigen:

Bitte gib ein Wort ein.
$ buch
Das Wort 'buch' ist mir unbekannt. Bitte gib eine Übersetzung ein.
$ book 
Bitte gib ein Wort ein.
$ buch
book

woerterbuch = {
  sonne = "sun",
  stern = "star",
  erde = "earth"
}
 
while true do
 
  print("Bitte gib ein Wort ein.")
  eingabe = io.read()
  uebersetzung = woerterbuch[eingabe]
 
  if uebersetzung then
    print(uebersetzung)
  else
    print("Das Wort '" .. eingabe .. "' ist mir unbekannt. Bitte gib eine Übersetzung ein.")
    uebersetzung = io.read()
    woerterbuch[eingabe] = uebersetzung
  end
   
end

Die Lösung auf repl.it

Tabelleninhalte durchlaufen mit pairs()

Listenartige Tabellen können wir mit ipairs() durchlaufen. Die Funktion pairs() ist die allgemeinere Variante. Sie funktioniert für jede Art von Schlüssel:

woerterbuch = {
  sonne = "sun",
  stern = "star",
  erde = "earth"
}

for deutsch, englisch in pairs(woerterbuch) do
  print(deutsch .. ": " .. englisch)
end

Das Programm auf repl.it

Navigation

Übersicht

Vorherige Lektion

Nächste Lektion


Wir bedanken uns bei der Peakboard GmbH für die freundliche Unterstützung bei der Entwicklung dieses Kurses.

Peakboard ist eine All-in-One-Lösung aus Soft- und Hardware, mit der Du Daten aus unterschiedlichen Datenquellen erhebst, auswertest und in Echtzeit auf Bildschirmen visualisierst. Mit der kostenlosen Software, dem Peakboard Designer, gestaltest Du Dein individuelles Dashboard und bindest deine Datenschnittstellen an. Die Hardware, die Peakboard Box, verarbeitet und kommuniziert die Datenströme dezentral und damit ressourcenschonend direkt am Industriearbeitsplatz. Damit sorgst Du für mehr Transparenz und optimierst so ganz einfach deine Prozesse.