17: Is

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 cm
Laten 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:

Voorbeeld
De tweede regel van de output is niet wat we hadden verwacht!

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.

Geheugen

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 = city
wordt 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".

Lijsten in het geheugen

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.

Een waarde in een lijst vervangen

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]+1
Python 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.

Centrale probleem

In het programma is de tweede regel

newSize = oldSize
en 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.


Updaten
Wanneer vervolgens het programma bij de regel

newSize[1] = newSize[1]*2.54
komt, 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.)

Resultaat

De volgende regel van het programma lijkt op de vorige,

newSize[2] = newSize[2]*2.54
waarmee 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 [:]

Voorbeeld
Een kopie maken met [:]

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 standaardwaarde 0 genomen;
  • wanneer tail wordt weggelaten wordt de standaardwaardewaarde len(..) (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.

Voorbeeld
Een kopie maken met copy.copy

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.

Voorbeeld
Een kopie maken met list()

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.)

Oefening met Kort Antwoord: Lijst argument
Welk getal is output van het volgende programma?

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])
Correct! Binnen 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 en L2 naar exact hetzelfde lijst-object?
  • Dezelfde waarden: Is de inhoud van lijst L1 gelijk aan de inhoud van lijst L2?

Nu blijkt dat in Python de standaard gelijkheids-operator ==  Dezelfde waarden betekenis heeft, zoals in het volgende voorbeeld laat zien.

Voorbeeld
De betekenis van ==

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).

Voorbeeld
De betekenis van is

Oefening met Kort Antwoord: True Count
Hoe vaak komt 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)
Correct! Het resultaat is True False True False

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. Dus copy(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 je copy.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] is False want de eerste elementen zijn verschillend ([1, 2] != 1).

Voorbeeld
deepcopy and recursive equality

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().

Voorbeeld
Tubular tuples

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:

Voorbeeld
Een circulaire referentie

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..