Vorlesungsskript 12

Listenoperationen

In diesem Vorlesungsskript lernen wir den Unterschied zwischen unveränderlichen und veränderlichen Objekten kennen und den Unterschied zwischen destruktiven und nichtdestruktiven Operationen. Gewappnet mit diesem Wissen wenden wir uns dann Pythons Listenoperationen zu – den Anweisungen, mit denen wir Listen verarbeiten können. Schließlich lernen wir noch ein paar Operationen kennen, die speziell für Listen von Zahlen bzw. für Listen von Strings nützlich sind.

Unveränderliche Objekte

Die Datentypen, mit denen wir uns bisher beschäftigt haben – Zahlen und Strings – sind unveränderlich (engl. immutable). Ein Objekt dieser Datentypen, einmal erzeugt und als Folge von Nullen und Einsen im Arbeitsspeicher abgelegt, kann nicht mehr verändert werden. Es bleibt gleich, bis es nicht mehr gebraucht und automatisch aus dem Arbeitsspeicher gelöscht wird.

Alle Ausdrücke, die auf Zahlen und Strings operieren, geben also tatsächlich neue Objekte zurück; die Input-Objekte bleiben unangetastet. Zum Beispiel:

>>> a = 'dog'
>>> b = 'house'
>>> c = a + b
>>> a
'dog'
>>> b
'house'
>>> c
'doghouse'

In der dritten Anweisung wird aus 'dog' und 'house' ein neuer String erzeugt, die beiden Objekte werden aber nicht verändert und bleiben in a bzw. b gespeichert.

Zahlen- und Stringobjekte sind also unveränderlich. Was natürlich schon verändert werden kann, ist der Wert einer Variablen. Man ersetzt dann das in der Variablen gespeicherte Objekt durch ein anderes Objekt. Zum Beispiel:

>>> a = 'dog'
>>> a = a + 'house'
>>> a
'doghouse'

Das alte Objekt (in diesem Beispiel der String 'dog') bleibt zunächst im Arbeitsspeicher, obwohl es nicht mehr in der Variablen a gespeichert ist. Es wird ggf. später automatisch gelöscht, wenn es nicht mehr gebraucht wird.

Listen sind veränderlich

Im Gegensatz zu Zahlen und Strings sind Listen veränderlich (engl. mutable). Man kann zu einer Liste Elemente hinzufügen und welche wegnehmen und man hat hinterher immer noch dasselbe Listenobjekt, aber sein Inhalt hat sich verändert.

Bei Anweisungen, die auf veränderlichen Objekten wie Listen operieren, unterscheidet man daher zwischen destruktiven und nichtdestruktiven Anweisungen. Destruktive Anweisungen verändern den Inhalt des bestehenden Objekts und der bisherige Zustand geht verloren. Nichtdestruktive Anweisungen verhalten sich wie bei Zahlen und Strings: Es wird ein neues Objekt erzeugt und das alte bleibt unverändert bestehen.

Vergleichsoperatoren für Listen

Ganz ähnlich wie bei Strings lassen sich auch bei Listen die Vergleichsoperatoren ==, !=, <, >, <= und >= nutzen. Zwei Listen sind „gleich“, wenn sie die gleichen Elemente in derselben Reihenfolge haben. Die Standard-Sortierreihenfolge von Listen, nach der „kleiner als“ und „größer als“ bestimmt werden, ergibt sich so, dass nach dem ersten Element sortiert wird. Bei gleichem ersten Element wird nach dem zweiten Element sortiert, usw. Zum Beispiel:

>>> [23, 42] == [42, 23]
False
>>> [23, 42] == [23, 42]
True
>>> [1.0, 'a'] == [1, 'a']
True
>>> [] == [None]
False
>>> [23, 42] > [23, 42]
False
>>> [23, 43] > [23, 42]
True
>>> [22, 43] > [23, 42]
False

Nichtdestruktive Listenoperationen

Folgende Ausdrücke (Operatoren-, Indexing- und Slicing-Ausdrücke, Funktions- und Methodenaufrufe) lassen sich mit Listen verwenden, ohne die zugrundeliegenden Listen zu verändern:

Ausdruck Gibt zurück
x in l True, wenn das Element x in der Liste l vorkommt, sonst False
x not in l False, wenn das Element x in der Liste l vorkommt, sonst True
l + m eine neue Liste mit den Elementen der Listen l und m (die Konkatenation der beiden Listen)
n * l oder l * n eine neue Liste mit den Elementen der Liste l, n-mal wiederholt
l[i] das Element am Index i in der Liste l (wie bei Strings gezählt beginnend bei 0 bzw. bei negativen Indizes von hinten bei -1)
l[i:j] eine Subliste von l, beginnend mit dem Element am Index i und endend vor dem Element am Index j (wie auch beim String-Slicing können Indizes weggelassen und ggf. eine Schrittlänge angegeben werden)
len(l) Länge der Liste l (Anzahl ihrer Elemente)
l.index(x) wenn x ein Element der Liste l ist: das wievielte Element es ist (zählend von 0, beginnend am Anfang)
ansonsten tritt ein Fehler auf
l.count(x) wie oft das Element x in der Liste l vorkommt
l.copy() eine neue Liste mit denselben Elementen wie l
sorted(l) eine neue Liste mit denselben Elementen wie l, aber entsprechend ihrer Standard-Sortierreihenfolge sortiert

Zum Beispiel:

>>> 'e' in ['a', 'b', 'c', 'd']
False
>>> 'b' in ['a', 'b', 'c', 'd']
True
>>> ['b', 'c'] in ['a', 'b', 'c', 'd']
False
>>> ['b', 'c'] in ['a', ['b', 'c'], 'd']
True
>>> 'e' not in ['a', 'b', 'c', 'd']
True
>>> ['a'] + ['b', 'c', 'd']
['a', 'b', 'c', 'd']
>>> ['a', 'b'] * 3
['a', 'b', 'a', 'b', 'a', 'b']
>>> 3 * ['a', 'b']
['a', 'b', 'a', 'b', 'a', 'b']
>>> len(['a', 'b', 'c', 'd'])
4
>>> len(['a', ['b', 'c'], 'd'])
3
>>> ['a', 'b', 'c', 'd'][1]
'b'
>>> ['a', 'b', 'c', 'd'][2]
'c'
>>> ['a', ['b', 'c'], 'd'][1]
['b', 'c']
>>> ['a', ['b', 'c'], 'd'][1][0]
'b'
>>> ['a', ['b', 'c'], 'd'][2]
'd'
>>> ['a', 'b', 'c', 'd'][1:3]
['b', 'c']
>>> ['a', ['b', 'c'], 'd'][1:3]
[['b', 'c'], 'd']
>>> ['a', ['b', 'c'], 'd'][1:]
[['b', 'c'], 'd']
>>> ['a', ['b', 'c'], 'd'][:2]
['a', ['b', 'c']]
>>> ['a', 'b', 'c', 'd'].index('c')
2
>>> ['a', ['b', 'c'], 'd'].index('c')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: 'c' is not in list
>>> ['a', 'b', 'c', 'd', 'a'].count('b')
1
>>> ['a', 'b', 'c', 'd', 'a'].count('a')
2
>>> l = ['a', 'b', 'c', 'd']
>>> m = l.copy()
>>> l.append('e')
>>> l
['a', 'b', 'c', 'd', 'e']
>>> m
['a', 'b', 'c', 'd']
>>> sorted(['b', 'a', 'd', 'c'])
['a', 'b', 'c', 'd']

Destruktive Listenoperationen

Die folgenden Anweisungen verändern bestehende Listen. Manche davon sind Zuweisungen, andere sind Löschanweisungen und wieder andere sind Ausdrücke. Auch die Ausdrücke geben aber nichts zurück, genauer gesagt: Sie geben None zurück (mit Ausnahme der Methode pop). Der Nutzen dieser Ausdrücke liegt nicht in ihren Rückgabewerten, sondern in dem Effekt, den sie auf die bestehenden Objekte haben.

Anweisung Wirkung
l[i] = x Das Element der Liste mit dem Index i wird durch das Objekt x ersetzt.
l[i:j] = m Die Elemente mit den Indizes i bis j - 1 werden entfernt und durch die Elemente von m ersetzt.*
del l[i] Das Element der Liste mit dem Index i wird entfernt.
del l[i:j] Die Elemente mit den Indizes i bis j - 1 werden entfernt.*
l.append(x) Das Objekt x wird der Liste l am Ende hinzugefügt.
l.insert(i, x) Das Objekt x wird der Liste l am Index i hinzugefügt.
l.extend(m) oder l += m Die Elemente von m werden der Liste l am Ende hinzugefügt.
l *= n Die Liste l wird so erweitert, dass sie dieselben Elemente wie vorher enthält, aber n-mal wiederholt.
l.clear() Entfernt alle Elemente aus der Liste l.
l.pop() Entfernt das letzte Element aus der Liste l und gibt es zurück.
l.pop(i) Entfernt das Element am Index i aus der Liste l und gibt es zurück.
l.remove(x) Sucht das erste Vorkommen des Objektes x in der Liste l und entfernt es. Wird das Objekt nicht gefunden, tritt ein Fehler auf.
l.reverse() Kehrt die Reihenfolge der Elemente der Liste l um.
l.sort() Sortiert die Liste l entsprechend der Standard-Sortierreihenfolge der Elemente.

* Hier lassen sich i und j auch weglassen und eine Schrittlänge hinzufügen, wie bei Slicing-Ausdrücken.

Zum Beispiel:

>>> l = ['a', 'b', 'c', 'd']
>>> l[1] = 'B'
>>> l
['a', 'B', 'c', 'd']
>>> l[2:4] = ['C']
>>> l
['a', 'B', 'C']
>>> l[2:4] = ['C', 'D']
>>> l
['a', 'B', 'C', 'D']
>>> l[3:3] = [55]
>>> l
['a', 'B', 'C', 55, 'D']
>>> l[5:] = [0]
>>> l
['a', 'B', 'C', 55, 'D', 0]
>>> l[:1] = ['I']
>>> l
['I', 'B', 'C', 55, 'D', 0]
>>> del l[:3]
>>> l
[55, 'D', 0]
>>> del l[1]
>>> l
[55, 0]
>>> l.append(213)
>>> l
[55, 0, 213]
>>> l.insert(2, 44)
>>> l
[55, 0, 44, 213]
>>> l.extend([44, 1])
>>> l
[55, 0, 44, 213, 44, 1]
>>> l += [0]
>>> l
[55, 0, 44, 213, 44, 1, 0]
>>> l.pop()
0
>>> l
[55, 0, 44, 213, 44, 1]
>>> l.pop(1)
0
>>> l
[55, 44, 213, 44, 1]
>>> l.remove(44)
>>> l
[55, 213, 44, 1]
>>> l.reverse()
>>> l
[1, 44, 213, 55]
>>> l.sort()
>>> l
[1, 44, 55, 213]
>>> l *= 2
>>> l
[1, 44, 55, 213, 1, 44, 55, 213]
>>> l.clear()
>>> l
[]

Unerwartete Folgen von destruktiven Operationen

Destruktive Operationen ändern den Inhalt von Objekten, was mitunter unerwartete Folgen haben kann, wenn man nicht genau aufpasst. Zum Beispiel kann sich das Ergebnis eines Vergleichs zweier Listen ändern, wenn sich er Inhalt einer der Listen ändert. Betrachten Sie das folgende Beispiel:

>>> l = [23, 42]
>>> m = [23, 42]
>>> l == m
True
>>> l.append(101)
>>> l == m
False

Hier vergleichen wir zwei Listen, fügen dann der einen ein Element am Anfang hinzu und vergleichen dann wieder, mit anderem Ergebnis.

Zum anderen kann es zu Verwirrung kommen, wenn dasselbe Objekt in zwei Variablen gespeichert ist und man den Inhalt der einen Variablen ändert. Damit ändert man automatisch auch den Inhalt der anderen Variablen, weil er ja dasselbe Objekt ist. Zum Beispiel:

>>> carnivores = ['dog', 'cat']
>>> my_pets = carnivores
>>> my_pets
['dog', 'cat']
>>> my_pets.append('hamster')
>>> my_pets
['dog', 'cat', 'hamster']
>>> carnivores
['dog', 'cat', 'hamster']

Hier haben wir der Liste carnivores unbeabsichtigt das Element 'hamster' hinzugefügt, weil wir nicht beachtet haben, dass es dasselbe Listenobjekt ist wie my_pets. Die Lösung wäre, auf der zweiten Zeile der Variablen my_pets nicht dasselbe Objekt zuzuweisen wie carnivores, sondern eine Kopie davon:

>>> carnivores = ['dog', 'cat']
>>> my_pets = carnivores.copy()
>>> my_pets
['dog', 'cat']
>>> my_pets.append('hamster')
>>> my_pets
['dog', 'cat', 'hamster']
>>> carnivores
['dog', 'cat']

Nichtdestruktive Operationen mit Listen von Zahlen

Die Funktionen min und max haben wir bereits mit mehreren Zahlen als Argumente aufgerufen. Sie geben dann das kleinste bzw. größte Argument zurück. Sie lassen sich aber auch mit nur einem Argument aufrufen, das dann eine Liste sein muss – und geben dann das kleinste bzw. größte Element zurück. Außerdem gibt es noch die Funktion sum, das die Summe aller Elemente einer Liste von Zahlen berechnet.

Ausdruck Gibt zurück
min(l) die kleinste Zahl in der Liste l
max(l) die größte Zahl in der Liste l
sum(l) die Summe aller Zahlen in der Liste l

Zum Beispiel:

>>> min([1, -42, 0])
-42
>>> max([1, -42, 0])
1
>>> sum([1, -42, 0])
-41

Mit Hilfe von sum lässt sich leicht z.B. der Mittelwert aller Zahlen in einer Liste berechnen, als Quotient der Summe und der Anzahl der Elemente:

>>> my_numbers = [1, 2, 3, 4]
>>> sum(my_numbers) / len(my_numbers)
2.5

Nichtdestruktive Operationen mit Listen von Strings

Strings haben die Methode split, mit der man den String in eine Liste seiner Teilstrings zerlegen kann. Umgekehrt kann man mit der join-Methode eine Liste von Strings zu einem String zusammenfügen.

Ausdruck Gibt zurück
s.split() den String s, aufgeteilt in eine Liste von Strings anhand von beliebigen Whitespace-Zeichen als Trennzeichen
s.split(sep) den String s, aufgeteilt in eine Liste von Strings anhand des Trennzeichens sep
sep.join(l) die Strings in der Liste l zusammengefügt zu einem einzigen String, jeweils mit dem String sep dazwischen

Die split-Methode ist sehr nützlich, um mehrspaltigen Input zu verarbeiten. Am einfachsten ist es, wenn die Felder im Input durch Whitespace getrennt sind, selbst nie leer sind und keinen Whitespace enthalten. Dann kann man die Methode ohne Argument aufrufen und aller Whitespace wird automatisch entfernt, auch am Ende der Zeilen. Zum Beispiel:

>>> ' ab cde  fghi jklm\n'.split()
['ab', 'cde', 'fghi', 'jklm']
>>> 'der\tdie\tART\t241408360.16429\n'.split()
['der', 'die', 'ART', '241408360.16429']

Hier ist ein Beispielprogramm pos.py, das mehrspaltigen Input dieser Art liest und von jeder Zeile nur das Feld mit dem Index 2 ausgibt:

import sys

for line in sys.stdin:
    fields = line.split()
    print(fields[2])

Auf die DeReKo-Frequenzliste angewendet, gibt das Programm eine Liste der Wortarten aus:

$ cat DeReKo-2014-II-MainArchive-STT.100000.freq | python3 pos.py
$,
$.
ART
ART
KON
$(
APPR
ART
$(
$(
…

Oft können die Felder im Input allerdings leer sein oder selbst Leerzeichen enthalten. Dann gibt der Aufruf der Methode wie oben nicht das gewünschte Ergebnis:

>>> 'New York\tNew York\tNE\t968363\n'.split()
['New', 'York', 'New', 'York', 'NE', '968363']

In diesem Fall muss man split das Trennzeichen (z.B. Tabulator oder Semikolon) als Argument übergeben. Nur dieses wird dann als Trennzeichen verwendet, anderer Whitespace bleibt unangetastet:

>>> 'New York\tNew York\tNE\t968363\n'.split('\t')
['New York', 'New York', 'NE', '968363\n']

Newline-Zeichen am Ende muss man dann ggf. separat mit rstrip entfernen:

>>> 'New York\tNew York\tNE\t968363\n'.rstrip().split('\t')
['New York', 'New York', 'NE', '968363']
>>> 'Donald Duck;Blumenstr. 13;Entenhausen\n'.rstrip().split(';')
['Donald Duck', 'Blumenstr. 13', 'Entenhausen']

Hier einige Beispiele für die join-Methode:

>>> ';'.join(['Donald Duck', 'Blumenstr. 13', 'Entenhausen'])
'Donald Duck;Blumenstr. 13;Entenhausen'
>>> ' '.join(['Ente', 'Ente', 'Ente'])
'Ente Ente Ente'
>>> ''.join(['Ente', 'Ente', 'Ente'])
'EnteEnteEnte'