Как на самом деле работают цепные сравнения в Python?


Python Doc для сравнения говорит:

Сравнения могут быть связаны произвольно, например, x < y <= z эквивалентно x < y and y <= z, за исключением того, что y оценивается только один раз (но в обоих случаях z не оценивается вообще, когда x < y оказывается ложным).

И эти так вопросы / ответы проливают еще немного света на такое использование:

Итак, что-то вроде (надуманный пример):

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

Запрашивает ввод только один раз. В этом есть смысл. И вот что:

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

Только просит Val2 Если Val1 находится между 1 и 10 и только печатает "ууу!"Если Val2 также между 10 и 20 (доказывая, что они могут быть "скованы произвольно"). Это тоже имеет смысл.

Но я ... все еще любопытно, как это на самом деле реализуется/интерпретируется на уровне лексера/парсера/компилятора (или чего-то еще).

Первый пример выше в основном реализован следующим образом:

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

Где x реально существует (и фактически по существу не имеет названия) только для этих сравнений? Или это каким-то образом заставляет оператор сравнения возвращать и булев результат, и оценку правильного операнда (который будет использоваться для дальнейшего сравнения) или что-то в этом роде?

Расширение анализ второго примера приводит меня к мысли, что он использует что-то вроде безымянного промежуточного результата (кто-то просветит меня, если для этого есть термин), поскольку он не оценивает все операнды перед сравнением.

1 4

1 ответ:

Вы можете просто позволить Python сказать вам, какой байт-код создается с помощью 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 использует стек; CALL_FUNCTION байт-код использует элементы в стеке (input global и 'Value:' string) для вызова функции с одним аргументом, чтобы заменить эти два элемента в стеке результатом вызова функции. Перед вызовом функции константа 1 загружалась в стек.

Так что к тому времени input был назван стек выглядит например:

input_result
1

И DUP_TOP дублирует верхнее значение, прежде чем вращает три верхних стека значений, чтобы получить:

1
input_result
input_result

И a COMPARE_OP, чтобы проверить два верхних элемента с помощью <, заменив два верхних элемента результатом.

Если этот результат был False JUMP_IF_FALSE_OR_POP байт-код переходит к 27, который поворачивает False сверху с оставшимся input_result, чтобы очистить его с помощью POP_TOP, чтобы затем вернуть оставшееся False верхнее значение в качестве результат.

Если результат True однако, это значение выскакивает из стека байт-кодом JUMP_IF_FALSE_OR_POP и на его место загружается значение 10 сверху, и мы получаем:

10    
input_result

И вместо этого делается и возвращается другое сравнение.

Таким образом, по существу Python делает следующее:

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

Со значениями stack_*, очищенными снова.

Стек, таким образом, содержит неназванный промежуточный результат для сравнения