¿Por qué una función puede modificar algunos argumentos como los percibe la persona que llama, pero no otros?

182

Estoy tratando de entender el enfoque de Python para el alcance variable. En este ejemplo, ¿por qué es f()capaz de alterar el valor de x, como se percibe dentro main(), pero no el valor de 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()

Salida:

Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]
FMc
fuente
77
bien explicado aquí nedbatchelder.com/text/names.html
Roushan

Respuestas:

212

Algunas respuestas contienen la palabra "copiar" en el contexto de una llamada de función. Lo encuentro confuso.

Python no copia los objetos que pasas durante una llamada a la función cada vez .

Los parámetros de la función son nombres . Cuando llama a una función, Python vincula estos parámetros a cualquier objeto que pase (a través de nombres en un ámbito de llamada).

Los objetos pueden ser mutables (como listas) o inmutables (como enteros, cadenas en Python). Objeto mutable que puedes cambiar. No puede cambiar un nombre, solo puede vincularlo a otro objeto.

Su ejemplo no se trata de ámbitos o espacios de nombres , se trata de nombrar, enlazar y mutabilidad de un objeto en 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

Aquí hay buenas fotos sobre la diferencia entre variables en otros idiomas y nombres en Python .

jfs
fuente
3
Este artículo me ayudó a comprender mejor el problema y sugiere una solución alternativa y algunos usos avanzados: Valores de parámetros predeterminados en Python
Gfy
@Gfy, he visto ejemplos similares antes, pero para mí no describe una situación del mundo real. Si está modificando algo que se pasa, no tiene sentido darle un valor predeterminado.
Mark Ransom
@MarkRansom, yo creo que sí tiene sentido si desea proporcionar destino de salida opcional como en: def foo(x, l=None): l=l or []; l.append(x**2); return l[-1].
Janusz Lenar
Para la última línea del código de Sebastian, decía "# lo anterior no tiene ningún efecto en la lista original". Pero en mi opinión, solo no tiene efecto en "n", sino que cambió la "x" en la función main (). ¿Estoy en lo correcto?
user17670
1
@ user17670: x = []en f()no tiene efecto en la lista xen la función principal. He actualizado el comentario, para hacerlo más específico.
jfs
15

Ya tiene varias respuestas, y estoy ampliamente de acuerdo con JF Sebastian, pero puede encontrar esto útil como acceso directo:

Cada vez que veas varname =, estás creando un nuevo enlace de nombre dentro del alcance de la función. Cualquier valor al que varnamese haya vinculado antes se pierde dentro de este alcance .

Cada vez que ves varname.foo()estás llamando a un método varname. El método puede alterar varname (por ejemplo list.append). varname(o, más bien, el objeto que varnamenombra) puede existir en más de un ámbito, y dado que es el mismo objeto, cualquier cambio será visible en todos los ámbitos.

[tenga en cuenta que la globalpalabra clave crea una excepción al primer caso]

John Fouhy
fuente
13

fen realidad no altera el valor de x(que siempre es la misma referencia a una instancia de una lista). Más bien, altera el contenido de esta lista.

En ambos casos, se pasa una copia de una referencia a la función. Dentro de la función,

  • nse le asigna un nuevo valor. Solo se modifica la referencia dentro de la función, no la que está fuera de ella.
  • xno se le asigna un nuevo valor: ni la referencia dentro ni fuera de la función se modifican. En cambio, xel valor de se modifica.

Dado que tanto el xinterior de la función como el exterior se refieren al mismo valor, ambos ven la modificación. Por el contrario, el ninterior de la función y el exterior se refieren a diferentes valores después de que nse reasignó dentro de la función.

Konrad Rudolph
fuente
8
"copiar" es engañoso. Python no tiene variables como C. Todos los nombres en Python son referencias. No puede modificar el nombre, solo puede vincularlo a otro objeto, eso es todo. Solo tiene sentido hablar de objetos mutables e inmutables en Python, no son nombres.
jfs
1
@JF Sebastian: Tu declaración es engañosa en el mejor de los casos. No es útil pensar en los números como referencias.
Pitarou
9
@dysfunctor: los números son referencias a objetos inmutables. Si prefiere pensar en ellos de otra manera, tiene un montón de casos especiales extraños para explicar. Si los considera inmutables, no hay casos especiales.
S.Lott
@ S.Lott: Independientemente de lo que ocurra debajo del capó, Guido van Rossum se esforzó mucho en diseñar Python para que el programador pueda considerar los números como simplemente ... números.
Pitarou
1
@JF, la referencia se copia.
habnabit
7

Cambiaré el nombre de las variables para reducir la confusión. 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()

Cuando llama a la función f , el tiempo de ejecución de Python hace una copia de xmain y la asigna a xf , y de manera similar asigna una copia de nmain a nf .

En el caso de n , el valor que se copia es 1.

En el caso de x, el valor que se copia no es la lista literal [0, 1, 2, 3] . Es una referencia a esa lista. xf y xmain apuntan a la misma lista, por lo que cuando modifica xf también modifica xmain .

Sin embargo, si escribieras algo como:

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

encontrará que xmain no ha cambiado. Esto se debe a que, en la línea xf = ["foo", "bar"] , debe cambiar xf para que apunte a una nueva lista. Cualquier cambio que realice en esta nueva lista no tendrá efectos en la lista a la que xmain todavía apunta.

Espero que ayude. :-)

Pitarou
fuente
2
"En el caso de n, el valor que se copia ..." - Esto está mal, no se realiza ninguna copia aquí (a menos que cuente referencias). En cambio, python usa 'nombres' que apuntan a los objetos reales. nf y xf apuntan a nmain y xmain, hasta nf = 2, donde nfse cambia el nombre para señalar 2. Los números son inmutables, las listas son mutables.
Casey Kuball
2

Es porque una lista es un objeto mutable. No está configurando x al valor de [0,1,2,3], está definiendo una etiqueta para el objeto [0,1,2,3].

Debería declarar su función f () así:

def f(n, x=None):
    if x is None:
        x = []
    ...
Luiz Damim
fuente
3
No tiene nada que ver con la mutabilidad. Si lo hiciera en x = x + [4]lugar de x.append(4), no vería ningún cambio en la persona que llama, aunque una lista es mutable. Tiene que ver con si realmente está mutado.
glglgl
1
OTOH, si lo hace, x += [4]entonces xestá mutado, al igual que lo que sucede x.append(4), por lo que la persona que llama verá el cambio.
PM 2Ring
2

n es un int (inmutable) y se pasa una copia a la función, por lo que en la función está cambiando la copia.

X es una lista (mutable), y se pasa una copia del puntero a la función, por lo que x.append (4) cambia el contenido de la lista. Sin embargo, usted dijo que x = [0,1,2,3,4] en su función, no cambiaría el contenido de x en main ().

Jason Coon
fuente
3
Mira la frase "copia del puntero". Ambos lugares obtienen referencias a los objetos. n es una referencia a un objeto inmutable; x es una referencia a un objeto mutable.
S.Lott
2

Si las funciones se reescriben con variables completamente diferentes y llamamos a id en ellas, entonces ilustra bien el punto. Al principio no entendí esto y leí la publicación de jfs con la gran explicación , así que traté de entenderme / convencerme a mí mismo:

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 y x tienen la misma identificación. Solo etiquetas diferentes para la misma estructura subyacente que dice el artículo.

jouell
fuente
0

Python es un lenguaje puro de paso por valor si lo piensa de la manera correcta. Una variable python almacena la ubicación de un objeto en la memoria. La variable Python no almacena el objeto en sí. Cuando pasa una variable a una función, está pasando una copia de la dirección del objeto al que apunta la variable.

Contrasta estas dos funciones

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

def goo(x):
    x = []

Ahora, cuando escribes en el shell

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

Compara esto con goo.

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

En el primer caso, pasamos una copia de la dirección de vaca a foo y foo modificó el estado del objeto que reside allí. El objeto se modifica.

En el segundo caso, pasa una copia de la dirección de vaca a goo. Entonces goo procede a cambiar esa copia. Efecto: ninguno.

Yo llamo a esto el principio de la casa rosa . Si haces una copia de tu dirección y le dices a un pintor que pinte la casa en esa dirección de rosa, terminarás con una casa rosa. Si le da al pintor una copia de su dirección y le dice que la cambie a una nueva dirección, la dirección de su casa no cambia.

La explicación elimina mucha confusión. Python pasa las direcciones de las variables almacenadas por valor.

ncmathsadist
fuente
Un pase pura por valor de puntero no es muy diferente de un pase por referencia si se piensa en la manera correcta ...
galinette
Mira bien. Si fuera pura referencia, habría cambiado su argumento. No, Python no es un lenguaje puro de referencia. Pasa referencias por valor.
ncmathsadist
0

Python es copia por valor de referencia. Un objeto ocupa un campo en la memoria, y una referencia está asociada con ese objeto, pero en sí mismo ocupa un campo en la memoria. Y el nombre / valor está asociado con una referencia. En la función python, siempre copia el valor de la referencia, por lo que en su código, n se copia para que sea un nombre nuevo, cuando lo asigna, tiene un nuevo espacio en la pila de llamadas. Pero para la lista, el nombre también se copió, pero se refiere a la misma memoria (ya que nunca asigna un nuevo valor a la lista). ¡Eso es una magia en python!

sunxd
fuente
0

Mi comprensión general es que cualquier variable de objeto (como una lista o un dict, entre otros) se puede modificar a través de sus funciones. Lo que creo que no puede hacer es reasignar el parámetro, es decir, asignarlo por referencia dentro de una función invocable.

Eso es consistente con muchos otros idiomas.

Ejecute el siguiente script corto para ver cómo funciona:

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

y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)
Boris Epstein
fuente
-3

Había modificado mi respuesta toneladas de veces y me di cuenta de que no tenía que decir nada, Python ya se había explicado.

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)

Este demonio no es la referencia / valor / mutable o no / instancia, espacio de nombres o variable / lista o str, ES LA SINTAXIS, SIGNO IGUAL.

Capacho
fuente