¿Cómo funcionan realmente las comparaciones encadenadas en Python?

El documento de Python para comparaciones dice:

Las comparaciones se pueden encadenar arbitrariamente, por ejemplo, x < y <= z es equivalente a x < y and y <= z , excepto que y se evalúa solo una vez (pero en ambos casos z no se evalúa en absoluto cuando se encuentra x < y ser falso).

Y estas preguntas / respuestas de SO arrojan más luz sobre tal uso:

  • Los operadores de comparación de Python encadenan / agrupan de izquierda a derecha?
  • ¿Qué significa “evaluado solo una vez” para las comparaciones encadenadas en Python? , en particular la respuesta actualmente aceptada.

Así que algo como (ejemplo artificial):

 if 1 < input("Value:") < 10: print "Is greater than 1 and less than 10" 

Solo pide entrada una vez. Esto tiene sentido. Y esto:

 if 1 < input("Val1:") < 10 < input("Val2:") < 20: print "woo!" 

solo pregunta por Val2 si Val1 está entre 1 y 10 y solo imprime “woo!” si Val2 también está entre 10 y 20 (demostrando que pueden ser “encadenados arbitrariamente”). Esto también tiene sentido.

Pero todavía tengo curiosidad por saber cómo se implementa / interpreta esto en el nivel lexer / parser / compiler (o lo que sea).

Es el primer ejemplo anterior básicamente implementado de esta manera:

 x = input("Value:") 1 < x and x < 10: print "Is between 1 and 10" 

¿Dónde x realmente solo existe (y en realidad es esencialmente sin nombre) para esas comparaciones? ¿O de alguna manera hace que el operador de comparación devuelva tanto el resultado booleano como la evaluación del operando correcto (que se usará para una comparación adicional) o algo por el estilo?

Extender el análisis al segundo ejemplo me lleva a creer que está usando algo como un resultado intermedio sin nombre (alguien me educa si hay un término para eso) ya que no evalúa todos los operandos antes de hacer la comparación.

Simplemente puede dejar que Python le diga qué código de byte se produce con el módulo dis :

 >>> import dis >>> def f(): return 1 < input("Value:") < 10 ... >>> dis.dis(f) 1 0 LOAD_CONST 1 (1) 3 LOAD_GLOBAL 0 (input) 6 LOAD_CONST 2 ('Value:') 9 CALL_FUNCTION 1 12 DUP_TOP 13 ROT_THREE 14 COMPARE_OP 0 (<) 17 JUMP_IF_FALSE_OR_POP 27 20 LOAD_CONST 3 (10) 23 COMPARE_OP 0 (<) 26 RETURN_VALUE >> 27 ROT_TWO 28 POP_TOP 29 RETURN_VALUE 

Python usa una stack; el bytecode CALL_FUNCTION utiliza elementos en la stack (la input global y la cadena 'Value:' ) para llamar a una función con un argumento, para reemplazar esos dos elementos en la stack con el resultado de la llamada a la función. Antes de la llamada a la función, la constante 1 se cargó en la stack.

Entonces, cuando se llamó la input la stack parece:

 input_result 1 

y DUP_TOP duplica el valor superior, antes de girar los tres valores de stack superiores para llegar a:

 1 input_result input_result 

y un COMPARE_OP para probar los dos elementos superiores con < , reemplazando los dos elementos superiores con el resultado.

Si ese resultado fue False el bytecode JUMP_IF_FALSE_OR_POP salta a 27, lo que hace girar False en la parte superior con el input_result restante para borrarlo con un POP_TOP , para devolver el valor superior False restante como resultado.

Sin embargo, si el resultado es True , el bytecode JUMP_IF_FALSE_OR_POP hace estallar ese valor de la stack y en su lugar se carga el valor 10 en la parte superior y obtenemos:

 10 input_result 

y otra comparación se hace y se devuelve en su lugar.

En resumen, esencialmente Python hace esto:

 stack_1 = stack_2 = input('Value:') if 1 < stack_1: result = False else: result = stack_2 < 10 

con los valores de stack_* borrados nuevamente.

La stack, entonces, contiene el resultado intermedio sin nombre para comparar