1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 2 из 5

Состояние и трассировка: что делает программа на самом деле

Коротко

Состояние программы - значения всех её переменных в данный момент и место в тексте, откуда выполнение продолжится. Каждый шаг исполнителя переводит одно состояние в следующее; кроме этого программа делает только одно - выводит текст. Трассировка - таблица, в которой строка - это шаг, а колонки - переменные: её заполняют руками, строка за строкой, ДО запуска. Она показывает, что программа делает на самом деле, а не что автор имел в виду. Ошибка находится в первой строке таблицы, где значение разошлось с ожиданием, - и обычно это не та строка, на которую думал.

Если ты привык вести таблицу трассировки и находишь по ней ошибку, листай до «Теперь сам» в конце.

Как думаешь, что напечатает эта программа

Автор хотел найти второе по величине число в списке:

a = [3, 8, 5, 8]
luchshij = 0
vtoroj = 0
for x in a:
    if x > luchshij:
        vtoroj = luchshij
        luchshij = x
print(vtoroj)

Ответь себе, не запуская: что окажется на экране? Читается программа гладко - новое наибольшее отодвигает старое на второе место. Если твой ответ 5, именно для таких случаев и нужна таблица.

На что это похоже

Запись шахматной партии. По ней можно восстановить позицию на любом ходу, не помня партию: взял начальную расстановку и применил ходы по порядку. Позиция - это состояние, ход - это шаг. Тренер, разбирая проигрыш, не спрашивает «что ты хотел», он ставит позицию двадцатого хода и показывает, где план разошёлся с доской. Трассировка делает с программой то же самое.

Механизм: исполнитель Python и его шаги

Исполнитель в этом курсе - интерпретатор Python: программа, которая берёт твой текст и выполняет его сама, шаг за шагом. Строго говоря, CPython (обычный Python) сначала переводит весь файл в промежуточный байт-код, поэтому синтаксическая ошибка в последней строке (скажем, незакрытая скобка) не даст выполниться и первой; но выполняет этот байт-код сам интерпретатор. Есть и другой способ - компилятор: он переводит программу в другой язык, обычно в команды процессора, и выполняется уже перевод. Чаще всего переводят заранее и целиком, но бывает и по частям прямо во время работы программы. Для рассуждения разницы нет: и там и там шаги идут по порядку, и каждый шаг меняет состояние.

Какие шаги бывают:

шаг что меняет в состоянии
присваивание x = ... значение одной переменной
проверка условия в if или while ни одной переменной, но выбирает, какая строка следующая
очередной круг for переменную цикла: она получает следующий элемент
print(...) ничего в переменных, зато добавляет строку на экран

Таблица трассировки повторяет эту картину: в колонке «строка» - что выполнилось, в колонках переменных - значения после шага, в колонке «условие» - результат проверки. Пустая клетка значит «не изменилось», а до первого присваивания - «значения ещё нет». Когда шаги внутри круга цикла короткие, строку таблицы отводят целому кругу и пишут значения на его конец.

Правило

Трассировка: одна строка таблицы - один выполненный шаг (или целый круг цикла); в клетки пишется значение ПОСЛЕ шага; условия записываются своим результатом, True или False. Заполнять таблицу надо по тексту программы, а не по замыслу, и останавливаться на первой строке, где значение расходится с ожидаемым.

Разбор: таблица для второго по величине

Ведём таблицу для программы из начала урока. Колонки: текущий x, результат условия x > luchshij, значения luchshij и vtoroj после круга.

круг x x > luchshij luchshij vtoroj
до цикла 0 0
1 3 True 3 0
2 8 True 8 3
3 5 False 8 3
4 8 False 8 3

Программа напечатает 3. Расхождение в третьей строке: пришло 5, оно меньше наибольшего, но больше второго, и должно было занять второе место. Условие x > luchshij его просто не пропустило - ветки для «меньше первого, но больше второго» в программе нет. Читая код, это легко не заметить: мысль «новое наибольшее отодвигает старое» верна, просто она описывает не все случаи.

Исправленная версия добавляет недостающую ветку, а равное наибольшему пропускает - иначе на списке [3, 8, 5, 8] вторым окажется вторая восьмёрка:

vtoroj.py
a = [3, 8, 5, 8]
luchshij = 0
vtoroj = 0
for x in a:
    if x > luchshij:
        vtoroj = luchshij
        luchshij = x
    else:
        if x > vtoroj and x < luchshij:
            vtoroj = x
print(vtoroj)
↳  выводpython3 vtoroj.py
5

Обе версии рассчитаны на положительные числа и хотя бы два разных значения: начальный ноль играет роль «меньше всех», и на списке [-8, -3] исправленная версия напечатает 0. Как записать такое требование к входу, разберёт глава «Задача и модель стоимости».

Разбор: обмен двух значений

Нужно поменять местами значения a и b. Первая попытка выглядит очевидной:

obmen.py
a = 4
b = 9
a = b
b = a
print(a, b)
↳  выводpython3 obmen.py
9 9

Таблица объясняет за две строки. После a = b в состоянии a = 9, b = 9: старая четвёрка стёрта, её больше нет нигде. Следующий шаг b = a копирует девятку в b. Присваивание не «переносит» значение, а перезаписывает клетку, и то, что в ней было, пропадает. Значит, старое значение надо где-то сохранить ДО перезаписи - во временной переменной:

obmen2.py
a = 4
b = 9
vremennaya = a
a = b
b = vremennaya
print(a, b)
↳  выводpython3 obmen2.py
9 4

В Python есть и запись в одну строку, a, b = b, a, - почему она работает, разберёт следующая глава.

Где ошибаются

Первая ошибка - вести таблицу в голове и пропускать «очевидные» круги. Дефект второго по величине жил ровно в круге, который казался очевидным. Вторая - заполнять клетки тем, что строка должна сделать, а не тем, что она делает: так таблица повторяет замысел и ничего не находит. Третья - открыть отладчик или машинную трассу раньше своей таблицы. Тогда расхождение показывают тебе, а не находишь ты, и в следующий раз без отладчика его снова не найти.

Зачем это в работе

Отладчик в редакторе показывает ровно эту таблицу: строку, на которой стоит выполнение, и значения переменных. print внутри цикла печатает её по строке на круг. Разница только в том, кто заполняет клетки, а умение то же: остановиться на первом расхождении с ожиданием. На собеседовании, где отладчика нет, просят прогнать код на маленьком входе вслух - и это тоже трассировка. На ревью она отвечает на вопрос «а что будет на пустом списке» быстрее, чем спор.

Теперь сам

Заполни таблицу и скажи, что напечатает программа:

s = 0
i = 1
while i < 10:
    s = s + i
    i = i * 2
print(s, i)

Ответ.

шаг i < 10 s i
до цикла 0 1
круг 1 True 1 2
круг 2 True 3 4
круг 3 True 7 8
круг 4 True 15 16
проверка False

Напечатает 15 16. Обрати внимание на последнюю строку: условие проверилось ещё раз и оказалось ложным - поэтому i вышло за десять. Значение переменной цикла после цикла - частый вопрос, и отвечает на него только последняя строка таблицы.

Главное

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

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

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