In deze les leggen we meer in detail uit hoe lijsten in Python worden opgeslagen. Het kopiëren en vergelijken van lijsten kan onverwachte gevolgen hebben. We zullen uitleggen hoe Python intern werkt, waardoor je fouten kunt begrijpen en ze kunt vermijden. In het algemeen is het probleem dat meerdere verschillende variabelen kunnen verwijzen naar ( in het Engels "point to" of "refer to") dezelfde lijst. Aan het eind van de les beschrijven we de "is
" operator die ons kan vertellen of twee variabelen echt naar dezelfde lijst verwijzen.
Voorbeeld
Laten we aannemen dat we een programma willen dat een lijst met lengte in inches met de naam oldSize
naar een lijst met dezelfde lengte, genaamd newSize
, in cm converteert. De meest voor de hand liggende manier is newSize = oldSize
te gebruiken om een kopie te maken en de lijstelementen vervolgens één voor één langs te lopen en die omzetten:
oldSize = ["letter", 8.5, 11] # size of paper in inches newSize = oldSize # make a copy newSize[1] = newSize[1]*2.54 # convert to cm newSize[2] = newSize[2]*2.54 # convert to cmLaten we eens kijken of dit werkt. Wanneer je
newSize
afdrukt dan zie je, zoals verwacht, ["letter", 21.59, 27.94]
. Maar er wacht ons een verrassing wanneer we oldSize
printen: de waarden van oldSize
zijn ook veranderd! Ter illustratie:
We zullen nu gedetailleerd, door middel van diagrammen, uitleg geven over wat er gebeurde. Het voornaamste probleem is dat newSize = oldSize
niet echt de hele lijst kopieert: het kopieerde slechts een verwijzing (pijl) naar dezelfde lijst.
Klik op de schuiftitels om de tabs te openen en te sluiten.
We zullen een tabel gebruiken om de variabelen en hun waarden in het geheugen van Python weer te geven. Na het uitvoeren van het programma
city = "Moose Factory" population = 2458 employer = citywordt in de tabel (met de zwarte rand) getoond hoe het geheugen van Python eruit ziet:
De naam van de eerste variabele is
city
en zijn waarde is de string "Moose Factory"
. De naam van de tweede variabele is population
en zijn waarde is het gehele getal 2458
. De naam van de derde variabele is employer
en zijn waarde is de string "Moose Factory"
.Vervolgens laten we zien hoe een lijst er in het geheugen uit ziet. Neem als voorbeeld de programmaregel
myList = ["Moose Factory", 2458]Dit zal één variabele aanmaken met de naam
myList
. Er wordt een lijst aangemaakt, en de waarde van myList
is gelijk aan het "wijzen" of "verwijzen" naar die lijst. We representeren de lijst door gebruik te maken van een box, en de waarden van de elementen worden in de box getoond naast hun corresponderende indices. De lijst wordt in het blauw getoond.De pijl laat zien dat
myList
verwijst naar deze nieuwe lijst. Het element met index 0 van de lijst is de string "Moose Factory"
, en het element met index 1 is het gehele getal 2458
. Als je bijvoorbeeld print(myList[1])
dan zal Python 2458
uitvoeren.Nu zullen we, ter illustratie, een extra regel toevoegen aan het vorige voorbeeld. Omdat er een baby is bijgekomen voeren we de volgende extra regels uit:
myList = ["Moose Factory", 2458] myList[1] = myList[1]+1Python berekent
2458+1
en dat is gelijk aan 2459
. Dit wordt de waarde die op index 1 in de lijst staat. Na de update krijgen we het diagram zoals hieronder weergegeven.(2458 is niet een deel van het geheugen van Python, we laten het zien om de verandering te benadrukken)
oldSize
en newSize
Nu keren we terug naar het centrale voorbeeld. De eerste regel is
oldSize = ["letter", 8.5, 11]en Pythons geheugen ziet er, nadat deze regel is uitgevoerd, uit als in het diagram beneden. We hebben een lijst van lengte 3 gemaakt.
In het programma is de tweede regel
newSize = oldSizeen hier komen we tot het belangrijkste punt: in de tweede regel maakt
=
geen kopie van de lijst! Het maakte slechts een nieuwe verwijzing naar dezelfde lijst. Dit wordt geïllustreerd door de twee verschillende pijlen die naar exact dezelfde box verwijzen. Dit verschilt nogal van hoe getallen en strings worden gekopieerd (zoals in de eerste slide). Het diagram laat zien dat we twee variabelen hebben, die naar dezelfde lijst verwijzen.
newSize[1] = newSize[1]*2.54komt, gaat Python naar
newSize
, naar de waarde met index 1 (8.5) en vermenigvuldigt dit met 2.54, en plaatst de waarde terug zoals te zien valt. Omdat oldSize
naar dezelfde lijst verwijst wordt ook oldSize
aangepast!(Voor de duidelijkheid: 8.5 maakt geen deel uit van het geheugen van Python, maar we laten het zien om de verandering te benadrukken.)
De volgende regel van het programma lijkt op de vorige,
newSize[2] = newSize[2]*2.54waarmee de andere waarde van de lijst wordt aangepast. Nadat deze regel is uitgevoerd ziet het geheugen van Python eruit zoals in het diagram hieronder wordt weergegeven.
Of we nu newSize
of oldSize
printen, Python geeft als output ["letter", 21.59, 27.94]
.
Er is nog een manier om naar dit voorbeeld te kijken. Namelijk door dit voorbeeld te bekijken in de Visualization tool. Merk op dat je in plaats van de pijlen de lijsten een naam krijgen via hun "ID." Elke lijst wordt bewaard op een "adres" dat gelijk is aan de ID; een adres is als een pijl die naar de box wijst met dat ID. |
Hoe op correcte wijze een lijst in Python te kopiëren
Hoewel het soms nuttig is om meerdere verwijzingen te hebben naar dezelfde lijst, is dat hier niet wat we willen.
We zullen drie oplossingen bespreken. Ze lijken op elkaar, maar in elke oplossing komt een nieuw aspect van Python aan de orde. Ze zijn dus alle drie de moeite waard om door te werken.
Methode 1, gebruik [:]
Hierboven lieten we zien dat newList = oldList[:]
wel een echte kopie van de oude lijst maakt. Hoewel de syntax wat vreemd oogt, is het een zusje van iets wat we eerder zagen. In de les over strings introduceerden we een manier om uit een string een substring te halen: string[first:tail]
geeft als resultaat een substring die begint met de index first
en eindigt met index tail-1
. We noemden al dat het gebruikt kan worden om een deellijst van een lijst te bepalen. In het bijzonder;
- wanneer
first
wordt weggelaten wordt de standaardwaarde0
genomen; - wanneer
tail
wordt weggelaten wordt de standaardwaardewaardelen(..)
(de lengte van de lijst/string) genomen.
Wat hier dus eigenlijk gebeurt is dat oldList[:]
een nieuwe sublijst maakt die precies dezelfde data bevat als de originele lijst. Dat is natuurlijk in feite een nieuwe kopie.
Methode 2, gebruik copy.copy()
Er is een module met de naam copy met meerdere methoden die met kopiëren te maken hebben. De meest eenvoudige is copy.copy(L)
: wanneer je dit aanroept op een lijst L
, zal de functie een echte kopie van L
teruggeven.
De copy.copy()
functie werkt ook op andersoortige objecten, maar we zullen daar hier niet verder op in gaan.
Methode 3, gebruik list()
De laatste manier om een echte kopie te maken is door middel van list() functie. Normaal wordt list()
gebruikt om data van een ander type te vertalen naar het lijst
-type. (Zo vertaalt list("hello")
de string "hello" in een lijst met 5 elementen, waarbij elk element één enkel karakter is.) Maar als je probeert een lijst om te zetten in een lijst, dan maakt het effectief een kopie.
Lijsten als argumenten
We merken op dat vanwege de manier waarop lijsten werken, iedere functie die een lijst als argument accepteert, de inhoud van een lijst kan veranderen. (Je kon dat al zien in de replace
-oefening.)
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])
func
veranderen we het element met index 0 in 7, en het element met index 1 in 7+4=11. Dus printen we 7*11.Lijsten vergelijken door middel van is
Wanneer zijn twee lijsten L1
en L2
gelijk? We kunnen deze vraag op twee manieren interpreteren:
- Dezelfde lijst: Wijzen
L1
enL2
naar exact hetzelfde lijst-object? - Dezelfde waarden: Is de inhoud van lijst
L1
gelijk aan de inhoud van lijstL2
?
Nu blijkt dat in Python de standaard gelijkheids-operator ==
Dezelfde waarden betekenis heeft, zoals in het volgende voorbeeld laat zien.
Om Dezelfde lijst te testen heeft Python de is
operator. We gebruiken deze operator op dezelfde manier als ==
: de syntax
«list1» is «list2»
geeft True
wanneer de variabelen naar dezelfde lijst verwijzen, en False
wanneer zij naar verschillende lijsten verwijzen (ook al hebben die dezelfde inhoud).
True
voor in de output van dit programma? (Teken een diagram om je met tellen te helpen.)
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)
Je moet niet zonder meer is gebruiken met strings of getallen omdat == al op correcte wijze de gelijkheid test, en het gedrag van is is moeilijk te voorspellen bij strings en getallen. |
Geneste lijsten
We hebben de meest belangrijke informatie nu wel gegeven, maar er is nog een veelvoorkomende situatie waarvan het waard is die te melden. Een geneste lijst is een lijst binnen een andere lijst. Bijvoorbeeld
sample = [365.25, ["first", 5]]
laat een geneste lijst zien. De buitenste lijst, waar
sample
naar verwijst, heeft twee elementen; het element met index 0 is een decimaal getal en het element op index 1 is de inwendige lijst. De binnenste lijst is ["first", 5]
. Je kunt dus meer niveaus hebben. Wanneer je geneste lijsten begint te gebruiken, houdt dan voor ogen dat:
- Wanneer we de drie methodes toepassen op
sample
dan zal de buitenste lijst gekopieerd worden, maar niet de binnenste lijst. Duscopy(sample)[1] is sample[1]
, betekent dat copy nog steeds een verwijzing bevat naar een deel van de originele lijst. Dit is niet altijd wat je wilt. Wanneer je een echte kopie wilt maken op alle niveaus, gebruik jecopy.deepcopy()
. - Lijsten testen met
==
is intuïtief: Python voert==
uit op elk element van de lijst. Zo is bijvoorbeeld[[1, 2], 3]==[[1, 2], 3]
True
, en[[1, 2], 3]==[1, 2, 3]
isFalse
want de eerste elementen zijn verschillend ([1, 2] != 1
).
Je hebt nu deze les afgerond! Het volgende materiaal is optioneel.
Tuples: ("immutable", "lists")
We noemden hierboven dat wanneer je een functie aanroept die werkt op een lijst het die lijst kan veranderen. Soms wil je dit onmogelijk maken! Een oplossing in Python is om tuples te maken. Tuples zijn in principe hetzelfde als lijsten, behalve dat ze niet veranderd kunnen worden. We zeggen dat lijsten "veranderbaar" zijn, en tuples "onveranderbaar" . (Strings en getallen zijn ook onveranderbaar.) Dit kan een nuttige manier zijn om programmeerfouten die te maken hebben met het veranderen van lijsten te voorkomen. Tuples zijn qua syntax bijna identiek aan lijsten, behalve dat bij tuples ronde haakjes ()
worden gebruikt in plaats van rechte haken []
. Je kunt tuples in lists omzetten en vice versa door middel van de functies tuple()
en list()
.
Naar ∞
en verder: een lijst die zichzelf bevat
Het is mogelijk om een lijst te hebben die zichzelf bevat! Maak eenvoudigweg een lijst, en zorg er dan voor dat één van zijn elementen verwijst naar de hele lijst:
Merk op dat Pythons output engine slim genoeg is om te herkennen dat de lijst terug naar zichzelf gaat: het drukt af "...
", en print niet de inhoud van L
opnieuw. Dit natuurlijk om een oneindige lus te voorkomen..