Perché una funzione può modificare alcuni argomenti come percepiti dal chiamante, ma non altri?


182

Sto cercando di capire l'approccio di Python all'ambito variabile. In questo esempio, perché è in f()grado di modificare il valore di x, come percepito all'interno main(), ma non il valore di n?

def f(n, x):
    n = 2
    x.append(4)
    print('In f():', n, x)

def main():
    n = 1
    x = [0,1,2,3]
    print('Before:', n, x)
    f(n, x)
    print('After: ', n, x)

main()

Produzione:

Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]

Risposte:


212

Alcune risposte contengono la parola "copia" in un contesto di una chiamata di funzione. Lo trovo confuso.

Python non copia gli oggetti si passa nel corso di una chiamata di funzione sempre .

I parametri di funzione sono nomi . Quando chiamate una funzione, Python associa questi parametri a qualunque oggetto passi (tramite nomi nell'ambito di un chiamante).

Gli oggetti possono essere mutabili (come liste) o immutabili (come numeri interi, stringhe in Python). Oggetto mutevole che puoi cambiare. Non puoi cambiare un nome, puoi semplicemente legarlo a un altro oggetto.

Il tuo esempio non riguarda gli ambiti o gli spazi dei nomi , riguarda la denominazione, l'associazione e la mutabilità di un oggetto in Python.

def f(n, x): # these `n`, `x` have nothing to do with `n` and `x` from main()
    n = 2    # put `n` label on `2` balloon
    x.append(4) # call `append` method of whatever object `x` is referring to.
    print('In f():', n, x)
    x = []   # put `x` label on `[]` ballon
    # x = [] has no effect on the original list that is passed into the function

Ecco delle belle foto sulla differenza tra variabili in altre lingue e nomi in Python .


3
Questo articolo mi ha aiutato a capire meglio il problema e suggerisce una soluzione alternativa e alcuni usi avanzati: Valori dei parametri predefiniti in Python
Gfy,

@Gfy, ho visto esempi simili prima, ma per me non descrive una situazione del mondo reale. Se stai modificando qualcosa che è passato, non ha senso impostarlo come predefinito.
Mark Ransom,

@MarkRansom, penso che ha senso se si desidera fornire destinazione di uscita opzionale come in: def foo(x, l=None): l=l or []; l.append(x**2); return l[-1].
Janusz Lenar,

Per l'ultima riga del codice di Sebastian, diceva "# quanto sopra non ha alcun effetto sull'elenco originale". Ma a mio avviso, non ha alcun effetto su "n", ma ha cambiato la "x" nella funzione main (). Ho ragione?
user17670,

1
@ user17670: x = []in f()non ha alcun effetto sull'elenco xnella funzione principale. Ho aggiornato il commento, per renderlo più specifico.
jfs,

15

Hai già un numero di risposte, e sono ampiamente d'accordo con JF Sebastian, ma potresti trovare questo utile come scorciatoia:

Ogni volta che vedi varname =, stai creando un nuovo nome associato nell'ambito della funzione. Qualunque valore varnameprima era vincolato viene perso in questo ambito .

Ogni volta che vedi varname.foo()che stai chiamando un metodo varname. Il metodo può modificare varname (ad es list.append.). varname(o meglio l'oggetto chevarname nomina) può esistere in più di un ambito, e poiché è lo stesso oggetto, qualsiasi modifica sarà visibile in tutti gli ambiti.

[nota che la globalparola chiave crea un'eccezione al primo caso]


13

fin realtà non altera il valore di x(che è sempre lo stesso riferimento a un'istanza di un elenco). Piuttosto, altera il contenuto di questo elenco.

In entrambi i casi, una copia di un riferimento viene passata alla funzione. All'interno della funzione,

  • nviene assegnato un nuovo valore. Viene modificato solo il riferimento all'interno della funzione, non quello esterno.
  • xnon viene assegnato un nuovo valore: né il riferimento all'interno né all'esterno della funzione viene modificato. Al contrario, xil valore viene modificato.

Poiché sia ​​la xfunzione interna che quella esterna si riferiscono allo stesso valore, entrambi vedono la modifica. Al contrario, l' ninterno della funzione e l'esterno si riferiscono a valori diversi dopo che è nstato riassegnato all'interno della funzione.


8
"copia" è fuorviante. Python non ha variabili come C. Tutti i nomi in Python sono riferimenti. Non puoi modificare il nome, puoi semplicemente associarlo a un altro oggetto, tutto qui. Ha senso parlare di oggetti mutabili e immutabili in Python, non sono nomi.
jfs,

1
@JF Sebastian: La tua affermazione è fuorviante al massimo. Non è utile pensare ai numeri come riferimenti.
Pitarou,

9
@dysfunctor: i numeri sono riferimenti a oggetti immutabili. Se preferisci pensarli in qualche altro modo, hai un sacco di strani casi speciali da spiegare. Se li consideri immutabili, non ci sono casi speciali.
S. Lott,

@ S.Lott: Indipendentemente da ciò che accade sotto il cofano, Guido van Rossum ha fatto molti sforzi per progettare Python in modo che il programmatore possa considerare i numeri come solo ... numeri.
Pitarou,

1
@JF, il riferimento viene copiato.
habnabit,

7

Rinominerò le variabili per ridurre la confusione. n -> nf o nmain . x -> xf o xmain :

def f(nf, xf):
    nf = 2
    xf.append(4)
    print 'In f():', nf, xf

def main():
    nmain = 1
    xmain = [0,1,2,3]
    print 'Before:', nmain, xmain
    f(nmain, xmain)
    print 'After: ', nmain, xmain

main()

Quando chiamate la funzione f , il runtime di Python crea una copia di xmain e la assegna a xf , e allo stesso modo assegna una copia di nmain a nf .

Nel caso di n , il valore che viene copiato è 1.

Nel caso di x il valore che viene copiato non è l'elenco letterale [0, 1, 2, 3] . È un riferimento a tale elenco. xf e xmain indicano lo stesso elenco, quindi quando si modifica xf si modifica anche xmain .

Se, tuttavia, dovessi scrivere qualcosa del tipo:

    xf = ["foo", "bar"]
    xf.append(4)

scopriresti che xmain non è cambiato. Questo perché, nella riga xf = ["foo", "bar"] hai cambiato xf per puntare a un nuovo elenco. Qualsiasi modifica apportata a questo nuovo elenco non avrà alcun effetto sull'elenco a cui punta ancora xmain .

Spero che aiuti. :-)


2
"Nel caso di n, il valore che viene copiato ..." - Questo è sbagliato, qui non viene eseguita alcuna copia (a meno che non si contino i riferimenti). Invece, python usa "nomi" che puntano agli oggetti reali. nf e xf indicano nmain e xmain, fino a nf = 2quando il nome nfviene cambiato in punto 2. I numeri sono immutabili, gli elenchi sono mutabili.
Casey Kuball,

2

È perché un elenco è un oggetto mutabile. Non stai impostando x sul valore di [0,1,2,3], stai definendo un'etichetta per l'oggetto [0,1,2,3].

Dovresti dichiarare la tua funzione f () in questo modo:

def f(n, x=None):
    if x is None:
        x = []
    ...

3
Non ha nulla a che fare con la mutabilità. Se lo facessi x = x + [4]invece di x.append(4), non vedresti alcun cambiamento nel chiamante anche se un elenco è mutabile. Ha a che fare con se è effettivamente mutato.
glglgl

1
OTOH, se lo fai x += [4]allora xè mutato, proprio come succede x.append(4), quindi il chiamante vedrà il cambiamento.
PM 2Ring

2

n è un int (immutabile) e una copia viene passata alla funzione, quindi nella funzione si sta modificando la copia.

X è un elenco (modificabile) e viene passata una copia del puntatore della funzione in modo che x.append (4) cambi il contenuto dell'elenco. Tuttavia, hai detto x = [0,1,2,3,4] nella tua funzione, non cambieresti il ​​contenuto di x in main ().


3
Guarda la frase "copia del puntatore". Entrambi i luoghi ottengono riferimenti agli oggetti. n è un riferimento a un oggetto immutabile; x è un riferimento a un oggetto mutabile.
S. Lott,

2

Se le funzioni vengono riscritte con variabili completamente diverse e le chiamiamo id , illustra bene il punto. All'inizio non ho capito e ho letto il post di jfs con la grande spiegazione , quindi ho cercato di capire / convincermi:

def f(y, z):
    y = 2
    z.append(4)
    print ('In f():             ', id(y), id(z))

def main():
    n = 1
    x = [0,1,2,3]
    print ('Before in main:', n, x,id(n),id(x))
    f(n, x)
    print ('After in main:', n, x,id(n),id(x))

main()
Before in main: 1 [0, 1, 2, 3]   94635800628352 139808499830024
In f():                          94635800628384 139808499830024
After in main: 1 [0, 1, 2, 3, 4] 94635800628352 139808499830024

z e x hanno lo stesso id. Solo tag diversi per la stessa struttura sottostante come dice l'articolo.


0

Python è un linguaggio pass-by-value puro se ci pensi nel modo giusto. Una variabile python memorizza la posizione di un oggetto in memoria. La variabile Python non memorizza l'oggetto stesso. Quando si passa una variabile a una funzione, si passa una copia dell'indirizzo dell'oggetto a cui punta la variabile.

Contrasta queste due funzioni

def foo(x):
    x[0] = 5

def goo(x):
    x = []

Ora, quando si digita nella shell

>>> cow = [3,4,5]
>>> foo(cow)
>>> cow
[5,4,5]

Confronta questo con goo.

>>> cow = [3,4,5]
>>> goo(cow)
>>> goo
[3,4,5]

Nel primo caso, passiamo una copia all'indirizzo della mucca a foo e foo ha modificato lo stato dell'oggetto che vi risiede. L'oggetto viene modificato.

Nel secondo caso si passa una copia dell'indirizzo di vacca a goo. Quindi goo procede a modificare quella copia. Effetto: nessuno.

Io chiamo questo il principio della casa rosa . Se fai una copia del tuo indirizzo e dici a un pittore di dipingere la casa a quell'indirizzo rosa, finirai con una casa rosa. Se dai al pittore una copia del tuo indirizzo e gli dici di cambiarlo in un nuovo indirizzo, l'indirizzo della tua casa non cambia.

La spiegazione elimina molta confusione. Python passa le variabili degli indirizzi memorizzate per valore.


Un puro passaggio per valore del puntatore non è molto diverso da un passaggio per riferimento se ci pensate nel modo giusto ...
galinette,

Guarda goo. Se tu fossi un puro passaggio per riferimento, avrebbe cambiato argomento. No, Python non è un linguaggio pass-by-reference puro. Passa riferimenti per valore.
ncmathsadist,

0

Python è una copia per valore di riferimento. Un oggetto occupa un campo in memoria e un riferimento è associato a quell'oggetto, ma esso stesso occupa un campo in memoria. E il nome / valore è associato a un riferimento. Nella funzione python, copia sempre il valore del riferimento, quindi nel tuo codice, n viene copiato per essere un nuovo nome, quando lo assegni, ha un nuovo spazio nello stack del chiamante. Ma per l'elenco, anche il nome è stato copiato, ma si riferisce alla stessa memoria (poiché non si assegna mai un nuovo valore all'elenco). Questa è una magia in pitone!


0

La mia comprensione generale è che qualsiasi variabile oggetto (come un elenco o un dict, tra gli altri) può essere modificata attraverso le sue funzioni. Ciò che credo non sia in grado di fare è riassegnare il parametro, ovvero assegnarlo per riferimento all'interno di una funzione richiamabile.

Ciò è coerente con molte altre lingue.

Esegui il seguente breve script per vedere come funziona:

def func1(x, l1):
    x = 5
    l1.append("nonsense")

y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)

-3

Avevo modificato la mia risposta tonnellate di volte e mi sono reso conto che non dovevo dire nulla, si era già spiegato Python.

a = 'string'
a.replace('t', '_')
print(a)
>>> 'string'

a = a.replace('t', '_')
print(a)
>>> 's_ring'

b = 100
b + 1
print(b)
>>> 100

b = b + 1
print(b)
>>> 101

def test_id(arg):
    c = id(arg)
    arg = 123
    d = id(arg)
    return

a = 'test ids'
b = id(a)
test_id(a)
e = id(a)

# b = c  = e != d
# this function do change original value
del change_like_mutable(arg):
    arg.append(1)
    arg.insert(0, 9)
    arg.remove(2)
    return

test_1 = [1, 2, 3]
change_like_mutable(test_1)



# this function doesn't 
def wont_change_like_str(arg):
     arg = [1, 2, 3]
     return


test_2 = [1, 1, 1]
wont_change_like_str(test_2)
print("Doesn't change like a imutable", test_2)

Questo diavolo non è il riferimento / valore / mutabile o no / istanza, spazio dei nomi o variabile / lista o str, È LA SINTASSI, SEGNO UGUALE.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.