Como todos sabemos, la forma pitónica de intercambiar los valores de dos elementos a
y b
es
a, b = b, a
y debe ser equivalente a
b, a = a, b
Sin embargo, hoy, cuando estaba trabajando en un código, descubrí accidentalmente que los siguientes dos intercambios dan resultados diferentes:
nums = [1, 2, 4, 3] i = 2 nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i] print(nums) # [1, 2, 4, 3] nums = [1, 2, 4, 3] i = 2 nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1] print(nums) # [1, 2, 3, 4]
Esto es alucinante para mí. ¿Alguien puede explicarme qué pasó aquí? Pensé que en un intercambio de Python las dos asignaciones ocurren de forma simultánea e independiente.
Para comprender el orden de evaluación, hice una clase 'Variable' que imprime cuando se establece y se obtiene su 'valor'.
class Variable: def __init__(self, name, value): self._name = name self._value = value @property def value(self): print(self._name, 'get', self._value) return self._value @value.setter def value(self): print(self._name, 'set', self._value) self._value = value a = Variable('a', 1) b = Variable('b', 2) a.value, b.value = b.value, a.value
Cuando se ejecuta da como resultado:
b get 2 a get 1 a set 2 b set 1
Esto muestra que primero se evalúa el lado derecho (de izquierda a derecha) y luego se evalúa el lado izquierdo (nuevamente, de izquierda a derecha).
Con respecto al ejemplo de OP: el lado derecho evaluará los mismos valores en ambos casos. Se establece el primer término del lado izquierdo y esto afecta la evaluación del segundo término. Nunca se evaluó de manera simultánea e independiente, es solo que la mayoría de las veces que se usa esto, los términos no dependen unos de otros. Establecer un valor en una lista y luego tomar un valor de esa lista para usarlo como índice en la misma lista generalmente no es una cosa y si eso es difícil de entender, lo entiende. Como cambiar la longitud de una lista en un bucle for es malo, esto tiene el mismo olor. (Sin embargo, una pregunta estimulante, como habrás adivinado porque corrí hacia un bloc de notas)
Una forma de analizar fragmentos de código en CPython es desmontar su código de bytes para su máquina de pila simulada.
>>> import dis >>> dis.dis("nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]") 1 0 LOAD_NAME 0 (nums) 2 LOAD_NAME 0 (nums) 4 LOAD_NAME 1 (i) 6 BINARY_SUBSCR 8 LOAD_CONST 0 (1) 10 BINARY_SUBTRACT 12 BINARY_SUBSCR 14 LOAD_NAME 0 (nums) 16 LOAD_NAME 1 (i) 18 BINARY_SUBSCR 20 ROT_TWO 22 LOAD_NAME 0 (nums) 24 LOAD_NAME 1 (i) 26 STORE_SUBSCR 28 LOAD_NAME 0 (nums) 30 LOAD_NAME 0 (nums) 32 LOAD_NAME 1 (i) 34 BINARY_SUBSCR 36 LOAD_CONST 0 (1) 38 BINARY_SUBTRACT 40 STORE_SUBSCR 42 LOAD_CONST 1 (None) 44 RETURN_VALUE
Agregué las líneas en blanco para facilitar la lectura. Las dos expresiones de búsqueda se calculan en los bytes 0-13 y 14-19. BINARY_SUBSCR reemplaza los dos valores superiores en la pila, un objeto y un subíndice, con el valor obtenido del objeto. Los dos valores obtenidos se intercambian para que el primero calculado sea el primer límite. Las dos operaciones de almacenamiento se realizan en los bytes 22-27 y 28-41. STORE_SUBSCR usa y elimina los tres primeros valores de la pila, un valor para almacenar, un objeto y un subíndice. (La parte de retorno Ninguno aparentemente siempre se agrega al final). La parte importante de la pregunta es que los cálculos para las tiendas se realizan secuencialmente en lotes separados e independientes.
La descripción más cercana en Python del cálculo de CPython requiere la introducción de una variable de pila
stack = [] stack.append(nums[nums[i]-1]) stack.append(nums[i]) stack.reverse() nums[i] = stack.pop() nums[nums[i]-1] = stack.pop()
Aquí está el desmontaje de la declaración invertida
>>> dis.dis("nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]") 1 0 LOAD_NAME 0 (nums) 2 LOAD_NAME 1 (i) 4 BINARY_SUBSCR 6 LOAD_NAME 0 (nums) 8 LOAD_NAME 0 (nums) 10 LOAD_NAME 1 (i) 12 BINARY_SUBSCR 14 LOAD_CONST 0 (1) 16 BINARY_SUBTRACT 18 BINARY_SUBSCR 20 ROT_TWO 22 LOAD_NAME 0 (nums) 24 LOAD_NAME 0 (nums) 26 LOAD_NAME 1 (i) 28 BINARY_SUBSCR 30 LOAD_CONST 0 (1) 32 BINARY_SUBTRACT 34 STORE_SUBSCR 36 LOAD_NAME 0 (nums) 38 LOAD_NAME 1 (i) 40 STORE_SUBSCR 42 LOAD_CONST 1 (None) 44 RETURN_VALUE
Me parece que esto solo sucedería cuando el contenido de la lista esté en el rango de índices de lista para la lista. Si por ejemplo:
nums = [10, 20, 40, 30]
El código fallará con:
>>> nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i] Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: list index out of range
Así que definitivamente, un gotcha. Nunca use nunca el contenido de una lista como índice de esa lista.
Esto se debe a que la evaluación, específicamente en el lado izquierdo de =
, ocurre de izquierda a derecha:
nums[i], nums[nums[i]-1] =
Primero se asigna nums[i]
, y luego ese valor se usa para determinar el índice en la asignación a nums[nums[i]-1]
Al hacer la tarea así:
nums[nums[i]-1], nums[i] =
... el índice de nums[nums[i]-1]
depende del valor antiguo de nums[i]
, ya que la asignación a nums[i]
sigue después...
De python.org
La asignación de un objeto a una lista de destino, opcionalmente entre paréntesis o corchetes, se define recursivamente de la siguiente manera.
...
- De lo contrario: el objeto debe ser iterable con la misma cantidad de elementos que objetivos en la lista de objetivos, y los elementos se asignan, de izquierda a derecha, a los objetivos correspondientes.
Así que interpreto que eso significa que su tarea
nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]
es aproximadamente equivalente a
tmp = nums[nums[i]-1], nums[i] nums[i] = tmp[0] nums[nums[i] - 1] = tmp[1]
(con una mejor verificación de errores, por supuesto)
mientras que el otro
nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]
es como
tmp = nums[i], nums[nums[i]-1] nums[nums[i] - 1] = tmp[0] nums[i] = tmp[1]
Entonces, el lado derecho se evalúa primero en ambos casos. Pero luego las dos piezas del lado izquierdo se evalúan en orden y las asignaciones se realizan inmediatamente después de la evaluación. Crucialmente, esto significa que el segundo término en el lado izquierdo solo se evalúa después de que ya se haya realizado la primera asignación. Entonces, si actualiza nums[i]
primero, entonces nums[nums[i] - 1]
se refiere a un índice diferente que si actualiza nums[i]
segundo lugar.
Esto sucede de acuerdo con las reglas:
Entonces, con nums = [1, 2, 4, 3]
, tu código en el primer caso
nums[2], nums[nums[2]-1] = nums[nums[2]-1], nums[2]
es equivalente a:
nums[2], nums[nums[2]-1] = nums[nums[2]-1], nums[2] nums[2], nums[nums[2]-1] = nums[3], nums[2] nums[2], nums[nums[2]-1] = 3, 4
y como ahora se evalúa el lado derecho, las asignaciones son equivalentes a:
nums[2] = 3 nums[nums[2]-1] = 4 nums[2] = 3 nums[3-1] = 4 nums[2] = 3 nums[2] = 4
lo que da:
print(nums) # [1, 2, 4, 3]
En el segundo caso, obtenemos:
nums[nums[2]-1], nums[2] = nums[2], nums[nums[2]-1] nums[nums[2]-1], nums[2] = nums[2], nums[3] nums[nums[2]-1], nums[2] = 4, 3 nums[nums[2]-1] = 4 nums[2] = 3 nums[4-1] = 4 nums[2] = 3 nums[3] = 4 nums[2] = 3 print(nums) # [1, 2, 3, 4]
En el lado izquierdo de su expresión, está leyendo y escribiendo números [i], no sé si Python garantiza el procesamiento de las operaciones de desempaquetado en orden de izquierda a derecha, pero supongamos que sí, su primer ejemplo sería equivalente a.
t = nums[nums[i]-1], nums[i] # t = (3,4) nums[i] = t[0] # nums = [1,2,3,3] n = nums[i]-1 # n = 2 nums[n] = t[1] # nums = [1,2,4,3]
Mientras que su segundo ejemplo sería equivalente a
t = nums[i], nums[nums[i]-1] # t = (4,3) n = nums[i]-1 # n = 3 nums[n] = t[0] # nums = [1,2,4,4] nums[i] = t[0] # nums = [1,2,3,4]
Lo cual es consistente con lo que tienes.
Thierry dio una buena respuesta, déjame ser más claro. Tenga en cuenta que si nums = [1, 2, 4, 3]
,
en este código:
nums[nums[i]-1], nums[i]
en este código:
nums[i], nums[nums[i]-1]
Quizás la buena pregunta sobre un intercambio fue usar:
nums[i], nums[i-1] = nums[i-1], nums[i]
?
Intentalo:
>>> print(nums) >>> [1, 2, 4, 3] >>> nums[i], nums[i-1] = nums[i-1], nums[i] >>> print(nums) >>> [1, 4, 2, 3]
ChD