17: Is

In dieser Lektion werden wir mehr ins Detail gehen bezüglich der Frage, wie Python Listen speichert. Listen zu kopieren und zu vergleichen kann unerwartete Konsequenzen haben. Wir werden erklären, wie Python intern funktioniert, so dass du diese Fehler verstehen und vermeiden kannst. Das Problem im Allgemeinen ist, dass mehrere unterschiedliche Variablen auf dieselbe Liste "zeigen" oder dieselbe Liste "bezeichnen" können. Gegen Ende der Lektion beschreiben wir den "is" Operator, der uns sagt, ob zwei Variablen wirklich auf genau dieselbe Liste verweisen.

Beispiel

Nehmen wir zum Beispiel ein Code Fragment, das eine Liste von Längen in Zoll mit dem Namen oldSize in eine Liste derselben Längen in Zentimetern mit dem Namen newSize konvertiert. Der naheliegende Weg, dies zu erreichen, ist, die Zeile newSize = oldSize zu benutzen, um eine Kopie zu erzeugen, und sie dann durchzugehen, um alle Werte zu ändern:

oldSize = ["letter", 8.5, 11]   # Größe eines Papiers in Zoll
newSize = oldSize               # erstelle eine Kopie
newSize[1] = newSize[1]*2.54    # konvertiere in cm
newSize[2] = newSize[2]*2.54    # konvertiere in cm
Jetzt lasst uns sehen, ob das tatsächlich funktioniert. Wenn man newSize ausgibt, bekommt man ["letter", 21.59, 27.94] wie erwartet. Aber es gibt eine große Überraschung, wenn man auch oldSize ausgibt: die Werte von oldSize haben sich verändert! Zur Illustration:

Beispiel
Die zweite Zeile des Outputs ist nicht, was wir erwarten!

Wir werden nun im Detail erklären, was genau passiert ist, mit Hilfe einiger Diagramme. Das Hauptproblem ist, dass newSize = oldSize nicht wirklich die vollständige Liste kopiert hat: es hat nur einen Verweis (arrow) auf dieselbe Liste kopiert.

Klicke auf die Folientitel, um den Tab zu wechseln.

Speicher

Wir werden eine Tabelle benutzen, um die Variablen und Werte in Pythons Speicher zu repräsentieren. Zum Beispiel, nachdem wir folgenden Code laufen lassen

city = "Moose Factory"
population = 2458
employer = city
zeigt diese Tabelle (mit dem schwarzen Rand) wie Pythons Speicher aussieht:

Der Name der ersten Variable ist city und ihr Wert ist der String "Moose Factory". Der Name der zweiten Variable ist population und ihr Wert ist die Zahl 2458. Der Name der dritten Variable ist employer und ihr Wert ist der String "Moose Factory".


Listen im Speicher

Als nächstes zeigen wir, wie eine Liste im Speicher aussieht. Nehmen wir zum Beispiel das Code Fragment

myList = ["Moose Factory", 2458]
Dies wird nur eine Variable mit dem Namen myList erstellen. Eine Liste wird erstellt und der Wert von myList wird gesetzt als Äquivalent zu "zeigen" oder "verweisen" auf diese Liste. Wir repräsentieren die Liste mit Hilfe einer Box, und die Werte der Listeneinträge werden in der Box neben den ihnen entsprechenden Indizes angezeigt. Die Liste wird in blau angezeigt.

Der Pfeil zeigt, dass myList auf diese neue Liste verweist. Das Element bei Index 0 auf der Liste ist der String "Moose Factory", und das Element bei Index 1 ist die Zahl 2458. Zum Beispiel, wenn Du print(myList[1]) schreibst, wird Python 2458 ausgeben.


Einen Listenwert ersetzen

Jetzt fügen wir zu Illustrationszwecken eine weitere Zeile zum vorigen Beispiel hinzu. Ein Baby wird geboren, also lassen wir folgendes Codefragment laufen

myList = ["Moose Factory", 2458]
myList[1] = myList[1]+1
Python berechnet 2458+1 was äquivalent zu 2459 ist, und dies ersetzt nun den Wert bei Index 1 auf der Liste. Nach dem Update haben wir das unten gezeigte Diagramm.

(Die 2458 ist kein Teil von Pythons Speicher, sondern ist nur da um die Veränderung hervorzuheben.)


oldSize und newSize
Nun zurück zum Hauptbeispiel. Die erste Zeile war

oldSize = ["letter", 8.5, 11]
also sieht Pythons Speicher wie das Diagramm unten aus, nachdem diese Zeile ausgeführt wurde. Wir haben eine Liste der Länge 3 erstellt.

Hauptproblem
In unserem Programm lautet die zweite Zeile

newSize = oldSize
und hier finden wir tatsächlich das Hauptproblem: = dupliziert die Liste nicht! Stattdessen hat es nur einen neuen Verweis auf dieselbe Liste kopiert. Dies wird illustriert von zwei unterschiedlichen Pfeilen, die auf genau dieselbe Box zeigen. Das ist ein ziemlicher Unterschied dazu, Zahlen oder Ausdrücke zu kopieren (wie auf der ersten Folie).

Wie das Diagramm zeigt, haben wir zwei Variablen, die beide auf dieselbe Liste verweisen.


Updaten
Als nächstes, wenn das Programm die Zeile erreicht

newSize[1] = newSize[1]*2.54
Python sieht newSize, schlägt den Wert von dessen Index 1 nach (8.5) und multipliziert ihn mit 2.54, dann ersetzt es den Wert wie gezeigt. Aber, da oldSize sich auf dieselbe Liste bezieht, ist ein Nebeneffekt, dass wir auch oldSize modifiziert haben!

(Wiederum ist 8.5 nicht Teil von Pythons Speicher, sondern wird benutzt, um die Veränderung hervorzuheben.)


Ergebnis
Die nächste Zeile ist ähnlich,

newSize[2] = newSize[2]*2.54
was auch den anderen Wert in der Liste beeinflusst. Nachdem diese Zeile ausgeführt ist, sieht Pythons Speicher aus wie das Diagramm unten.

Wenn wir nun entweder newSize oder oldSize ausgeben, gibt uns Python ["letter", 21.59, 27.94].


Ein anderer Weg sich dieses Beispiel anzusehen, ist, es durch ein Visualization Tool laufen zu lassen. Falls Du das tust, musst Du beachten, dass Listen, anstatt von Pfeilen, mittels ihrer "ID" dargestellt werden. Jede Liste lebt gewissermaßen an einer "Adresse", die äquivalent zu ihrer ID ist; ein Adresswert ist wie ein Pfeil, der auf eine Box an einer ID Adresse zeigt.

Wie man in Python eine Liste richtig kopiert

Obwohl es manchmal nützlich ist, mehrere Verweise auf die selbe Liste zu haben, ist es hier nicht das, was wir wollen.

Wir geben drei Lösungen. Sie sind alle mehr oder weniger äquivalent, aber jede wird dir etwas Neues über Python beibringen, also ist es die Zeit wert, alle drei zu lesen.

Methode 1, Verwendung von [:]

Beispiel
Kopieren mit [:]

Oben demonstrieren wir, dass newList = oldList[:] eine echte Kopie der alten Liste erstellt. Obwohl diese Syntax seltsam aussieht, ist sie verwandt mit etwas, was wir bereits kennengelernt haben. In der Lektion zu Strings haben wir ein Verfahren vorgestellt, um Teilstrings zu isolieren: string[first:tail] gibt uns den Teilstring, der mit dem Index first anfängt und mit dem Index tail-1 endet. Wir hatten erwähnt, dass es auch verwendet werden kann, um Teillisten von Listen zu erstellen. Zusätzlich,

  • wenn Du first auslässt, nimmt es den Defaultwert von 0;
  • wenn Du tail auslässt, nimmt es den Defaultwert von len (die Länge der Liste/des Strings).

Was also tatsächlich passiert, ist dass oldList[:] eine neue Teilliste erstellt, allerdings eine, die dieselben Daten enthält wie die Originalliste, also ist es eine neue Kopie.

Methode 2, Verwendung von copy.copy()

Es gibt ein Modul namens copy, das mehrere Methoden in Verbindung mit dem Kopieren enthält. Die einfachste ist copy.copy(L): wenn sie auf eine Liste L angewandt wird, gibt die Funktion eine echte Kopie von L aus.

Beispiel
Kopieren mit copy.copy

Die copy.copy() Funktion kann auch für andere Arten von Objekten verwendet werden, aber diese werden hier nicht diskutiert werden.

Methode 3, Verwendung von list()

Die letzte Weise, eine echte Kopie zu erstellen, ist die Verwendung der list() Funktion. Normalerweise wird list() benutzt, um Daten von anderen Typen in den list Typen zu konvertieren. (Zum Beispiel list("hello") konvertiert den String "hello" in eine Liste mit fünf Elementen, jedes davon ein Buchstabe.) Aber wenn Du versuchst, etwas, das bereits eine Liste ist, in eine Liste zu konvertieren, erstellt es einfach eine Kopie.

Beispiel
Kopieren mit list()

Listen als Argumente

Beachte, dass aufgrund der Art und Weise wie eine Liste funktioniert, jede Funktion, bei der eine Liste als Argument funktioniert, auch tatsächlich die Inhalte einer Liste verändern kann. (Du hast dies schon in der replace Übung gesehen.)

Kurzübung: Listenargumente
Welche Zahl ist der Output des folgenden Programms?

def func(list):
    list[0] = list[0] + list[1]
    list[1] = list[1] + list[0]
data = [3, 4]
func(data)
print(data[0]*data[1])
Korrekt! Innerhalb von fonc ändern wir das Element beim Index 0 auf 7, und den Wert beim Index 1 auf 7+4=11. Also haben wir das Ergebnis 7*11.

Listen mit Hilfe von is vergleichen

Wann sind zwei Listenvariablen L1 und L2 äquvalent? Es gibt zwei unterschiedliche Möglichkeiten, diese Frage zu interpretieren:

  • Selbe Identität: Zeigen/Verweisen L1 und L2 auf genau dasselbe Listenobjekt?
  • Selbe Werte: Sind die Inhalte von Liste L1 äquivalent zu den Inhalten von Liste L2?

Es stellt sich heraus, dass in Python der Standard Äquvalenz- oder auch Gleichheitsoperator == die Selbe Werte Bedeutung hat, wie das folgende Beispiel zeigt.

Beispiel
Die Bedeutung von ==
Um auf Selbe Identität hin zu prüfen, hat Python den is Operator. Wir benutzen diesen Operator genauso wie ==: die Syntax

«list1» is «list2»

gibt True aus, falls die Listen auf dieselbe Liste verweisen und False, falls sie auf unterschiedliche Listen Verweisen (sogar wenn diese dieselben Inhalte haben).

Beispiel
Die Bedeutung von is

Kurzübung: True Count
Wie oft kommt True im Output dieses Programms vor? (Zeichne ein Diagramm, um den Überblick zu behalten.)

list1 = [9, 1, 1]
list3 = list(list1)
list2 = list1
list4 = list3[:]
list1[0] = 4
list5 = list1
print(list1 is list2, list2 is list3, list3 is list4, list4 is list5)
print(list1 == list2, list2 == list3, list3 == list4, list4 == list5)
Korrekt! Die Ausgabe ist
True False False False

Du solltest is nicht mit Strings oder Zahlen benutzen, weil == bereits in korrekter Weise Äquivalenz testet, und das Verhalten von is ist schwer vorherzusagen in Bezug auf Strings und Zahlen.

Verschachtelte lists

Wir haben bereits die meisten wichtigen Informationen erwähnt, aber es gibt noch eine weitere häufige Situation, die es zu erwähnen lohnt. Eine geschachtelte Liste ist eine Liste innerhalb einer anderen Liste, zum Beispiel

sample = [365.25, ["first", 5]]

zeigt eine geschachtelte Liste. Die äußere Liste, auf die sample verweist, hat zwei Elemente; das Element an Index 0 ist eine Gleitpunktzahl ("float") und das Element an Index 1 ist die innere Liste. Die innere Liste ist ["first", 5]. (Du kannst auch eine Verschachtelung mit mehreren Ebenen herstellen.) Sobald Du anfängst, verschachtelte Listen zu benutzen, denk daran:

  • Die drei Methoden von oben auf sample anzuwenden, kopiert die äußere Liste, aber nicht die innere Liste. Also copy(sample)[1] is sample[1], was bedeutet, dass die Kopie immernoch einen Verweis auf einen Teil der ursprünglichen Liste enthält. Das war vermutlich nicht was Du wolltest. Wenn Du eine echte Kopie auf jeder Ebene machen willst, benutze copy.deepcopy().
  • Geschachtelte Listen mit == zu testen ist relativ intuitiv: Python wendet == rekursiv auf jedes Listenelement an. Zum Beispiel [[1, 2], 3]==[[1, 2], 3] ist True, und [[1, 2], 3]==[1, 2, 3] ist False, da die ersten Elemente unterschiedlich sind ([1, 2] != 1).

Beispiel
deepcopy und rekursive Äquivalenz

Du hast diese Lektion abgeschlossen! Das folgende Extramaterial ist freiwillig.


Tupel: ("immutable", "lists")

Wir haben oben erwähnt, dass, wenn Du eine Funktion auf eine Liste anwendest, sie die Liste verändern kann. Manchmal willst Du das unmöglich machen! Eine Lösung in Python ist es, Tupel zu erstellen, die dasselbe wie Listen sind, außer, dass sie nicht verändert werden können. Wir sagen Listen sind "veränderlich" und Tupel sind "unveränderlich". (Strings und Zahlen sind auch unveränderlich.) Das kann eine nützliche Strategie sein, um zu verhindern, dass Programmierfehler Listen verändern, die nicht geändert werden sollten. Tupel sind beinahe identisch mit Listen, außer dass sie runde Klammern benutzen () anstatt eckiger Klammern []. Du kannst zwischen Tupeln und Listen konvertieren, indem Du tuple() und list() benutzt.

Beispiel
Schlauchtupel

Zur und darüber hinaus: Selbst-Enthaltung

Es ist möglich, eine Liste zu haben, die sich selbst enthält! Mache einfach eine Liste und dann lasse eines ihrer Elemente auf die ganze Liste zurück verweisen:

Beispiel
Ein zirkulärer Verweis

Du siehst, dass Pythons Outputgenerator schlau genug ist, zu erkennen, dass die Liste in einer Schleife auf sich selbst zurück verweist: er gibt "..." aus anstatt L noch einmal als Ganzes auszugeben, um eine unendliche Schleife zu vermeiden.