¿Por qué la asignación a múltiples objectives (identificador / atributo) produce resultados extraños?

Tengo un código como este:

def foo(): bar = initial_bar = Bar() while True: next_bar = Bar() bar.next_bar = next_bar bar = next_bar return initial_bar 

La intención es que se forme una cadena de Bar que pueda seguirse, estilo de lista enlazada.

Todo esto estaba muy bien; pero a través de alguna noción equivocada, quería cortarlo por una línea, combinando las asignaciones al final del bucle en una sola línea.

 def foo(): bar = initial_bar = Bar() while True: next_bar = Bar() bar = bar.next_bar = next_bar return initial_bar 

Debido a que bar = bar.next_bar = next_bar se expandirá a bar.next_bar = next_bar seguido por efectivamente bar = bar.next_bar . (Excepto que no lo hace.)

El problema es que esto no funciona; la “barra inicial” devuelta no tiene su next_bar definido. Puedo resolverlo fácilmente volviendo a la solución de dos líneas más explícita, pero ¿qué está pasando?

Es hora de sacar la dis .

 >>> import dis >>> dis.dis(foo) 2 0 LOAD_GLOBAL 0 (Bar) 3 CALL_FUNCTION 0 6 DUP_TOP 7 STORE_FAST 0 (bar) 10 STORE_FAST 1 (initial_bar) 3 13 SETUP_LOOP 32 (to 48) >> 16 LOAD_GLOBAL 1 (True) 19 POP_JUMP_IF_FALSE 47 4 22 LOAD_GLOBAL 0 (Bar) 25 CALL_FUNCTION 0 28 STORE_FAST 2 (next_bar) 5 31 LOAD_FAST 2 (next_bar) 34 DUP_TOP 35 STORE_FAST 0 (bar) 38 LOAD_FAST 0 (bar) 41 STORE_ATTR 2 (next_bar) 44 JUMP_ABSOLUTE 16 >> 47 POP_BLOCK 6 >> 48 LOAD_FAST 1 (initial_bar) 51 RETURN_VALUE 

Si observa detenidamente eso, verá que en la línea crítica (línea 5, vea los números a la izquierda, posiciones 31-47), hace esto:

  • Cargar next_bar (31) dos veces (34);
  • Escríbelo (la primera copia en la stack) a la bar (35);
  • Escríbelo (la segunda copia en la stack) a bar.next_bar (38, 41).

Esto se ve más obviamente en un caso de prueba mínimo.

 >>> def a(): ... b = c = d ... >>> dis.dis(a) 2 0 LOAD_GLOBAL 0 (d) 3 DUP_TOP 4 STORE_FAST 0 (b) 7 STORE_FAST 1 (c) 10 LOAD_CONST 0 (None) 13 RETURN_VALUE 

Mira lo que está haciendo. Esto significa que b = c = d es en realidad equivalente a b = d; c = d b = d; c = d . Normalmente esto no importará, pero en el caso mencionado originalmente, sí importa. Significa que en la línea crítica,

 bar = bar.next_bar = next_bar 

no es equivalente a

 bar.next_bar = next_bar bar = next_bar 

Sino más bien a

 bar = next_bar bar.next_bar = next_bar 

Esto, de hecho, está documentado en la sección 6.2 de la documentación de Python, declaraciones simples , declaraciones de asignación :

Una statement de asignación evalúa la lista de expresiones (recuerde que esto puede ser una expresión única o una lista separada por comas; esta última produce una tupla) y asigna el único objeto resultante a cada una de las listas de destino, de izquierda a derecha .

También hay una advertencia relacionada en esa sección que se aplica a este caso:

ADVERTENCIA: aunque la definición de asignación implica que las superposiciones entre el lado izquierdo y el derecho son “seguras” (por ejemplo a, b = b, a intercambia dos variables), se superponen dentro de la colección de variables asignadas no son seguros Por ejemplo, el siguiente progtwig imprime [0, 2] :

 x = [0, 1] i = 0 i, x[i] = 1, 2 print x 

Es posible ir a bar.next_bar = bar = next_bar y eso produce el resultado inicialmente deseado, pero ten piedad de cualquiera (incluido el autor original algún tiempo después) que tendrá que leer el código más tarde y regocijarse por el hecho de que , en palabras que estoy seguro de que Tim habría usado si hubiera pensado en ellos,

Explícito es mejor que un caso de esquina potencialmente confuso.