Osa 5

Referenser

Hittills har vi tänkt att en variabel är en slags "låda" som innehåller variabelns värde. Från teknisk synvinkel stämmer detta inte i Python – variabler innehåller inte ett värde, utan en referens till ett objekt, såsom en siffra, sträng eller en lista.

I praktiken innebär det att man inte lagrar ett värde i en variabel, utan däremot en minnesposition där man kan hitta variabelns värde.

En referens kan beskrivas som en pil från variabeln till dess riktiga värde:

5 2 1

Referensen berättar alltså var det riktiga värdet finns. Funktionen id berättar vad en variabel refererar till:

a = [1, 2, 3]
print(id(a))
b = "Det här är också en referens"
print(id(b))
Exempelutskrift

4538357072 4537788912

Referensen, alltså variabelns id är ett heltal. Man kan tänka att talet är adressen för variabelns värde i datorns minne. Observera att om du kör koden ovan på din dator, kommer resultatet sannolikt vara ett annat eftersom variablerna knappast har lagrats på exakt samma adress i din dators minne.

Som vi redan såg i förra delens exempel, visar Python Tutors visualiseringsverktyg referenser som "pilar" till det egentliga innehållet. Verktyget "lurar" ändå när det kommer till strängar, och visar dem som att strängens innehåll skulle vara lagrat i själva variabeln:

5 2 1a

Så är det inte i verkligheten – strängar behandlas också internt av Python på samma sätt som listor.

Flera av Pythons inbyggda datatyper – som str – är oföränderliga. Det betyder att värdet på objektet aldrig kan ändras. Däremot kan ett värde ersättas med ett nytt värde.

5 2 2

I Python finns också datatyper som är föränderliga. Till exempel kan innehållet i en lista förändras utan att man behöver skapa en ny lista:

5 2 3

Något förvånande är att också grundläggande datatyper för lagring av tal och sanningsvärden, int, float och bool, är oföränderliga. Låt oss använda följande kod som exempel:

tal = 1
tal = 2
tal += 10

Det verkar som att koden ändrar på talet, men från teknisk synvinkel är det inte så. Istället skapar varje instruktion ett nytt tal.

Utskriften från det här programmet är intressant:

tal = 1
print(id(tal))
tal += 10
print(id(tal))
a = 1
print(id(a))
Exempelutskrift

4535856912 4535856944 4535856912

I början refererar variabeln tal till adressen 4535856912 och när variabelns värde förändras refererar variabeln till adressen 4535856944. När variabeln a definieras och får värdet 1, kommer variabeln att referera till samma ställe som variabeln tal när dess värde var 1.

Det verkar som att Python har lagrat siffran 1 i adressen 4535856912 och alltid då en variabels värde är 1, refererar variabeln till det här specifika stället i "datorns minne".

Även om de grundläggande datatyperna int, float och bool är referenser behöver man som programmerare egentligen inte fundera på det.

Flera referenser till en och samma lista

Vi undersöker vad som händer om vi försöker kopiera en lista:

a = [1, 2, 3]
b = a
b[0] = 10

Deklarationen b = a kopierar variabeln a:s värde till variabeln b. Det är ändå viktigt att observera att variabelns värde inte är en lista utan en referens till listan.

Deklarationen b = a kopierar alltså referensen, varpå det efter kopieringen finns två referenser till samma lista.

5 2 4

Listan kan behandlas med båda referenserna:

lista = [1, 2, 3, 4]
lista2 = lista

lista[0] = 10
lista2[1] = 20

print(lista)
print(lista2)
Exempelutskrift

[10, 20, 3, 4] [10, 20, 3, 4]

Om flera variabler refererar till samma lista, kan vi använda vilken som helst av variablerna för att komma åt listan. Det innebär dock också att ändringar som görs via en referens också kommer att påverka alla andra referenser.

Visualiseringsverktyget klargör igen vad som sker i programmet:

5 2 4a

Att kopiera en lista

Om du vill skapa en verklig kopia av en lista kan du skapa en ny lista och lägga till alla element från den ursprungliga listan i den nya listan:

lista = [1, 2, 3, 3, 5]

kopia = []
for element in lista:
    kopia.append(element)

kopia[0] = 10
kopia.append(6)
print("lista", lista)
print("kopia", kopia)
Exempelutskrift

lista [1, 2, 3, 3, 5] kopia [10, 2, 3, 3, 5, 6]

Så här ser det ut i visualiseringsverktyget:

5 2 4b

Variabeln ny_lista refererar till en annan lista än min_lista.

Ett enklare sätt att kopiera en lista är att använda hakparenteser [], som vi använt tidigare för att extrahera innehåll från strängar och listor. Notationen [:] väljer alla element i en samling. Därmed skapar det en kopia av en lista:

lista = [1,2,3,4]
kopia = lista[:]

lista[0] = 10
kopia[1] = 20

print(lista)
print(kopia)
Exempelutskrift

[10, 2, 3, 4] [1, 20, 3, 4]

En lista som parameter i en funktion

När vi ger en lista som argument till en funktion, får funktionen tillgång till referensen till listan. Det här innebär att funktionen kan ändra på listan.

Till exempel lägger följande funktion till ett nytt element i den lista som getts som argument:

def lagg_till_element(lista: list):
    nytt_element = 10
    lista.append(nytt_element)

lista = [1,2,3]
print(lista)
lagg_till_element(lista)
print(lista)
Exempelutskrift
[1, 2, 3] [1, 2, 3, 10]

Märk att funktionen lagg_till_element inte returnerar något, utan ändringen sker direkt i den lista som getts som argument. Visualiseringsverktyget presenterar situationen så här:

5 2 4c

Global frame syftar till huvudprogrammets variabler, och den blå lådan lagg_till_element på funktionens parametrar och variabler. Som visualiseringen visar, refererar funktionen till samma lista som huvudprogrammet, vilket betyder att ändringar som görs i listan inom funktionen också syns i huvudprogrammet.

Om man vill undvika detta kan man inne i funktionen först skapa en ny kopia av argumentlistan, göra ändringar i den och slutligen returnera den:

def lagg_till_element(lista: list) -> list:
    nytt_element = 10
    kopia = lista[:]
    kopia.append(nytt_element)
    return kopia

siffror = [1, 2, 3]
siffror2 = lagg_till_element(siffror)

print("Ursprunglig lista:", siffror)
print("Ny lista:", siffror2)
Exempelutskrift

Ursprunglig lista: [1, 2, 3] Ny lista: [1, 2, 3, 10]

Om du inte är helt säker på vad som händer i en kodsnutt, kan det löna sig att utnyttja visualiseringsverktyget.

Ändra på en lista som getts som argument

Vi försöker skapa en funktion som ökar på varje element i en lista med tio:

def oka_pa_alla(lista: list):
    ny_lista = []
    for element in lista:
        ny_lista.append(element + 10)
    lista = ny_lista

tal = [1, 2, 3]
print("start:",tal)
oka_pa_alla(tal)
print("efter funktionen:", tal)
Exempelutskrift

start: [1, 2, 3] efter funktionen: [1, 2, 3]

Av någon orsak fungerar funktionen inte. Varför det?

Funktionen tar en referens till en lista som argument. Det här är lagrat i variabeln min_lista. Tilldelningen min_lista = ny_lista tilldelar ett nytt värde till den samma variabeln. Variabeln min_lista hänvisar nu till den nya listan som skapades i funktionen, vilket betyder att referensen till den ursprungliga listan inte längre är tillgänglig inom funktionen. Tilldelningen har dock inte någon påverkan utanför funktionen.

5 2 6

Dessutom innehåller variabeln ny_lista nu de nya värdena, men de är inte tillgängliga utanför funktionen. Variabeln försvinner alltså när funktionen körts och programmet fortsätter tillbaka till huvudfunktionen. Variabeln tal i huvudfunktionen hänvisar alltid till den ursprungliga listan.

Visualiseringsverktyget hjälper igen. När du går igenom stegen utförligt märker du att den ursprungliga listan inte påverkas av funktionen på något sätt alls:

5 2 4d

Ett enkelt sätt att korrigera problemet är att kopiera över alla element från den nya listan till den gamla:

def oka_pa_alla(lista: list):
    ny_lista = []
    for element in lista:
        ny_lista.append(element + 10)

    # vi kopierar de nya värdena till den gamla listan
    for i in range(len(lista)):
        lista[i] = ny_lista[i]

Eller lite enklare tack vare Python:

>>> lista = [1, 2, 3, 4]
>>> lista[1:3] = [10, 20]
>>> lista
[1, 10, 20, 4]

I exemplet ovan ersätts en del av listan med värden från en annan lista.

Vi kan naturligtvis också göra detta för en hel lista:

>>> lista = [1, 2, 3, 4]
>>> lista[:] = [100, 99, 98, 97]
>>> lista
[100, 99, 98, 97]

Allt innehåll i den gamla listan ersätts. Inspirerat av det här har vi nu skapat en fungerande version av funktionen som ökar på elementens värden:

def oka_pa_alla(lista: list):
    ny_lista = []
    for element in lista:
        ny_lista.append(element + 10)

    lista[:] = ny_lista

Egentligen finns det ingen orsak till att skapa en ny lista inom funktionen. Vi kan helt enkelt tilldela värdena direkt till den ursprungliga listan:

def oka_pa_alla(lista: list):
    for i in range(len(lista)):
        lista[i] += 10
Loading
Loading
Loading
Loading
Loading
Loading

Funktioners sidoeffekter

Som vi sett ovan, kan en funktion som tar emot en referens till en lista som argument ändra på listan. Om programmeraren inte har tagit i beaktande att listan kan komma att ändras inne i funktionen, kan detta förorsaka problem på andra håll i programmet.

Låt oss ta en titt på en funktion som borde hitta det nästminsta värdet i en lista:

def nast_minst(lista: list) -> int:
    # i en sorterad lista finns det nästminsta elementet på index 1
    lista.sort()
    return lista[1]

tal = [1, 4, 2, 5, 3, 6, 4, 7]
print(nast_minst(tal))
print(tal)
Exempelutskrift
2 [1, 2, 3, 4, 4, 5, 6, 7]

Funktionen hittar det nästminsta värdet, men sorterar dessutom listan. Om elementens ordning har betydelse på andra håll i programmet kommer det här funktionsanropet eventuellt att orsaka fel. Oplanerade modifikationer som görs hos objekt som ges som referens till en funktion kallas sidoeffekter.

Vi kan förhindra den här sidoeffekten genom att göra en liten ändring i funktionen:

def nast_minst(lista: list) -> int:
    kopia = sorted(lista)
    return kopia[1]

tal = [1, 4, 2, 5, 3, 6, 4, 7]
print(nast_minst(tal))
print(tal)
Exempelutskrift

2 [1, 4, 2, 5, 3, 6, 4, 7]

Funktionen sorted returnerar en ny sorterad kopia av listan, så vi behöver inte mera "sabotera" den ursprungliga listan när vi söker efter det näst minsta värdet.

Det är en god vana att försöka undvika sidoeffekter i funktioner. Sidoeffekter kan göra det svårare att säkerställa att programmet fungerar som det ska i alla situationer.

Funktioner som saknar sidoeffekter kallas rena funktioner. Då man arbetar med funktionell programmering är rena funktioner speciellt viktiga. Vi kommer se närmare på det under fortsättningskursen i programmering.

Loading...
:
Loading...

Log in to view the quiz

Du har nått slutet av den här delen! Fortsätt till nästa del:

Se dina poäng genom att klicka på cirkeln nere till höger av sidan.