¿Cómo paso una variable por referencia?
Escribí esta clase para probar:
class PassByReference:
def __init__(self):
self.variable = 'Original'
self.change(self.variable)
print(self.variable)
def change(self, var):
var = 'Changed'
Cuando intenté crear una instancia, el resultado fue Original
. Entonces parece que los parámetros en Python se pasan por valor. ¿Es eso correcto? ¿Cómo puedo modificar el código para obtener el efecto de paso por referencia, de modo que el resultado sea Changed
?
A veces la gente se sorprende de que un código como x = 1
, donde x
es el nombre de un parámetro, no afecte el argumento de la persona que llama, pero el código como x[0] = 1
sí sí. Esto sucede porque la asignación de elementos y la asignación de sectores son formas de mutar un objeto existente, en lugar de reasignar una variable, a pesar de la =
sintaxis. Consulte ¿Por qué una función puede modificar algunos argumentos tal como los percibe la persona que llama, pero no otros? para detalles.
Consulte también ¿Cuál es la diferencia entre pasar por referencia y pasar por valor? para una importante discusión terminológica independiente del idioma.
Los argumentos se pasan por asignación . La razón detrás de esto es doble:
- el parámetro pasado es en realidad una referencia a un objeto (pero la referencia se pasa por valor)
- Algunos tipos de datos son mutables, pero otros no.
Entonces:
Si pasa un objeto mutable a un método, el método obtiene una referencia a ese mismo objeto y puede mutarlo para su deleite, pero si vuelve a vincular la referencia en el método, el alcance externo no sabrá nada al respecto, y después Ya terminaste, la referencia externa seguirá apuntando al objeto original.
Si pasa un objeto inmutable a un método, aún no podrá volver a vincular la referencia externa y ni siquiera podrá mutar el objeto.
Para que quede aún más claro, veamos algunos ejemplos.
Lista: un tipo mutable
Intentemos modificar la lista que se pasó a un método:
def try_to_change_list_contents(the_list):
print('got', the_list)
the_list.append('four')
print('changed to', the_list)
outer_list = ['one', 'two', 'three']
print('before, outer_list =', outer_list)
try_to_change_list_contents(outer_list)
print('after, outer_list =', outer_list)
Producción:
before, outer_list = ['one', 'two', 'three']
got ['one', 'two', 'three']
changed to ['one', 'two', 'three', 'four']
after, outer_list = ['one', 'two', 'three', 'four']
Dado que el parámetro pasado es una referencia outer_list
, no una copia del mismo, podemos usar los métodos de lista mutante para cambiarlo y reflejar los cambios en el alcance externo.
Ahora veamos qué sucede cuando intentamos cambiar la referencia que se pasó como parámetro:
def try_to_change_list_reference(the_list):
print('got', the_list)
the_list = ['and', 'we', 'can', 'not', 'lie']
print('set to', the_list)
outer_list = ['we', 'like', 'proper', 'English']
print('before, outer_list =', outer_list)
try_to_change_list_reference(outer_list)
print('after, outer_list =', outer_list)
Producción:
before, outer_list = ['we', 'like', 'proper', 'English']
got ['we', 'like', 'proper', 'English']
set to ['and', 'we', 'can', 'not', 'lie']
after, outer_list = ['we', 'like', 'proper', 'English']
Dado que el the_list
parámetro se pasó por valor, asignarle una nueva lista no tuvo ningún efecto que el código fuera del método pudiera ver. Era the_list
una copia de la outer_list
referencia y habíamos the_list
señalado una nueva lista, pero no había forma de cambiar el lugar outer_list
señalado.
Cadena: un tipo inmutable
Es inmutable, por lo que no hay nada que podamos hacer para cambiar el contenido de la cadena.
Ahora, intentemos cambiar la referencia.
def try_to_change_string_reference(the_string):
print('got', the_string)
the_string = 'In a kingdom by the sea'
print('set to', the_string)
outer_string = 'It was many and many a year ago'
print('before, outer_string =', outer_string)
try_to_change_string_reference(outer_string)
print('after, outer_string =', outer_string)
Producción:
before, outer_string = It was many and many a year ago
got It was many and many a year ago
set to In a kingdom by the sea
after, outer_string = It was many and many a year ago
Nuevamente, dado que el the_string
parámetro se pasó por valor, asignarle una nueva cadena no tuvo ningún efecto que el código fuera del método pudiera ver. Era the_string
una copia de la outer_string
referencia y habíamos the_string
señalado una nueva cadena, pero no había forma de cambiar el lugar outer_string
señalado.
Espero que esto aclare un poco las cosas.
EDITAR: Se ha observado que esto no responde a la pregunta que @David hizo originalmente: "¿Hay algo que pueda hacer para pasar la variable por referencia real?". Trabajemos en eso.
¿Cómo solucionamos esto?
Como muestra la respuesta de @Andrea, podrías devolver el nuevo valor. Esto no cambia la forma en que se pasan las cosas, pero le permite recuperar la información que desea:
def return_a_whole_new_string(the_string):
new_string = something_to_do_with_the_old_string(the_string)
return new_string
# then you could call it like
my_string = return_a_whole_new_string(my_string)
Si realmente desea evitar el uso de un valor de retorno, puede crear una clase para contener su valor y pasarlo a la función o usar una clase existente, como una lista:
def use_a_wrapper_to_simulate_pass_by_reference(stuff_to_change):
new_string = something_to_do_with_the_old_string(stuff_to_change[0])
stuff_to_change[0] = new_string
# then you could call it like
wrapper = [my_string]
use_a_wrapper_to_simulate_pass_by_reference(wrapper)
do_something_with(wrapper[0])
Aunque esto parezca un poco engorroso.
El problema surge de una mala comprensión de qué variables hay en Python. Si estás acostumbrado a la mayoría de los idiomas tradicionales, tienes un modelo mental de lo que sucede en la siguiente secuencia:
a = 1
a = 2
Usted cree que a
es una ubicación de memoria que almacena el valor 1
y luego se actualiza para almacenar el valor 2
. Así no es como funcionan las cosas en Python. Más bien, a
comienza como una referencia a un objeto con el valor 1
y luego se reasigna como una referencia a un objeto con el valor 2
. Esos dos objetos pueden seguir coexistiendo aunque a
ya no se refieran al primero; de hecho, pueden ser compartidos por otras referencias dentro del programa.
Cuando llamas a una función con un parámetro, se crea una nueva referencia que hace referencia al objeto pasado. Esto es independiente de la referencia que se usó en la llamada a la función, por lo que no hay forma de actualizar esa referencia y hacer que haga referencia a un nuevo objeto. En tu ejemplo:
def __init__(self):
self.variable = 'Original'
self.Change(self.variable)
def Change(self, var):
var = 'Changed'
self.variable
es una referencia al objeto de cadena 'Original'
. Cuando llamas, Change
creas una segunda referencia var
al objeto. Dentro de la función reasignas la referencia var
a un objeto de cadena diferente 'Changed'
, pero la referencia self.variable
está separada y no cambia.
La única forma de evitar esto es pasar un objeto mutable. Como ambas referencias hacen referencia al mismo objeto, cualquier cambio realizado en el objeto se refleja en ambos lugares.
def __init__(self):
self.variable = ['Original']
self.Change(self.variable)
def Change(self, var):
var[0] = 'Changed'
Las otras respuestas me parecieron bastante largas y complicadas, así que creé este diagrama simple para explicar la forma en que Python trata las variables y los parámetros.
No es paso por valor ni paso por referencia: es llamada por objeto. Vea esto, de Fredrik Lundh:
Llamada por objeto
He aquí una cita significativa:
"...las variables [nombres] no son objetos; no pueden ser denotadas por otras variables ni referidas por objetos".
En su ejemplo, cuando Change
se llama al método, se crea un espacio de nombres para él; y var
se convierte en un nombre, dentro de ese espacio de nombres, para el objeto de cadena 'Original'
. Ese objeto tiene entonces un nombre en dos espacios de nombres. A continuación, var = 'Changed'
se vincula var
a un nuevo objeto de cadena y, por lo tanto, el espacio de nombres del método se olvida 'Original'
. Finalmente, se olvida ese espacio de nombres y la cadena 'Changed'
junto con él.
Piense en cosas que se pasan por asignación en lugar de por referencia/por valor. De esa manera, siempre estará claro lo que está sucediendo, siempre y cuando comprenda lo que sucede durante la tarea normal.
Entonces, al pasar una lista a una función/método, la lista se asigna al nombre del parámetro. Agregar a la lista dará como resultado que la lista se modifique. Reasignar la lista dentro de la función no cambiará la lista original, ya que:
a = [1, 2, 3]
b = a
b.append(4)
b = ['a', 'b']
print a, b # prints [1, 2, 3, 4] ['a', 'b']
Dado que los tipos inmutables no se pueden modificar, parece que se pasan por valor: pasar un int a una función significa asignar el int al parámetro de la función. Solo puedes reasignar eso, pero no cambiará el valor de las variables originales.
No hay variables en Python
La clave para comprender el paso de parámetros es dejar de pensar en "variables". Hay nombres y objetos en Python y juntos aparecen como variables, pero es útil distinguir siempre los tres.
- Python tiene nombres y objetos.
- La asignación vincula un nombre a un objeto.
- Pasar un argumento a una función también vincula un nombre (el nombre del parámetro de la función) a un objeto.
Eso es todo lo que hay que hacer. La mutabilidad es irrelevante para esta pregunta.
Ejemplo:
a = 1
Esto vincula el nombre a
a un objeto de tipo entero que contiene el valor 1.
b = x
Esto vincula el nombre b
al mismo objeto al que x
está vinculado actualmente el nombre. Después, el nombre ya b
no tiene nada que ver con el nombre x
.
Consulte las secciones 3.1 y 4.2 en la referencia del lenguaje Python 3.
Cómo leer el ejemplo en la pregunta.
En el código que se muestra en la pregunta, la declaración self.Change(self.variable)
vincula el nombre var
(en el alcance de la función Change
) al objeto que contiene el valor 'Original'
y la asignación var = 'Changed'
(en el cuerpo de la función Change
) asigna ese mismo nombre nuevamente: a algún otro objeto (eso sucede sostener una cuerda también, pero podría haber sido algo completamente distinto).
Cómo pasar por referencia
Entonces, si lo que desea cambiar es un objeto mutable, no hay problema, ya que todo se pasa efectivamente por referencia.
Si es un objeto inmutable (por ejemplo, un bool, un número, una cadena), el camino a seguir es envolverlo en un objeto mutable.
La solución rápida y sencilla para esto es una lista de un elemento (en lugar de self.variable
, pasar [self.variable]
y en la función modificar var[0]
).
El enfoque más pitónico sería introducir una clase trivial de un solo atributo. La función recibe una instancia de la clase y manipula el atributo.