Listen vs. Tuple

Einführung

Listen und Tupel brauchen sogar für Anfänger keine Einführung. Diese beiden sind die am häufigsten verwendeten Datenstrukturen in Python. Aber was sind die Gemeinsamkeiten und die Unterschiede zwischen ihnen, welchen Typ sollte man bevorzugen und wann? Genau das werden wir uns in diesem Artikel – Python List vs. Tuple – anschauen.

Ähnlichkeiten

Sequenz-Typ (Sequence Type)

Sowohl Listen als auch Tupel sind Sequenz-Datentypen, in denen die Elemente in Form einer Sequenz gespeichert werden. Die Reihenfolge, in der die Elemente eingefügt werden, wird beibehalten, weshalb Sequenztypen auch als geordnete Mengen (ordered sets) bezeichnet werden.
Sowohl Listen als auch Tupel unterstützen die Operationen und Funktionen, die bei veränderlichen und unveränderlichen Typen üblich sind. Die gemeinsamen Operationen sind Verkettung, Wiederholung, Indexierung und Slicing. Und die gemeinsamen Funktionen sind min, max, len, count und index.

Homogene/Heterogene Daten

Obwohl Listen und Tupel in der Regel für homogene Daten verwendet werden, können sie auch heterogene Daten enthalten, d. h. sie enthalten unterschiedliche Datentypen als Elemente. Das folgende Beispiel zeigt, dass Listen und Tupel sowohl homogene als auch heterogene Daten enthalten können.

>>> mylist_1 = [10, 20, 30, 40, 50]
>>> mylist_2 = ["Python", "Simplified", ".com"]
>>> mylist_3 = ["Python", "Simplified", [1,2], (3,4)]
>>> mytuple_1 = (10, 20, 30, 40, 50)
>>> mytuple_2 = ("Python", "Simplified", ".com")
>>> mytuple_3 = ("Python", "Simplified", (1,2), [3,4])

Unterschiede

In diesem Abschnitt werden die Unterschiede zwischen Liste und Tupel in Python eingehend erläutert.

Syntax

Eine Liste ist eine Sammlung von Elementen, die in eckige Klammern [ ] eingeschlossen sind, während ein Tupel in runden Klammern ( ) eingeschlossen ist.

>>> mylist = [10, 20, 30, 40, 50]
>>> mytuple = (10, 20, 30, 40, 50)

Veränderlich vs. Unveränderlich (mutable vs. immutable)

Dies ist einer der Hauptunterschiede zwischen einer Liste und einem Tupel. Eine Liste ist ein veränderliches Objekt, während ein Tupel ein unveränderliches Objekt ist. Veränderlich bedeutet, dass der Inhalt des Objekts geändert werden kann (Einfügen, Ändern oder Löschen), sobald es erstellt wurde. Bei unveränderlichen Objekten kann ihr Inhalt nach der Erstellung nicht mehr geändert werden. 

Im folgenden Beispiel einer Liste wird der Inhalt der Liste geändert. Da es sich bei Listen um veränderbare Objekte handelt, können wir ihren Inhalt ändern.

>>> mylist_1 = [10, 20, 30, 40, 50]
>>> mylist_1.append(60)
>>> mylist_1[0] = 100
>>> del mylist_1[-1]
>>> mylist_1
[100, 20, 30, 40, 50]

Da Tupel jedoch unveränderliche Objekte sind, darf man ihren Inhalt nicht ändern. Andernfalls kommt es zu einem TypeError.

>>> mytuple_1 = (10, 20, 30, 40, 50)
>>> mytuple_1[0] = 100
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

Built-in Methods („Eingebaute Methoden“)

Da die Liste ein veränderbares Objekt ist, kann man ihren Inhalt einfügen, löschen oder ändern. Daher verfügt eine Liste über eine Vielzahl integrierter Methoden wie Insert, Pop, Remove, Sort, Sorted, Reverse usw. 
Ein Tupel hingegen unterstützt nur ein paar eingebaute Methoden, da ein Tupel unveränderlich ist. Da ein unveränderliches Objekt nicht verändert werden darf, ist es logisch, dass ein Tupel nicht viele eingebaute Funktionen hat.

Man kann dir(list) und dir(tuple) ausführen, um die unterstützten eingebauten Funktionen sowohl für list als auch für tuple zu sehen.

>>> dir(list)
>>> dir(tuple)

Speichereffizienz

Tupel sind speichereffizienter als Listen. Dies lässt sich in der Praxis besser verstehen. 

Liste: Wir wissen bereits, dass eine Liste ein veränderbares Objekt ist. Wenn man Elemente an die Liste anhängt, belegt Python den Speicher in einem bestimmten Intervall übermäßig voll (dieses Intervall wird von einem Algorithmus bestimmt, um den man sich nicht kümmern muss).

prev = 56
mylist_1 = []
print(f"Element-Nr.: {0}, Größe: {sys.getsizeof(mylist_1)}, Diff: {0}")

for num in range(1, 20):
    mylist_1.append(num)
    temp_size = sys.getsizeof(mylist_1)
    diff, prev = temp_size - prev, temp_size
    print(f"Element-Nr.: {num}, Größe: {temp_size}, Diff: {diff}")

Dieser Code gibt die folgenden Details aus. Wie man sehen kann, benötigt eine leere Liste 56 Bytes. Wenn man jedoch ein Element an eine leere Liste anhängt, ordnet Python 32 Bytes zu. Man erkennt auch, dass Python 24 zusätzliche Bytes zuweist (Überallokation), anstatt der 8 Bytes, die für ein Element benötigt werden. Die gleiche Überbelegung findet statt, wenn das 5., 9. und 17. Element an die Liste angehängt wird usw. Dieses Intervall wird durch einen Algorithmus bestimmt.

Ausgabe:

Element-Nr.: 0, Größe: 56, Diff: 0
Element-Nr.: 1, Größe: 88, Diff: 32
Element-Nr.: 2, Größe: 88, Diff: 0
Element-Nr.: 3, Größe: 88, Diff: 0
Element-Nr.: 4, Größe: 88, Diff: 0
Element-Nr.: 5, Größe: 120, Diff: 32
Element-Nr.: 6, Größe: 120, Diff: 0
Element-Nr.: 7, Größe: 120, Diff: 0
Element-Nr.: 8, Größe: 120, Diff: 0
Element-Nr.: 9, Größe: 184, Diff: 64
Element-Nr.: 10, Größe: 184, Diff: 0
Element-Nr.: 11, Größe: 184, Diff: 0
Element-Nr.: 12, Größe: 184, Diff: 0
Element-Nr.: 13, Größe: 184, Diff: 0
Element-Nr.: 14, Größe: 184, Diff: 0
Element-Nr.: 15, Größe: 184, Diff: 0
Element-Nr.: 16, Größe: 184, Diff: 0
Element-Nr.: 17, Größe: 256, Diff: 72
Element-Nr.: 18, Größe: 256, Diff: 0
Element-Nr.: 19, Größe: 256, Diff: 0
Element-Nr.: 20, Größe: 256, Diff: 0

Man kann auch feststellen, dass es einen Unterschied zwischen der Erstellung einer Liste mit 20 Elementen auf einmal und dem Anhängen von 20 Elementen nacheinander gibt. Der Unterschied in der Speicherzuweisung ist im folgenden Code zu sehen:

>>> sys.getsizeof(mylist_1)
256
>>> mylist_2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
>>> sys.getsizeof(mylist_2)
216

Tupel: Da Tupel unveränderlich sind, kann man ihren Inhalt nicht ändern. Wenn man jedoch ein Tupel erstellt, indem man ein Element nach dem anderen hinzufügt, und dabei die Tupelgröße überprüft, wird man feststellen, dass es immer einen Unterschied von 8 Byte gibt.

prev = 0
mytuple = ()
print(f"Element-Nr.: {0}, Größe: {sys.getsizeof(my_tuple)}, Diff: {0}")

for i in range(1, 11):
    my_tuple = tuple(range(1, i+1))
    tuple_size = sys.getsizeof(my_tuple)
    diff, prev = tuple_size - prev, tuple_size
    print(f"Element-Nr.: {i}, Größe: {tuple_size}, Diff: {diff}")

Ausgabe:

Element-Nr.: 0, Größe: 40, Diff: 0
Element-Nr.: 1, Größe: 48, Diff: 48
Element-Nr.: 2, Größe: 56, Diff: 8
Element-Nr.: 3, Größe: 64, Diff: 8
Element-Nr.: 4, Größe: 72, Diff: 8
Element-Nr.: 5, Größe: 80, Diff: 8
Element-Nr.: 6, Größe: 88, Diff: 8
Element-Nr.: 7, Größe: 96, Diff: 8
Element-Nr.: 8, Größe: 104, Diff: 8
Element-Nr.: 9, Größe: 112, Diff: 8
Element-Nr.: 10, Größe: 120, Diff: 8

Wenn man die obige mylist_1 in ein Tupel umwandelt, wird die Größe von 256 Byte auf 200 Byte reduziert.

>>> mytuple_1 = tuple(mylist_1)
>>> sys.getsizeof(mytuple_1)
200

Zeit-Effizienz

Erstellung von Liste und Tupel: Die Erstellung eines Tupels ist schneller als die einer Liste. Im folgenden Beispiel haben wir das Modul timeit verwendet, um zu sehen, wie viel Zeit es dauert, eine Liste und ein Tupel 10 Millionen Mal zu erstellen. Das Ergebnis zeigt deutlich, dass das Tupel der Gewinner ist.

>>> timeit("(1,2,3,4,5,6,7,8,9,10)", number=10_000_000)
0.20845220000046538
>>> timeit("[1,2,3,4,5,6,7,8,9,10]", number=10_000_000)
1.319473100000323

Zugriff auf die Elemente: Selbst der Zugriff auf die Elemente eines Tupels ist schneller als der Zugriff auf eine Liste. Auch wenn der Unterschied nicht sehr groß ist, ist ein Tupel dennoch der Gewinner. Das liegt daran, dass Tupel direkte Zeiger auf ihre Elemente haben, während die Liste ein anderes Zwischenfeld verwendet, das Zeiger auf die Elemente in der Liste enthält.

>>> my_tuple = tuple(range(100_000))
>>> my_list = list(my_tuple)
>>> timeit("my_tuple[99_999]", globals=globals(), number=10_000_000)
0.7667628999988665
>>> timeit("my_list[99_999]", globals=globals(), number=10_000_000)
0.7837690000014845

Wer ist also der Gewinner?

Zu welchem Ergebnis kommt man aufgrund des bisherigen Verständnisses von Liste vs. Tupel? Nun, es kommt darauf an. 

Wenn sich der Inhalt der Sequenz (Liste oder Tupel) während der Laufzeit des Programms nicht ändert, sollte man ein Tupel verwenden, andernfalls eine Liste. Oder anders ausgedrückt: Wenn man nur über die Sequenz iteriert, sollte man ein Tupel verwenden. Dies bietet Schreibschutz.

Quelle: Original-Artikel von Chetan Ambi