Python Language
Mutable vs Immutable (and Hashable) i Python
Sök…
Muterbar vs oföränderlig
Det finns två slags typer i Python. Oändliga typer och muterbara typer.
Immutables
Ett objekt av obestämbar typ kan inte ändras. Varje försök att modifiera objektet kommer att resultera i att en kopia skapas.
Denna kategori inkluderar heltal, flottörer, komplex, strängar, byte, tupler, intervall och frusensets.
För att markera den här egenskapen, låt oss spela med id
inbyggd. Denna funktion returnerar den unika identifieraren för objektet som skickas som parameter. Om id är densamma är det samma objekt. Om det ändras är detta ett annat objekt. (Vissa säger att detta faktiskt är minnets adress för objektet, men se upp för dem, de kommer från den mörka sidan av styrkan ...)
>>> a = 1
>>> id(a)
140128142243264
>>> a += 2
>>> a
3
>>> id(a)
140128142243328
Okej, 1 är inte 3 ... Nyheter ... Kanske inte. Men detta beteende glöms ofta när det gäller mer komplexa typer, särskilt strängar.
>>> stack = "Overflow"
>>> stack
'Overflow'
>>> id(stack)
140128123955504
>>> stack += " rocks!"
>>> stack
'Overflow rocks!'
A ha! Ser? Vi kan ändra det!
>>> id(stack)
140128123911472
Nej. Även om det verkar som om vi kan ändra strängen som namnges av den variabla stack
, skapar vi ett nytt objekt för att innehålla resultatet av sammankopplingen. Vi luras för att i processen går det gamla objektet ingenstans, så det förstörs. I en annan situation skulle det ha varit mer uppenbart:
>>> stack = "Stack"
>>> stackoverflow = stack + "Overflow"
>>> id(stack)
140128069348184
>>> id(stackoverflow)
140128123911480
I det här fallet är det uppenbart att om vi vill behålla den första strängen, behöver vi en kopia. Men är det så uppenbart för andra typer?
Övning
När du vet hur en oföränderlig typ fungerar, vad skulle du säga med koden nedan? Är det klokt?
s = ""
for i in range(1, 1000):
s += str(i)
s += ","
Mutables
Ett objekt av en muterbar typ kan ändras och det ändras in-situ . Inga implicita kopior görs.
Denna kategori inkluderar: listor, ordböcker, bytrådar och uppsättningar.
Låt oss fortsätta spela med vår lilla id
funktion.
>>> 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
(Som sidoanteckning använder jag byte som innehåller ascii-data för att göra min poäng klar, men kom ihåg att byte inte är utformade för att innehålla textdata. Kan kraften förlåta mig.)
Vad har vi? Vi skapar en bytray, modifierar den och använder id
, vi kan se till att detta är samma objekt, modifierat. Inte en kopia av det.
Naturligtvis, om ett objekt kommer att modifieras ofta, gör en muterbar typ ett mycket bättre jobb än en immutable typ. Tyvärr glöms ofta verkligheten för den här egenskapen när det gör ont.
>>> c = b
>>> c += b' rocks!'
>>> c
bytearray(b'StackOverflow rocks!')
Okej...
>>> b
bytearray(b'StackOverflow rocks!')
Waiiit en sekund ...
>>> id(c) == id(b)
True
Verkligen. c
är inte en kopia av b
. c
är b
.
Övning
Nu kan du bättre förstå vilken bieffekt som impliceras av en muterbar typ, kan du förklara vad som går fel i det här exemplet?
>>> 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...
Muterbart och oföränderligt som argument
Ett av de viktigaste användningsfallen när en utvecklare måste ta hänsyn till mutabilitet är när man överför argument till en funktion. Detta är mycket viktigt, eftersom detta kommer att avgöra funktionens förmåga att modifiera objekt som inte hör till dess omfattning, eller med andra ord om funktionen har biverkningar. Detta är också viktigt för att förstå var resultatet av en funktion måste göras tillgängligt.
>>> 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]
Här är misstaget att tro att lin
, som en parameter för funktionen, kan modifieras lokalt. I stället lin
och a
referens till samma objekt. Eftersom detta objekt är muterbara görs modifieringen på plats, vilket innebär att objektet som refereras av både lin
och a
modifieras. lin
behöver inte riktigt returneras, eftersom vi redan har en referens till detta objekt i form av a
. a
och b
slutet refererar till samma objekt.
Detta gäller inte samma sak för tuple.
>>> 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)
I början av funktionen, tin
och a
referens samma objekt. Men detta är ett oföränderligt objekt. Så när funktionen försöker modifiera den får tin
ett nytt objekt med modifieringen, medan a
håller en referens till det ursprungliga objektet. I detta fall åter tin
är obligatorisk, eller det nya objektet skulle gå förlorad.
Övning
>>> def yoda(prologue, sentence):
sentence.reverse()
prologue += " ".join(sentence)
return prologue
>>> focused = ["You must", "stay focused"]
>>> saying = "Yoda said: "
>>> yoda_sentence = yoda(saying, focused)
Obs: reverse
fungerar på plats.
Vad tycker du om den här funktionen? Har det biverkningar? Är returen nödvändig? Vad är värdet av att saying
efter samtalet? Av focused
? Vad händer om funktionen anropas igen med samma parametrar?