¿Cómo funciona la llamada en Python?

Para un proyecto en el que estoy trabajando, estoy implementando una estructura de datos de listas vinculadas, que se basa en la idea de un par, que defino como:

class Pair: def __init__(self, name, prefs, score): self.name = name self.score = score self.preferences = prefs self.next_pair = 0 self.prev_pair = 0 

donde self.next_pair y self.prev_pair son punteros a los enlaces anterior y siguiente, respectivamente.

Para configurar la lista enlazada, tengo una función de instalación que se parece a esto.

 def install(i, pair): flag = 0 try: old_pair = pair_array[i] while old_pair.next_pair != 0: if old_pair == pair: #if pair in remainders: remainders.remove(pair) return 0 if old_pair.score < pair.score: flag = 1 if old_pair.prev_pair == 0: # we are at the beginning old_pair.prev_pair = pair pair.next_pair = old_pair pair_array[i] = pair break else: # we are not at the beginning pair.prev_pair = old_pair.prev_pair pair.next_pair = old_pair old_pair.prev_pair = pair pair.prev_pair.next_pair = pair break else: old_pair = old_pair.next_pair if flag==0: if old_pair == pair: #if pair in remainders: remainders.remove(pair) return 0 if old_pair.score < pair.score: if old_pair.prev_pair==0: old_pair.prev_pair = pair pair.next_pair = old_pair pair_array[i] = pair else: pair.prev_pair = old_pair.prev_pair pair.next_pair = old_pair old_pair.prev_pair = pair pair.prev_pair.next_pair = pair else: old_pair.next_pair = pair pair.prev_pair = old_pair except KeyError: pair_array[i] = pair pair.prev_pair = 0 pair.next_pair = 0 

A lo largo del progtwig, estoy creando un diccionario de estas listas enlazadas, y quito los enlaces de algunos y los agrego en otros. Entre los podados y los reinstalados, los enlaces se almacenan en una matriz intermedia.

En el transcurso de la depuración de este progtwig, me he dado cuenta de que mi comprensión de la forma en que Python pasa los argumentos a las funciones es errónea. Considere este caso de prueba que escribí:

 def test_install(): p = Pair(20000, [3, 1, 2, 50], 45) print p.next_pair print p.prev_pair parse_and_get(g) first_run() rat = len(juggler_array)/len(circuit_array) pref_size = get_pref_size() print pref_size print install(3, p) print p.next_pair.name print p.prev_pair 

Cuando ejecuto esta prueba, obtengo el siguiente resultado.

 0 0 10 None 10108 0 

Lo que no entiendo es por qué la segunda llamada a p.next_pair produce un resultado diferente ( 10108 ) que la primera llamada ( 0 ). install no devuelve un objeto de Pair que puede sobrescribir el que se pasó (no devuelve None ), y no es como si estuviera pasando un indicador de instalación.

Mi entendimiento de la llamada por valor es que el intérprete copia los valores pasados ​​a una función, sin cambiar las variables de la persona que llama. Por ejemplo, si digo

 def foo(x): x = x+1 return x baz = 2 y = foo(baz) print y print baz 

Luego se deben imprimir 3 y 2 , respectivamente. Y de hecho, cuando pruebo eso en el intérprete de Python, eso es lo que sucede.

Realmente apreciaría si alguien me puede orientar en la dirección correcta aquí.

En Python, todo es un objeto. La asignación simple almacena una referencia al objeto asignado en el nombre asignado a. Como resultado, es más sencillo pensar que las variables de Python son nombres que se asignan a objetos, en lugar de objetos que se almacenan en ubicaciones con nombre.

Por ejemplo:

 baz = 2 

… almacena en baz un puntero, o referencia, al objeto entero 2 que se almacena en otro lugar. (Dado que el tipo int es inmutable, Python en realidad tiene un grupo de enteros pequeños y reutiliza el mismo objeto 2 todas partes, pero este es un detalle de implementación que no debe preocuparnos mucho).

Cuando se llama a foo(baz) , la variable local de foo() también apunta al objeto entero 2 al principio. Es decir, foo() -local nombre x y el nombre global baz son nombres para el mismo objeto, 2 . Entonces x = x + 1 se ejecuta. Esto cambia x para apuntar a un objeto diferente: 3 .

Es importante comprender que: x no es un cuadro que contiene 2 , y 2 se incrementa a 3 . No, x inicialmente apunta a 2 y ese puntero se cambia para que apunte a 3 . Naturalmente, dado que no cambiamos a qué objeto apunta baz , todavía apunta a 2 .

Otra forma de explicarlo es que en Python, todos los argumentos que pasan son por valor, pero todos los valores son referencias a objetos.

Un resultado contraintuitivo de esto es que si un objeto es mutable, se puede modificar a través de cualquier referencia y todas las referencias “verán” el cambio. Por ejemplo, considera esto:

 baz = [1, 2, 3] def foo(x): x[0] = x[0] + 1 foo(baz) print baz >>> [2, 2, 3] 

Esto parece muy diferente de nuestro primer ejemplo. Pero en realidad, el argumento se pasa de la misma manera. foo() recibe un puntero a baz con el nombre x y luego realiza una operación en él que lo cambia (en este caso, el primer elemento de la lista apunta a un objeto int diferente). La diferencia es que el nombre x nunca se apunta a un nuevo objeto; es x[0] que se modifica para apuntar a un objeto diferente. x sí todavía apunta al mismo objeto que baz . (De hecho, bajo el capó, la asignación a x[0] convierte en una llamada de método: x.__setitem__() .) Por lo tanto, baz “ve” la modificación a la lista. ¿Cómo no podría?

No ve este comportamiento con enteros y cadenas porque no puede cambiar enteros o cadenas; son tipos inmutables, y cuando los modificas (por ejemplo, x = x + 1 ), en realidad no los estás modificando, sino que estás vinculando el nombre de tu variable a un objeto completamente diferente. Si cambia baz a una tupla, por ejemplo, baz = (1, 2, 3) , encontrará que foo() le da un error porque no puede asignar elementos a una tupla; Las tuplas son otro tipo inmutable. “Cambiar” una tupla requiere crear una nueva, y la asignación luego apunta la variable al nuevo objeto.

Los objetos de las clases que defina son mutables, por lo que su instancia de Pair puede ser modificada por cualquier función a la que se pase, es decir, los atributos se pueden agregar, eliminar o reasignar a otros objetos. Ninguna de estas cosas volverá a unir ninguno de los nombres que apuntan a su objeto, por lo que todos los nombres que actualmente lo apuntan “verán” los cambios.

Python no copia nada cuando pasa variables a una función. No es una llamada por valor ni una llamada por referencia, pero de esos dos es más similar a la llamada por referencia. Podrías considerarlo como “llamada por valor, pero el valor es una referencia”.

Si pasa un objeto mutable a una función, la modificación de ese objeto dentro de la función afectará al objeto en cualquier lugar donde aparezca. (Si pasa un objeto inmutable a una función, como una cadena o un entero, entonces por definición no puede modificar el objeto en absoluto).

La razón por la que esto no es técnicamente paso por referencia es que puede volver a enlazar un nombre para que el nombre se refiera a algo completamente distinto. (Para los nombres de objetos inmutables, esto es lo único que puede hacerles.) Volver a encontrar un nombre que exista solo dentro de una función no afecta ningún nombre que pueda existir fuera de la función.

En su primer ejemplo con los objetos Pair , está modificando un objeto, por lo que ve los efectos fuera de la función.

En su segundo ejemplo, no está modificando ningún objeto, simplemente está volviendo a vincular nombres con otros objetos (otros enteros en este caso). baz es un nombre que apunta a un objeto entero (en Python, todo es un objeto, incluso enteros) con un valor de 2. Cuando pasa baz a foo(x) , el nombre x se crea localmente dentro de la función foo en la stack, y x se establece en el puntero que se pasó a la función, el mismo puntero que baz . Pero x y baz no son lo mismo, solo contienen punteros al mismo objeto. En la línea x = x+1 , x es rebote para apuntar a un objeto entero con un valor de 3, y ese puntero es lo que se devuelve de la función y se usa para vincular el objeto entero a y.

Si reescribió su primer ejemplo para crear explícitamente un nuevo objeto Pair dentro de su función basándose en la información del objeto Pair pasado (ya sea una copia que modifique o si crea un constructor que modifique los datos de la construcción) entonces su función no tendría el efecto secundario de modificar el objeto que se pasó.

Edición: por cierto, en Python no debe usar 0 como marcador de posición para significar “No tengo un valor”, use None . Y de la misma manera, no deberías usar 0 para significar False , como parece que estás haciendo en la flag . Pero todos los valores de 0 , None y False evalúan como False en las expresiones booleanas, por lo que no importa cuál de los que uses, puedes decir cosas como if not flag lugar de if flag == 0 .

Le sugiero que se olvide de implementar una lista enlazada y simplemente use una instancia de una list Python. Si necesita algo que no sea la list predeterminada de Python, tal vez pueda usar algo de un módulo de Python, como las collections .

Un bucle de Python para seguir los enlaces en una lista enlazada se ejecutará a la velocidad del intérprete de Python, es decir, lentamente. Si simplemente usa la clase de list incorporada, las operaciones de la lista se realizarán en el código C de Python y ganará velocidad.

Si necesita algo como una lista pero con una inserción rápida y una eliminación rápida, ¿puede hacer que un dict funcione? Si hay algún tipo de valor de ID (cadena o entero o lo que sea) que pueda usarse para imponer un orden en sus valores, podría usarlo como un valor clave y obtener una rápida inserción y eliminación de valores. Luego, si necesita extraer los valores en orden, puede usar la función del método dict.keys() para obtener una lista de valores clave y usarla.

Pero si realmente necesita listas vinculadas, le sugiero que busque un código escrito y depurado por otra persona y que lo adapte a sus necesidades. Busque en Google “receta de la lista vinculada de python” o “módulo de lista vinculada de python”.

Voy a echar en un factor un poco complicado:

 >>> def foo(x): ... x *= 2 ... return x ... 

Defina una función ligeramente diferente utilizando un método que sé que es compatible con números, listas y cadenas.

Primero, llámalo con cuerdas:

 >>> baz = "hello" >>> y = foo(baz) >>> y 'hellohello' >>> baz 'hello' 

A continuación, llámalo con listas:

 >>> baz=[1,2,2] >>> y = foo(baz) >>> y [1, 2, 2, 1, 2, 2] >>> baz [1, 2, 2, 1, 2, 2] >>> 

Con cadenas, el argumento no se modifica. Con las listas, se modifica el argumento.

Si fuera yo, evitaría modificar argumentos dentro de los métodos.