Python Language
Mutable versus Immutable (en Hashable) in Python
Zoeken…
Veranderlijk versus onveranderlijk
Er zijn twee soorten typen in Python. Onveranderlijke typen en veranderlijke typen.
Immutables
Een object van een onveranderlijk type kan niet worden gewijzigd. Elke poging om het object te wijzigen zal resulteren in een kopie.
Deze categorie omvat: gehele getallen, drijvers, complex, tekenreeksen, bytes, tupels, bereiken en frozensets.
Laten we, om deze eigenschap te markeren, spelen met de ingebouwde id
. Deze functie retourneert de unieke identificatie van het object dat als parameter is doorgegeven. Als de id hetzelfde is, is dit hetzelfde object. Als het verandert, is dit een ander object. (Sommigen zeggen dat dit eigenlijk het geheugenadres van het object is, maar pas op voor hen, ze komen van de donkere kant van de kracht ...)
>>> a = 1
>>> id(a)
140128142243264
>>> a += 2
>>> a
3
>>> id(a)
140128142243328
Oké, 1 is geen 3 ... Nieuws, misschien niet. Dit gedrag wordt echter vaak vergeten als het gaat om complexere typen, met name tekenreeksen.
>>> stack = "Overflow"
>>> stack
'Overflow'
>>> id(stack)
140128123955504
>>> stack += " rocks!"
>>> stack
'Overflow rocks!'
Aha! Zien? We kunnen het wijzigen!
>>> id(stack)
140128123911472
Nee. Hoewel het lijkt alsof we de tekenreeks met de variabele stack
kunnen wijzigen, maken we eigenlijk een nieuw object dat het resultaat van de aaneenschakeling bevat. We worden voor de gek gehouden omdat het oude object nergens heen gaat, dus het wordt vernietigd. In een andere situatie zou dat duidelijker zijn geweest:
>>> stack = "Stack"
>>> stackoverflow = stack + "Overflow"
>>> id(stack)
140128069348184
>>> id(stackoverflow)
140128123911480
In dit geval is het duidelijk dat als we de eerste string willen behouden, we een kopie nodig hebben. Maar is dat zo duidelijk voor andere types?
Oefening
Nu, wetende hoe een onveranderlijk type werkt, wat zou je zeggen met het onderstaande stukje code? Is het wijs?
s = ""
for i in range(1, 1000):
s += str(i)
s += ","
Mutables
Een object van een veranderlijk type kan worden gewijzigd en het wordt ter plaatse gewijzigd. Er worden geen impliciete kopieën gemaakt.
Deze categorie omvat: lijsten, woordenboeken, bytearrays en sets.
Laten we doorgaan met spelen met onze kleine id
functie.
>>> b = bytearray(b'Stack')
>>> b
bytearray(b'Stack')
>>> b = bytearray(b'Stack')
>>> id(b)
140128030688288
>>> b += b'Overflow'
>>> b
bytearray(b'StackOverflow')
>>> id(b)
140128030688288
(Als een kanttekening gebruik ik bytes die ascii-gegevens bevatten om mijn punt duidelijk te maken, maar onthoud dat bytes niet zijn ontworpen om tekstuele gegevens te bevatten. Moge de kracht me vergeven.)
Wat hebben we? We maken een bytearray, wijzigen deze en met behulp van de id
kunnen we ervoor zorgen dat dit hetzelfde object is, gewijzigd. Geen kopie ervan.
Natuurlijk, als een object vaak wordt aangepast, doet een veranderlijk type het veel beter dan een onveranderlijk type. Helaas wordt de realiteit van dit pand vaak vergeten wanneer het het meest pijn doet.
>>> c = b
>>> c += b' rocks!'
>>> c
bytearray(b'StackOverflow rocks!')
Oke...
>>> b
bytearray(b'StackOverflow rocks!')
Nog een seconde ...
>>> id(c) == id(b)
True
Inderdaad. c
is geen kopie van b
. c
is b
.
Oefening
Nu begrijp je beter welke bijwerking wordt geïmpliceerd door een veranderlijk type, kun je uitleggen wat er misgaat in dit voorbeeld?
>>> ll = [ [] ]*4 # Create a list of 4 lists to contain our results
>>> ll
[[], [], [], []]
>>> ll[0].append(23) # Add result 23 to first list
>>> ll
[[23], [23], [23], [23]]
>>> # Oops...
Veranderlijk en onveranderlijk als argumenten
Een van de belangrijkste gevallen waarin een ontwikkelaar rekening moet houden met veranderbaarheid, is het doorgeven van argumenten aan een functie. Dit is erg belangrijk, omdat dit de mogelijkheid voor de functie bepaalt om objecten te wijzigen die niet tot het bereik behoren, of met andere woorden als de functie bijwerkingen heeft. Dit is ook belangrijk om te begrijpen waar het resultaat van een functie beschikbaar moet worden gemaakt.
>>> def list_add3(lin):
lin += [3]
return lin
>>> a = [1, 2, 3]
>>> b = list_add3(a)
>>> b
[1, 2, 3, 3]
>>> a
[1, 2, 3, 3]
Hier is de fout om te denken dat lin
, als parameter voor de functie, lokaal kan worden gewijzigd. In plaats daarvan, lin
en a
verwijzing naar hetzelfde object. Omdat dit object kan worden gewijzigd, wordt de wijziging ter plekke uitgevoerd, wat betekent dat het object waarnaar wordt verwezen door zowel lin
als a
wordt gewijzigd. lin
hoeft niet echt te worden geretourneerd, omdat we al een verwijzing naar dit object hebben in de vorm van a
. a
en b
eindigen op hetzelfde object.
Dit gaat niet hetzelfde voor tupels.
>>> def tuple_add3(tin):
tin += (3,)
return tin
>>> a = (1, 2, 3)
>>> b = tuple_add3(a)
>>> b
(1, 2, 3, 3)
>>> a
(1, 2, 3)
Aan het begin van de functie, tin
en a
verwijzing naar hetzelfde object. Maar dit is een onveranderlijk object. Dus wanneer de functie probeert om het te wijzigen, tin
ontvangt een nieuw object met de wijziging, terwijl a
houdt een verwijzing naar de oorspronkelijke object. In dit geval is het retourneren van tin
verplicht, anders gaat het nieuwe object verloren.
Oefening
>>> def yoda(prologue, sentence):
sentence.reverse()
prologue += " ".join(sentence)
return prologue
>>> focused = ["You must", "stay focused"]
>>> saying = "Yoda said: "
>>> yoda_sentence = yoda(saying, focused)
Opmerking: reverse
werkt op zijn plaats.
Wat vindt u van deze functie? Heeft het bijwerkingen? Is de terugkeer noodzakelijk? Wat is de waarde van het saying
na het telefoontje? Van focused
? Wat gebeurt er als de functie opnieuw wordt opgeroepen met dezelfde parameters?