Diferencia entre mutación, revinculación, valor de copia y operador de asignación

#!/usr/bin/env python3.2 def f1(a, l=[]): l.append(a) return(l) print(f1(1)) print(f1(1)) print(f1(1)) def f2(a, b=1): b = b + 1 return(a+b) print(f2(1)) print(f2(1)) print(f2(1)) 

En f1 el argumento l tiene una asignación de valor predeterminada, y solo se evalúa una vez, por lo que los tres print resultados 1, 2 y 3. ¿Por qué f2 no hace lo mismo?

Conclusión:

Para hacer más fácil de navegar lo que aprendí para los futuros lectores de este hilo, resumo lo siguiente:

  • He encontrado este bonito tutorial sobre el tema.

  • Hice algunos progtwigs de ejemplo sencillos para comparar la diferencia entre mutación , revinculación , valor de copia y operador de asignación .

    Esto se trata en detalle en una pregunta de SO relativamente popular , pero trataré de explicar el problema en su contexto particular.


    Cuando declara su función, los parámetros predeterminados se evalúan en ese momento . No se actualiza cada vez que llama a la función.

    La razón por la que sus funciones se comportan de manera diferente es porque las está tratando de manera diferente. En f1 estás mutando el objeto, mientras que en f2 estás creando un nuevo objeto entero y asignándolo a b . No estás modificando b aquí, lo estás reasignando. Es un objeto diferente ahora. En f1 , mantienes el mismo objeto alrededor.

    Considere una función alternativa:

     def f3(a, l= []): l = l + [a] return l 

    Esto se comporta como f2 y no se sigue agregando a la lista predeterminada. Esto se debe a que está creando una nueva l sin modificar nunca el objeto en el parámetro predeterminado.


    El estilo común en python es asignar el parámetro predeterminado de None , luego asignar una nueva lista. Esto evita toda esta ambigüedad.

     def f1(a, l = None): if l is None: l = [] l.append(a) return l 

    Porque en f2 el nombre b es rebote, mientras que en f1 el objeto l está mutado.

    Este es un caso un poco complicado. Tiene sentido cuando comprendes bien cómo Python trata los nombres y los objetos . Debería esforzarse por desarrollar esta comprensión lo antes posible si está aprendiendo Python, porque es fundamental para absolutamente todo lo que hace en Python.

    Los nombres en Python son cosas como a , f1 , b . Solo existen dentro de ciertos ámbitos (es decir, no puede usar b fuera de la función que lo usa). En tiempo de ejecución, un nombre se refiere a un valor, pero en cualquier momento puede recuperarse a un nuevo valor con declaraciones de asignación como:

     a = 5 b = a a = 7 

    Los valores se crean en algún punto de su progtwig y pueden ser referidos por nombres, pero también por espacios en listas u otras estructuras de datos. En lo que antecede, el nombre a está vinculado al valor 5, y luego rebote al valor 7. Esto no tiene ningún efecto en el valor 5, que siempre es el valor 5 sin importar cuántos nombres estén actualmente vinculados a él.

    La asignación a b por otra parte, hace que el nombre b vincule con el valor al que se hace referencia en ese momento. Volver a encontrar el nombre a después no tiene ningún efecto en el valor 5, por lo que no tiene ningún efecto en el nombre b que también está vinculado al valor 5.

    La asignación siempre funciona de esta manera en Python. Nunca tiene ningún efecto sobre los valores. (Excepto que algunos objetos contienen “nombres”; volver a enlazar esos nombres obviamente afecta al objeto que contiene el nombre, pero no afecta a los valores a los que se hace referencia antes o después del cambio)

    Siempre que vea un nombre en el lado izquierdo de una statement de asignación, está (re) vinculando el nombre. Siempre que vea un nombre en cualquier otro contexto, está recuperando el valor (actual) al que hace referencia ese nombre.


    Con eso fuera del camino, podemos ver lo que está pasando en su ejemplo.

    Cuando Python ejecuta una definición de función, evalúa las expresiones utilizadas para los argumentos predeterminados y las recuerda en algún lugar a escondidas. Después de este:

     def f1(a, l=[]): l.append(a) return(l) 

    l no es nada, porque l es solo un nombre dentro del scope de la función f1 , y no estamos dentro de esa función. Sin embargo, el valor [] se almacena en algún lugar.

    Cuando la ejecución de Python se transfiere a una llamada a f1 , enlaza todos los nombres de los argumentos ( l ) a los valores apropiados, ya sea los valores pasados ​​por el llamante o los valores predeterminados creados cuando se definió la función. Entonces, cuando Python comience a ejecutar la llamada f3(5) , el nombre a estará vinculado al valor 5 y el nombre l estará vinculado a nuestra lista predeterminada.

    Cuando Python ejecuta l.append(a) , no hay ninguna asignación a la vista, por lo que nos referimos a los valores actuales de l y a . Entonces, si esto tiene algún efecto sobre l , solo puede hacerlo modificando el valor al que me l , y de hecho lo hace. El método de adición de una lista modifica la lista agregando un elemento al final. Entonces, después de esto, nuestro valor de lista, que sigue siendo el mismo valor almacenado para ser el argumento predeterminado de f1 , ahora tiene 5 (el valor actual de a ) añadido a él, y se parece a [5] .

    Luego volvemos l . Pero hemos modificado la lista predeterminada, por lo que afectará a futuras llamadas. ¡Pero también, hemos devuelto la lista predeterminada, por lo que cualquier otra modificación al valor que devolvamos afectará a futuras llamadas!

    Ahora, considere f2 :

     def f2(a, b=1): b = b + 1 return(a+b) 

    Aquí, como antes, el valor 1 se guarda en alguna parte para que sirva como valor predeterminado para b , y cuando comencemos a ejecutar la llamada f2(5) el nombre a quedará vinculado al argumento 5, y el nombre b quedará vinculado a el valor por defecto 1 .

    Pero luego ejecutamos la sentencia de asignación. b aparece en el lado izquierdo de la statement de asignación, por lo que estamos volviendo a encontrar el nombre b . Primero Python resuelve b + 1 , que es 6, luego une b a ese valor. Ahora b está vinculado al valor 6. Pero el valor predeterminado para la función no se ha visto afectado : ¡1 sigue siendo 1!


    Esperemos que eso haya aclarado las cosas. Realmente necesitas poder pensar en términos de nombres que se refieran a valores y que puedan recuperarse para que apunten a valores diferentes, a fin de comprender Python.

    Probablemente también vale la pena señalar un caso difícil. La regla que mencioné anteriormente (acerca de la asignación de nombres siempre vinculantes sin efecto en el valor, por lo que si algo más afecta a un nombre, debe hacerlo modificando el valor) son ciertas para la asignación estándar, pero no siempre para los operadores de asignación “aumentada” como += , -= y *= .

    Lo que estos hacen, lamentablemente, depende de lo que se utiliza en ellos. En:

     x += y 

    Esto normalmente se comporta como:

     x = x + y 

    es decir, calcula un nuevo valor con y vuelve a enlazar x con ese valor, sin efecto en el valor anterior. Pero si x es una lista, entonces realmente modifica el valor al que se refiere x ! Así que ten cuidado con ese caso.

    En f1, está almacenando el valor en una matriz o, mejor aún, en Python, una lista en la que, como en f2, está operando en los valores pasados. Esa es mi interpretación al respecto. Puedo estar equivocado