LANG·I Язык Глава 3 из 65
Развилки
Глава-квест: вы пишете текстовую игру по мотивам компьютерной пещеры 1976 года, и каждая новая конструкция языка открывает в ней новую комнату. К концу главы в квест можно играть.
Язык
Опирается на: 02 · Имена и значения
Что вы унесёте из главы
- писать программы, которые выбирают, что делать: if, elif, else и вложенные условия
- собирать сложные условия из and, or и not и проверять их таблицей истинности
- не попадаться в вечные ловушки: = вместо ==, or без второго сравнения и неправильный порядок проверок
Программы из прошлой главы умеют запоминать и спрашивать, но идут одной дорогой: строка за строкой, сверху вниз, что бы ни ответил человек. Фокусник не может сказать «если задумали больше пятидесяти, то…». Чтобы программа выбирала, ей нужны развилки. Из развилок мы соберём игру, текстовый квест про пещеру: каждая новая конструкция языка откроет в нём комнату, а в конце главы вы в эту пещеру спуститесь.
Сначала о другой пещере — той, которую полвека назад перенёс в компьютер программист-спелеолог.
Внутри такой игры — сплошные «если». Если игрок сказал «на север» и в этой комнате есть проход на север — перейти. Если в руках нет лампы, а вокруг темно — предупредить. Если у игрока птица в клетке, а впереди змея… Об этом чуть позже. Наша пещера будет скромнее, но устроена так же.
Комната первая. У дороги
Первая комната — та же, что у Краутера. Программа описывает место и спрашивает, войти ли в дом. Если игрок ответил «да», она описывает дом. Если нет — не описывает.
Запустите её дважды: ответьте «да», потом «нет». Слово if значит «если». За ним идёт условие — выражение, которое бывает истинным или ложным. Двойное == спрашивает «равны ли?», и ответ на этот вопрос — логическое значение True или False, тип bool из прошлой главы. Если условие истинно, Python выполняет строки, сдвинутые вправо под if; если ложно — перепрыгивает их. Последняя строка ни под каким if не стоит и выполняется всегда.
Зачем нужны отступы
В главе 1 лишний отступ ломал программу, и мы обещали объяснить, зачем отступы вообще нужны. Строки, сдвинутые на одинаковое расстояние под if, — это блок: они выполняются вместе или не выполняются вовсе. Где блок начинается, показывает двоеточие в конце строки с if. Где кончается — первая строка, которая снова начинается у левого края. Принято сдвигать на четыре пробела.
Во многих языках блок заключают в фигурные скобки, а отступы ставят только для красоты, и машина их не замечает. Python, вслед за учебным языком ABC, над которым до этого работал его автор Гвидо ван Россум, сделал отступы частью языка: раз люди всё равно читают программу по отступам, пусть по ним же читает и машина. Цена — строгость. Попробуйте убрать отступ у строки с лампой или, наоборот, сдвинуть последнюю строку вправо.
В программе выше сдвиньте последнюю строку, print("Ручей журчит…"), на четыре пробела вправо. Что изменится?
Строка теперь в блоке if и выполняется только вместе с ним. Ручей перестал журчать для тех, кто прошёл мимо дома, хотя ни одно слово в программе не поменялось.
Развилку удобно нарисовать: ромб — условие, прямоугольник — действие. Схему ниже строит сам код. Нажмите «Пройти», ответьте программе, и подсветится путь, которым она прошла.
Комната вторая. Дом
В доме лежит лампа. Игрок либо берёт её, либо нет, — и в обоих случаях программа должна что-то сказать. Для второго пути есть слово else, «иначе»:
Из двух блоков выполняется ровно один: либо тот, что под if, либо тот, что под else. Переменная lamp запоминает выбор игрока, и следующие комнаты смогут её проверить. Такие переменные со значением «да или нет» программисты называют флажками.
Флажок можно поставить и короче. Сравнение answer == "да" само по себе — значение True или False, его можно сразу запомнить:
Игрок ответил «Да» — с большой буквы. Что окажется в lamp?
False. Для Python «Да» и «да» — разные строки, потому что заглавная Д и строчная д — разные символы. Прощать игроку заглавные буквы и лишние пробелы мы научимся в главе 7, а пока будем отвечать маленькими буквами.
Комната третья. Решётка
Ручей уходит под землю, а вход в пещеру закрыт стальной решёткой с кодовым замком. Код — число. Замок сравнивает его с правильным и подсказывает, в какую сторону вы ошиблись. Кроме ==, в Python есть ещё пять сравнений:
| Запись | Читается | Истинно, например |
|---|---|---|
a == b | равно | 2 + 2 == 4 |
a != b | не равно | "да" != "Да" |
a < b | меньше | 1023 < 1024 |
a > b | больше | 1025 > 1024 |
a <= b | не больше | 18 <= 18 |
a >= b | не меньше | 18 >= 18 |
Четыре отдельных if проверяются независимо, один за другим, и при неверном коде срабатывают два из них. Сравнения можно составлять цепочкой, как в математике: 1000 <= code <= 9999 значит «код четырёхзначный». Строки тоже сравниваются, но по-своему.
Что напечатает вторая строка, print("10" < "9")?
True. Строки сравниваются как слова в словаре: по первому несовпадающему символу. У "10" и "9" первые символы — «1» и «9», а «1» в таблице символов стоит раньше. Поэтому числа из input() сначала превращают в int, а уже потом сравнивают.
Самая частая опечатка
Один знак равенства — присваивание, два — сравнение. Перепутать их легко, особенно поначалу.
Python не запустит такую программу и даже подскажет: Maybe you meant '==' or ':=' instead of '='? — «может быть, вы имели в виду == или :=?». Другие языки не так заботливы. В языке C похожая строка, if (code = 1024), ошибкой не считается: она присваивает и тут же проверяет результат, а 1024 не ноль, значит, истина. Такие опечатки годами жили в рабочих программах, и создатели Python одиночное = внутри условия попросту запретили. Присвоить что-то прямо в условии с версии 3.8 можно, но только особым знаком :=, который с == не спутаешь.
Комната четвёртая. Зал
За решёткой — огромный зал, из которого ведут три прохода. Можно написать три отдельных if, но тогда нужен ещё один — для ответа, который не подошёл ни к одному проходу, — а условие у него выйдет длинным и путаным. Для выбора из многих вариантов есть elif, сокращение от else if, «иначе, если»:
Python проверяет условия сверху вниз и выполняет блок первого истинного. Остальные он уже не смотрит, даже если они тоже истинны. else в конце ловит всё, что не подошло, и может отсутствовать. Схема цепочки похожа на лесенку: каждое «нет» ведёт к следующему вопросу.
Порядок проверок
Из правила «выполняется первое истинное» следует ловушка. В пещере у лампы садится батарея, и программа описывает свет по заряду:
Поменяем местами первую и третью проверку: сначала charge > 0, потом charge > 10, потом charge > 50. Что программа скажет при заряде 80?
«Лампа мигает». Первое же условие, charge > 0, истинно для 80, Python выполняет его блок и дальше не смотрит. Ветки «ярко» и «тускло» теперь недостижимы: любое число, которое в них попало бы, раньше поймает первая проверка.
На числовой прямой ниже цветом показано, какая ветка ловит каждое значение заряда. Поднимите широкую проверку наверх, и она «съест» более узкие.
Поэтому в цепочке elif узкие условия ставят раньше широких. Если одно условие шире другого, как «больше 10» шире «больше 50», более строгое должно стоять выше. Такую ошибку Python не заметит: программа отработает без единой жалобы, только пойдёт не тем путём.
Комната пятая. Птица и змея
В пещере Краутера и Вудса есть одна из самых известных загадок ранних компьютерных игр. В одной комнате живёт маленькая птица. Дальше по пещере, в зале Горного короля, дорогу преграждает огромная змея, и прогнать её может только птица. Но поймать птицу можно лишь в клетку, и она боится чёрного жезла, который игрок находит неподалёку: с жезлом в руках к ней не подойти. Условие поимки складывается из двух: есть клетка и нет жезла.
Поменяйте True и False в первых строках и проверьте все исходы. Три слова собирают сложные условия из простых:
a and b— «и»: истинно, только когда истинны оба;a or b— «или»: истинно, когда истинно хотя бы одно;not a— «не»: переворачивает истину в ложь и обратно.
Сколько бы ни было простых условий, у каждого всего два значения, поэтому можно перебрать все их сочетания и для каждого записать, чему равно сложное условие. Такая таблица называется таблицей истинности. Для двух переменных в ней четыре строки, для трёх — восемь, для $n$ переменных — $2^n$.
Взглянем на условие поимки глазами Шеннона. Каждая переменная — выключатель, and ставит выключатели друг за другом, or — рядом, а not превращает выключатель в «перевёрнутый»: он замкнут, когда переменная ложна. Лампочка в конце цепи загорается, когда всё выражение истинно.
and, or, not и скобок. Нажимайте на выключатели или на строки таблицы. Схема строится из самого выражения.Сравните в виджете not (cage and rod) и not cage or not rod: таблицы совпадают строка в строку. Это один из законов де Моргана: «неверно, что и то, и другое» — то же самое, что «неверно первое или неверно второе». Такие законы помогают упрощать запутанные условия, а в главе 29 — схемы.
Ловушка с or
Игроки ленятся и отвечают «д» вместо «да». Хочется написать так, как сказали бы вслух: «если ответ — да или д».
Игрок отвечает «нет». Что напечатает программа?
«Вы входите в дом» — при любом ответе. Python читает условие не как «ответ равен да или д», а как (answer == "да") or ("д"). Левая половина ложна, а правая — непустая строка "д", и она считается истиной. Правильно: answer == "да" or answer == "д", сравнение с каждой стороны.
Осталось понять, почему строка "д" вообще годится в условие. Это следующая комната.
Комната шестая. Безымянный путник
В условие можно поставить не только сравнение, но и любое значение. Python решает, истинно ли оно, по одному правилу: пустое и нулевое — ложь, всё остальное — истина. Ложью считаются False, 0, 0.0, пустая строка "", пустой список и особое значение None, «ничего». Это вы уже видели в таможне типов из прошлой главы: bool("") — False, bool("0") — True.
Нажмите Enter, ничего не вводя: input() вернёт пустую строку, и сработает else. Запись if name: короче, чем if name != "":, и программисты на Python пишут именно так. А not переворачивает проверку: if not name: — «если имени нет».
Комната седьмая. Лабиринт
Самое знаменитое место старой пещеры — лабиринт, о котором игра говорила: you are in a maze of twisty little passages, all alike, «вы в лабиринте извилистых проходов, похожих друг на друга». В нашем лабиринте две развилки подряд, и вторая встречается, только если на первой повернуть налево. Блок внутри блока — вложенное условие:
Каждый уровень вложенности — ещё четыре пробела. По отступу видно, какой else к какому if относится: нижний else стоит под первым if и отвечает ему. На схеме вложенное условие — ромб внутри ветки другого ромба.
Вложенность легко превращается в лесенку, уходящую за правый край экрана, — пять уровней отступа читать уже тяжело. Часто её можно расплющить, объединив условия через and:
Эта версия читается сверху вниз без лесенки, но спрашивает о второй развилке даже того, кто на первой свернул направо. Какая лучше — зависит от игры. Хорошее правило: вкладывайте условие, когда второй вопрос имеет смысл только после ответа на первый.
Табличка у входа: match
Текстовые игры понимают команды: «север», «юг», «взять лампу». Длинная цепочка elif way == … для каждой команды утомительна, и с 2021 года (версия 3.10) в Python есть конструкция match, «сопоставить»: значение сверяют с образцами по очереди.
Вертикальная черта | значит «или», подчёркивание _ ловит всё остальное, как else. Срабатывает первый подошедший образец. Волшебное слово XYZZY пришло из Adventure: оно переносило игрока из дома под землю, мимо запертой решётки, и обратно, и с тех пор его прячут в программы как шутку для своих. Возможности match гораздо шире простого перебора строк — он умеет разбирать сложные значения на части, — но для этой главы хватит и такого.
Вся пещера
Соберём комнаты в одну игру. Ниже — полный квест: дорога, дом с лампой, клеткой и жезлом, решётка с кодом (подсказка нацарапана на стене дома), зал, птица и змея, лабиринт. Флажок alive помнит, жив ли игрок: если он погиб или упёрся в запертую решётку, следующие комнаты проверяют флажок и пропускаются. Строка lamp = cage = rod = bird = False привязывает четыре имени к одному объекту False. Это безопасно: логическое значение изменить нельзя, можно только перевесить ярлык (помните восьмой фокус?).
Играть удобнее в терминале ниже: он запускает код из ячейки выше — со всеми вашими правками — и отмечает на карте комнаты, где вы побывали. Попробуйте найти оба сокровища (за одну игру до обоих не дойти, понадобятся две), а потом добавьте свою комнату. Комната, о которой программа напечатает строку вида print("== Название =="), появится под картой, в списке ваших комнат.
== Комната ==, которые печатает игра.Сколько всего путей в этой пещере
Каждое if раздваивает путь, и число возможных прохождений быстро растёт. Только в доме три независимых вопроса «да или нет», а это $2^3 = 8$ сочетаний предметов. Умножьте на исходы решётки, на выбор в зале, на две развилки лабиринта — выйдет несколько десятков разных историй. Пройти их все вручную тяжело, поэтому в главе 11 мы научимся писать тесты, которые проверяют ветки и их границы за нас. Задача «Мост тролля» ниже — первый шаг: там сервер проверяет каждую ветку вашей комнаты.
Задания
Четыре задачи на разбор случаев. В каждой программа читает ответы через input(), и сервер проверяет её на всех ветках, включая пограничные значения, на которых условия переключаются. С таких значений стоит начинать проверку любой программы с условиями.
По григорианскому календарю, введённому в 1582 году, год високосный, если делится на 4, — кроме годов, которые делятся на 100: те високосные, только если делятся ещё и на 400. Поэтому 1900 год был обычным, а 2000-й — високосным. Программа читает год и печатает одно слово: високосный или обычный.
Заготовка права для 2024 и 2023. Проверьте её на 1900 — год делится на 4, но високосным не был.
Остаток от деления, %, знаком с первой главы: year % 100 == 0 — «делится на 100».
Порядок проверок: сначала самое строгое правило (делится на 400), потом исключение (делится на 100), потом общее (делится на 4).
Узкие условия стоят раньше широких, как в проверке заряда лампы. То же одним выражением: year % 4 == 0 and (year % 100 != 0 or year % 400 == 0). Правило про столетия нужно, потому что год длится не 365,25 суток, а около 365,2422: без него календарь убегал бы от солнца на сутки за 128 лет.
Программа читает три целых числа — длины сторон, по одному в строке, — и печатает одно из четырёх: равносторонний, равнобедренный, разносторонний или не треугольник. Из отрезков складывается треугольник, только если каждый короче суммы двух других (при равенстве отрезки ложатся в одну линию, и треугольника не выходит). Отрезки нулевой или отрицательной длины треугольника тоже не дают.
Сначала отсейте то, что вообще не треугольник: a + b <= c или … — допишите проверки для двух других сторон. Отрицательные и нулевые длины эта проверка отсеет сама; подумайте почему.
Равнобедренный — когда равны хотя бы две стороны: a == b or b == c or a == c. Равносторонний тоже под это подходит, поэтому его проверяют раньше.
Нулевые и отрицательные длины отдельно проверять не нужно: если все три неравенства треугольника выполнены, каждая сторона положительна. Сложите a + b > c и a + c > b — получится 2a + b + c > b + c, то есть a > 0; так же для b и c. И снова решает порядок веток: равносторонний треугольник заодно и равнобедренный, поэтому его проверяют раньше.
Два игрока по очереди вводят свой ход: камень, ножницы или бумага. Камень тупит ножницы, ножницы режут бумагу, бумага заворачивает камень. Программа печатает первый, второй или ничья.
Ничья уже есть. Когда побеждает первый? Выпишите три случая и соедините их через or.
Каждый случай — пара условий через and: first == "камень" and second == "ножницы". Если не ничья и не победа первого — значит, победил второй.
and связывает сильнее, чем or, поэтому каждая пара собирается первой — как умножение раньше сложения. Скобки вокруг всего условия нужны, только чтобы перенести его на несколько строк. Со словарями из главы 8 то же правило можно записать короче: словарём «кто кого бьёт».
Новая комната для квеста. Программа спрашивает, сколько у игрока монет (целое число) и есть ли у него меч (ответ да или нет), и печатает последней строкой одну из фраз:
- монет 5 или больше —
Тролль берёт пять монет и пропускает вас. - монет от 1 до 4 и есть меч —
Тролль косится на меч и берёт сколько есть. - монет нет, но есть меч —
Бой! Тролль убегает под мост. - во всех остальных случаях —
Тролль не пускает вас на мост.
Сервер проверит каждую ветку и каждую границу: 5 монет, 4, 1, 0.
Начните с самого простого случая — пяти и больше монет. Меч там не важен.
«От 1 до 4» — это 1 <= coins <= 4, но если пять и больше уже отсеяны выше, достаточно coins >= 1. «Монет нет» — coins == 0.
Тесты сервера составлены как таблица всех путей: каждое сочетание «сколько монет» и «есть ли меч» на границах условий. Вставьте комнату в свой квест: между залом и лабиринтом как раз найдётся место для моста.
Куда дальше
Наш квест умеет выбирать, но каждый вопрос задаёт один раз. Ошиблись с кодом замка — игра окончена. Чтобы спросить снова, пришлось бы скопировать кусок программы, а чтобы спрашивать, пока игрок не угадает, — копировать его бесконечно. Из зала нельзя вернуться к дороге, по лабиринту нельзя бродить. Компьютер выполняет порядка миллиарда операций в секунду, а мы пишем каждую отдельной строкой. Как заставить его повторить действие десять, тысячу, миллион раз или повторять, пока что-нибудь не случится? Об этом — глава 4.