LANG·I Язык Глава 11 из 65
Отчёт комиссии
4 июня 1996 года «Ариан-5» разрушилась в первом же полёте, примерно через 39 секунд после зажигания двигателя: одно число не поместилось в шестнадцать бит. Глава устроена как отчёт комиссии по расследованию — и каждая её рекомендация становится вашим инструментом: исключения, проверки, тесты, отладка.
Язык
- 01 Первая программа
- 02 Переменные
- 03 Условия
- 04 Циклы
- 05 Функции
- 06 Списки
- 07 Строки
- 08 Словари
- 09 Рекурсия
- 10 Функции-значения
- 11 Ошибки и тесты вы здесь
- 12 Объекты
Опирается на: 05 · Свои слова
Что вы унесёте из главы
- ловить и обрабатывать ошибки: try, except, else, finally, а также поднимать свои через raise
- записывать предположения кода проверками и писать тесты, которые ловят реальные ошибки
- искать ошибку методично: читать трейсбек, печатать телеметрию, делить пополам
Прошлая глава кончилась предупреждением: чем короче и мощнее строки, тем легче в них спрятаться ошибке и тем дороже она обходится. Насколько дорого, показывает одна из самых известных аварий в истории программирования. Эта глава устроена как отчёт комиссии, которая её расследовала, и разделы в ней те же, что в отчёте: обстоятельства, цепочка событий, выводы, рекомендации. Каждая рекомендация комиссии станет инструментом, которым вы будете пользоваться в любой своей программе.
Предисловие
Отчёт комиссии Лионса — образец того, как расследуют ошибки. Он короткий, его можно прочесть за полчаса, и в нём ищут не виноватых, а причины. Комиссия шла от взрыва назад во времени, звено за звеном, пока не нашла первую причину, — и закончила списком из четырнадцати рекомендаций. Мы пройдём тот же путь.
1. Отказ
У ракеты есть два «вестибулярных аппарата» — инерциальные системы отсчёта, в отчёте SRI (от французского Système de Référence Inertielle). Каждая — коробка с лазерными гироскопами, акселерометрами и собственным компьютером, который по их показаниям вычисляет, как ракета повёрнута и с какой скоростью летит. Эти данные бортовой компьютер получает по шине и по ним поворачивает сопла двигателей. Чтобы отказ одной коробки не погубил полёт, их две: одна работает, вторая в горячем резерве. Если бортовой компьютер видит, что активная система отказала, он сразу переключается на резервную.
Ниже — полёт 501 по секундам, как его восстановила комиссия; время можно двигать. Переключатели внизу задают вопросы «а что было бы, если…», и каждый проверяет одну из рекомендаций комиссии.
Через 36,7 секунды после H0 резервная система SRI 1 объявила себя неисправной и замолчала. Примерно через 0,05 секунды по той же причине замолчала активная SRI 2. Резервная к этому моменту молчала уже с прошлого такта обмена данными (такт длится 72 миллисекунды), так что переключаться стало не на что. Хуже того: перед отключением SRI 2 отправила в шину диагностическое сообщение о своей поломке, а бортовой компьютер принял эти биты за данные о полёте. По ним выходило, что ракета сильно отклонилась, и компьютер скомандовал исправить отклонение, которого не было, — до упора повернул сопла ускорителей, а чуть позже и основного двигателя. Ракета резко повернулась. Угол атаки — угол между её осью и встречным потоком воздуха — превысил двадцать градусов, аэродинамические нагрузки начали рвать её на части, и примерно через 39 секунд после H0 сработала система самоликвидации.
2. Цепочка событий
Комиссия восстановила цепочку от конца к началу: взрыв — из-за угла атаки; угол атаки — из-за поворота сопел; поворот — из-за неверных данных; неверные данные — из-за отказа обеих SRI; отказ — из-за исключения в программе. Так же вы читаете трейсбек из главы 1: снизу вверх, от того, что случилось, к тому, где.
Первым звеном оказалась функция, которой в полёте вообще нечего было делать. До старта инерциальную систему нужно выставить — понять, где у неё низ и где север. Этим занимается юстировка. На прежних «Ариан» её оставляли работать ещё пятьдесят секунд после включения полётного режима SRI, то есть и первые десятки секунд полёта: если отсчёт остановят в последние секунды, его можно возобновить, не дожидаясь новой юстировки, которая занимает 45 минут и больше. За всю историю этой возможностью воспользовались один раз — в 1989 году, в полёте 33. «Ариан-5» она была не нужна вовсе, но программу SRI перенесли с «Ариан-4» почти без изменений: зачем трогать то, что хорошо работает.
Юстировка считала, среди прочего, величину BH — горизонтальное смещение, связанное с горизонтальной скоростью ракеты. В какой-то момент BH, вычисленную как 64-битное дробное число, нужно было переписать в 16-битное целое со знаком. Траектория «Ариан-5» в первые секунды другая: горизонтальная скорость нарастала в пять раз быстрее, чем у «Ариан-4». Через 36,7 секунды после H0 BH стала больше, чем помещается в шестнадцать бит.
Шестнадцать бит
Шестнадцать двоичных разрядов дают $2^{16} = 65\,536$ разных комбинаций. Если одну половину отдать отрицательным числам, а другую — нулю и положительным, получится диапазон от $-32\,768$ до $32\,767$. Как именно знак записывается в битах, разберём в главе 28. Сейчас хватит того, что число 40 000 в такую ячейку не помещается: случается переполнение.
Целые числа Python не переполняются никогда: 2 ** 1000 из главы 0 вышло числом из 302 цифр, и Python напечатал их все. Но регистры процессора имеют фиксированную ширину, а почти все остальные языки и библиотеки для быстрых вычислений работают с регистрами напрямую. Такова и numpy — библиотека, на которой держатся научные вычисления в Python. В ней есть тип int16, точно такой же, как в SRI.
Каждая строка ведёт себя по-своему. В первой 32 767 плюс 1 дало −32 768: ячейка, как одометр старой машины, перевалила через край и пошла по кругу, а numpy выдал предупреждение и продолжил считать. Во второй, при переписывании массива в int16, число 40 000 молча превратилось в −25 536, а 70 000 — в 4464. В третьей numpy отказался создавать int16 из слишком большого числа Python и бросил OverflowError; так он поступает с версии 2.0, а версии 1.x заворачивали число по кругу (последние из них — с предупреждением). Тихо испортить число или громко отказаться: при переполнении любая машина выбирает одно из двух. Поиграйте с регистром.
Программа SRI была написана на языке Ада, и слишком большое число в ней не испортилось молча: перевод оборвался исключением, в отчёте оно названо Operand Error, «ошибка операнда». С этой стороны всё случилось правильно: машина заметила, что число не помещается, и сказала об этом. Вопрос в том, кто её услышал.
Исключение
Когда функция не может выполнить свою работу, она вместо значения поднимает исключение — объект, который описывает, что пошло не так. Выполнение функции на этом прекращается, и исключение летит к той функции, которая её вызвала. Если та его не ловит, оно летит дальше, к её вызывающей, и так по всему стеку вызовов из главы 5. Если исключение долетело до самого верха и никто его не поймал, программа останавливается и печатает трейсбек — путь, который оно пролетело. Вот уменьшенная копия SRI. Числа в ней выдуманные: значений BH в открытом отчёте нет.
Слово raise — «подними» — бросает исключение. В трейсбеке видно все три кадра, через которые оно пролетело: flight_cycle, alignment, to_int16. Последняя строка программы так и не выполнилась. Нажмите «Шаги»: на 37-й секунде кадры исчезают один за другим.
Поймать исключение можно конструкцией try — «попробуй»:
Python выполняет блок try. Если в нём возникло исключение, указанное в except, управление прыгает туда, и программа живёт дальше. Блок else выполняется, только если исключения не было. А блок finally выполняется всегда, даже когда исключение полетело дальше: туда кладут то, что нужно сделать в любом случае, например закрыть файл или записать в журнал. В ячейке finally напечатал своё даже после return.
Исключения бывают разных видов, и виды выстроены в родословную. ZeroDivisionError и OverflowError — частные случаи ArithmeticError, KeyError и IndexError — частные случаи LookupError, а все они происходят от Exception. Обработчик except ArithmeticError ловит и деление на ноль, и переполнение.
Запись except LookupError as e кладёт пойманное исключение в переменную e: у него можно узнать тип и сообщение. Куда именно долетит исключение, зависит от того, где стоят обработчики и что они ловят. Проверьте себя.
main → flight_cycle → alignment → to_int16. Выберите, что пойдёт не так внизу и какие обработчики стоят в каждой функции, и бросьте исключение. Справа — та же программа на Python; кнопка «Проверить в Python» запускает её на сервере.Что сделал обработчик
Исключение в SRI поймали. Обработчик был и выполнил всё, что требовала спецификация: на любое исключение сообщить об отказе в шину, записать обстоятельства в энергонезависимую память (её прочли, когда коробки нашли среди обломков) и остановить процессор. На Python это выглядело бы примерно так:
Решение выключать процессор отчёт объясняет так: в программе «Ариан» привыкли бороться со случайными поломками железа. Если отказ случаен — сгорела микросхема, — разумно выключить больной блок и перейти на резервный. Но ошибка в программе не случайна. В обеих SRI стояла одна и та же программа, получала одни и те же данные и потому отказала одинаково, с разницей в пять сотых секунды. Две исправные коробки выключились из-за функции, которая в полёте была не нужна. Комиссия сформулировала это так: исходили из того, что программа верна, пока не доказано обратное, — а надо исходить из того, что она содержит ошибки, пока не доказано, что их нет.
Замените в ячейке return None на return attitude, и обе системы продолжат отдавать правильную ориентацию, откажет только юстировка. Так и советует одна из рекомендаций комиссии — до них дойдём через раздел.
Почему не поймали испытания
Ракеты испытывают годами, и «Ариан-5» не исключение. Но SRI проверяли на стенде «как коробку» — на все внешние воздействия, причём с запасом сверх нужного. А вот прогнать её вместе со всей системой управления по траектории «Ариан-5» не стали: траекторию по общему согласию не включили в требования к SRI. В больших имитациях полёта, где участвовало реальное бортовое оборудование, вместо обеих SRI работали их программные модели: в 1992 году решили, что сами коробки уже проверены. Доводы у этого решения были, и комиссия признала их технически обоснованными. Но, как она же пишет, будь такое испытание проведено, механизм отказа обнаружился бы. После аварии программу SRI прогнали на компьютере с записанной траекторией полёта 501, и цепочка событий, которая привела к отказу обеих SRI, воспроизвелась точно.
И ещё одна деталь. Переменных, которые могли переполниться при переводе в целое, в юстировке нашли семь. Четыре защитили, три оставили без защиты: компьютер SRI не должен был загружаться больше чем на 80 %, а эти три величины, по расчётам, были ограничены физически или имели большой запас. Для BH расчёт оказался неверен. Ссылки на обоснование этого решения в самом коде не было.
3. Выводы
Причина отказа, по словам комиссии, — полная потеря данных о положении ракеты через 37 секунд после зажигания двигателя (через 30 секунд после отрыва от стартового стола). Потеря вызвана ошибками в спецификации и проектировании программы инерциальной системы. Обзоры и испытания, проведённые за годы разработки, не включали достаточного анализа и проверки этой системы и всей системы управления, которые могли бы обнаружить отказ заранее.
Для аварии должно было совпасть многое: ненужная функция работала в полёте; переменную оставили без защиты; траектория отличалась от прежней; обработчик выключал процессор; обе системы были одинаковыми; на испытаниях вместо коробок работали их модели. Уберите любое звено, и цепочка порвётся. Поэтому и рекомендаций в отчёте много.
4. Рекомендации
Из четырнадцати рекомендаций комиссии возьмём те, что годятся для любой программы, не только ракетной, и превратим каждую в инструмент.
R1. Ничего лишнего
Самый дешёвый код — тот, которого нет: в нём нечему сломаться. Каждая строка, которая работает «на всякий случай», — это место, где может спрятаться ошибка. Перед тем как перенести кусок старой программы в новую, спросите: что он здесь делает?
R3. Лучшая оценка вместо молчания
Обработчик исключения решает, что делать дальше, и решения бывают разные. Иногда правильно упасть громко: лучше остановить программу, чем считать с испорченными данными. Иногда правильно продолжить с запасным значением, как советует R3. Неправильно только не думать об этом вовсе. У хорошего обработчика два правила. Первое: ловите только то исключение, которое ожидаете и знаете, как обработать, — except ZeroDivisionError, а не except Exception. Широкий обработчик проглотит и ошибки, о которых вы не думали, — например, опечатку в имени переменной. Второе: try должен охватывать как можно меньше кода — только те строки, где ожидается ошибка.
Напишите функцию safe_divide(a, b, default=None), которая возвращает a / b, а при делении на ноль — значение default. Другие ошибки глотать нельзя: safe_divide("1", 2) должна упасть с TypeError, как и обычное деление, — иначе опечатка в данных останется незамеченной.
Оберните деление в try. Какое исключение бросает 1 / 0? Ответ — в последней строке трейсбека.
except ZeroDivisionError: return default. Именно этот тип: с Exception в None превратится и TypeError.
Можно и проверить заранее: if b == 0: return default. Оба стиля встречаются; в Python чаще пишут «попробуй и поймай», потому что при проверке заранее легко забыть какой-нибудь случай. Здесь работают оба: и 0, и 0.0 равны нулю. А вот except Exception или голый except: провалят тест со строкой. Ошибка в данных молча станет None и всплывёт где-нибудь далеко, как диагностические биты SRI, которые бортовой компьютер принял за данные полёта.
R5. Назвать предположения
Каждая строка кода что-то предполагает. int(text) предполагает, что в строке число. sum(mags) / len(mags) — что список не пуст. Перевод BH в 16 бит предполагал, что BH меньше 32 768, и это предположение жило в документах, но не в коде. Рекомендация R5 говорит: вытащите предположения на свет. В Python для этого два средства.
Первое — raise со своим, понятным исключением. Если функция получила то, с чем работать не может, лучше ей сразу и ясно сказать об этом, чем выдать мусор. Своё исключение объявляют как наследника подходящего встроенного; слово class подробно разберём в следующей главе, а пока это рецепт в две строки:
Второе — assert, «утверждаю». Это проверка, которая молчит, пока условие верно, и поднимает AssertionError, когда нет. В ней записывают то, что программист считает невозможным: если это всё-таки случилось, в программе ошибка.
Опечатка поймана в тот момент, когда попала в функцию. Без проверки она всплыла бы через тысячу строк, когда средняя магнитуда 46,6 испортила бы отчёт. Чем два средства отличаются? raise — для ошибок, которые могут случиться при правильной программе: человек ввёл буквы вместо возраста, файл не нашёлся. assert — для ошибок в самой программе. Python можно запустить с ключом -O, и тогда все assert пропускаются, поэтому проверять ими ввод пользователя нельзя.
Ввод человека — главный источник ошибок, которые случаются при правильной программе. Программа ниже ловит ValueError, который бросает int, и спрашивает снова, пока не получит число. Запустите её и попробуйте ответить «сорок». А в задаче после неё вы сами поднимете исключение — своё, с понятным именем.
Объявите исключение AgeError — наследника ValueError — и напишите функцию parse_age(text). Она получает строку, которую ввёл человек, и возвращает возраст целым числом. Пробелы и переводы строки по краям не мешают: parse_age(" 42\n") == 42. Если в строке не целое число или возраст вне диапазона от 0 до 150 включительно, функция поднимает AgeError с понятным сообщением — именно AgeError, а не обычный ValueError.
int("abc") сам бросает ValueError. Поймайте его и вместо него поднимите своё: внутри except ValueError: напишите raise AgeError(f"«{text}» — не число").
int принимает и "-5". Проверку диапазона сделайте отдельно, после превращения в число. int("42.5") тоже бросает ValueError, так что дробные числа отсеются сами.
Тот, кто вызывает parse_age, может ловить AgeError и не знать, какие исключения бросает int внутри: детали спрятаны. А раз AgeError — наследник ValueError, старый код с except ValueError тоже продолжит работать. Хвост from None необязателен: он говорит Python не показывать в трейсбеке исходную ошибку int, раз мы заменили её своей, понятной. Между прочим, int сам отбрасывает пробелы по краям, но strip делает намерение явным, а сообщение — чище.
Следующая задача — про сам регистр SRI и про число, которое в него не помещается. Исключений в ней нет, только арифметика.
Напишите две функции. wrap16(n) получает целое число любой величины и возвращает то, что окажется в 16-битной ячейке со знаком, если записать туда $n$ так, как это делают целая арифметика C и numpy: по кругу. Например, wrap16(32768) == -32768, wrap16(40000) == -25536, wrap16(-1) == -1. clamp16(x) — стратегия R3, «лучшая оценка»: отбросить у числа дробную часть, как int, а если результат не помещается, вернуть ближайшую границу: clamp16(40000.7) == 32767, clamp16(-2.7) == -2. Бесконечность тоже надо обработать: clamp16(float("inf")) == 32767.
По кругу — значит по модулю $2^{16} = 65\,536$. Остаток n % 65536 в Python всегда от 0 до 65 535, даже для отрицательных $n$. Осталось сдвинуть верхнюю половину вниз.
Для wrap16: (n + 32768) % 65536 - 32768. Для clamp16 попробуйте int(float("inf")) — получите OverflowError. Значит, границы надо проверять до вызова int.
Сдвиг на 32 768 переводит диапазон $[-32\,768, 32\,767]$ в $[0, 65\,535]$, остаток заворачивает число по кругу, обратный сдвиг возвращает знак. В clamp16 сравнения идут до int: бесконечность сравнивается с числами без ошибок, а в целое не превращается. Вместо x >= 32767 можно было написать > 32767: для 32 767,5 оба варианта дают 32 767, так что здесь выбор ни на что не влияет. В задаче ниже мутанты ловят как раз такие места.
R2, R10, R11. Испытывать на настоящих данных
В главе 1 мы завели привычку проверять ответ на примере, который знаешь заранее. Теперь превратим её в инструмент. Модульный тест — маленькая функция, которая вызывает проверяемый код на известном входе и через assert сравнивает результат с ожидаемым. Тесты пишут один раз и запускают после каждого изменения программы: если что-то сломалось, они скажут сразу.
Последние строки — маленький запускатель тестов. Держится он на мысли из главы 10, что функции — обычные значения: globals() отдаёт словарь всех имён программы, и функции из него можно перебрать и вызвать. В больших проектах берут готовый запускатель, чаще всего pytest: он сам находит функции test_… во всех файлах, запускает их и печатает отчёт, а для проверки исключений у него есть pytest.raises.
Обычный случай и так работает, поэтому хороший тест проверяет края. На граничных случаях программы ломаются чаще всего: пустой список, один элемент, ноль, самое большое и самое маленькое допустимое значение и соседи по ту сторону границы, повторы, огромный вход. В test_edges стоят сами границы, 32 767 и −32 768. В test_overflow — их соседи снаружи. Траектория «Ариан-5» была именно таким краем, о котором никто не подумал.
Помните строку документации из главы 5? В ней мы записывали пример «digit_sum(1843) == 16». Если записать его в виде сеанса Python, модуль doctest проверит его автоматически:
doctest находит в строке документации строки, начинающиеся с >>>, выполняет их и сравнивает результат со строкой ниже. Поменяйте 16 на 17 и запустите снова — увидите, как выглядит провал. Документация, которая проверяет сама себя, не устареет незаметно. Об этом рекомендация R12: уделять документам-обоснованиям столько же внимания, сколько коду.
Кто проверит тесты
Тесты прошли — значит ли это, что ошибок нет? Эдсгер Дейкстра ответил на это в 1970 году: «Тестирование программы может показать присутствие ошибок, но никогда — их отсутствие». А ещё тесты бывают слабыми и проверяют то, что и так не ломается.
Чего стоят ваши тесты, можно узнать, если нарочно испортить программу и посмотреть, заметят ли они. Возьмём to_int16 и сделаем несколько мутантов — копий, в каждой из которых одна маленькая ошибка: > заменён на >=, пропущена проверка, int заменён на round. Упал на мутанте хоть один тест — мутант «убит». Прошли все — мутант выжил: такую ошибку ваши тесты пропустили бы. Идею мутационного тестирования предложил ещё студентом Ричард Липтон в 1971 году, а в 1978-м Ричард Демилло, Липтон и Фредерик Сейворд описали её в статье «Подсказки по выбору тестовых данных».
to_int16 каждую испорченную копию и запускает ваши тесты из ячейки выше. Дописывайте тесты в ячейку и выпускайте мутантов снова. Нажмите на выжившего, чтобы увидеть, что в нём испорчено. Один из мутантов с подвохом.Два теста из ячейки не убивают никого: 42 и −100 далеко от всех границ, а целые числа не замечают ни round, ни заворачивания. Допишите тесты так, чтобы поймать всех пятерых. Мутантами пользуются и в промышленности: в Google, по рассказу инженеров компании (2018), выживших мутантов показывают авторам кода прямо во время проверки кода коллегами. Мутанты — маленькие изменения, а реальные ошибки чаще всего тоже маленькие, поэтому тест, который убивает мутантов, ловит и живые опечатки.
Напишите тесты для функции to_int16 из этой главы: функции с именами, начинающимися на test_, которые через assert проверяют её поведение. Сервер запустит ваши тесты сначала на правильной to_int16 — там все они должны пройти, — а потом на пяти мутантах из зоопарка выше, кроме двойника. Каждого мутанта должен убить хотя бы один ваш тест.
Мутанты прячутся на границах. Проверьте сами границы — 32 767 и −32 768 — и их соседей снаружи, которые должны вызывать OverflowError. С обеих сторон: сверху и снизу.
Ещё два мутанта — про дробную часть. Что должна вернуть to_int16(2.7)? А to_int16(32767.5) — это переполнение или нет?
Чтобы проверить, что исключение есть, вызовите функцию в try, в except OverflowError напишите return, а после блока — assert False, "нет исключения".
Каждая строка здесь убивает кого-то. 32767 — мутанта с n >= 32767. -40000.0 — мутанта без нижней проверки. 2.7 — мутанта с round. 32767.5 — мутанта, который проверяет границы до отбрасывания дробной части. А любое переполнение — мутанта, который вместо исключения молча заворачивает число по кругу. Двойника не убивает никто: после int число n целое, а для целых n > 32767 и n >= 32768 — одно и то же условие, так что это та же самая программа, записанная иначе. Такие мутанты — вечная головная боль метода: отличить двойника от выжившего мутанта в общем случае не может никакая программа, почему — узнаете в главе 56.
R7. Больше телеметрии
Комиссии повезло: обе SRI нашлись среди обломков, и их память удалось прочесть. Иначе часть цепочки осталась бы догадкой. Ваша программа — тоже улетевшая ракета, и её телеметрия — то, что она печатает. Отладка начинается с того, чтобы перестать гадать и посмотреть, что происходит внутри. Вот четыре приёма, от простого к хитрому.
Прочитать трейсбек. Снизу вверх: тип ошибки, сообщение, строка, путь через функции. Многие ошибки на этом и кончаются.
Напечатать. Самая старая и до сих пор самая полезная техника — print в подозрительном месте. В f-строке есть удобная форма: print(f"{bh=}") печатает и имя, и значение: bh=40000.0. Печатайте значения, в которых сомневаетесь, и сравнивайте их с ожидаемыми; строка «здесь» скажет немного. Кнопка «Шаги» в ячейках этого курса — та же печать, только автоматическая, на каждом шаге.
Делить пополам. Программа работает на маленьком файле и падает на большом. Возьмите половину файла: упала — ошибка в этой половине, нет — в другой. Каждый запуск отбрасывает половину подозреваемых, и среди миллиона строк виновник находится за двадцать запусков: это двоичный поиск из главы 0 в роли отладчика. Такая строка, оказывается, есть и в каталоге землетрясений. Вот обработка, которая берёт логарифм глубины и на всём каталоге падает. Найдём виновника, не читая трейсбек:
У виновника глубина 0,0 км, а логарифма нуля не существует. На девятнадцать тысяч записей ушло четырнадцать запусков. Тот же приём работает и с кодом. Вчера программа работала, сегодня нет, а изменений между этими днями сотня: проверьте середину истории изменений, потом середину половины… Система контроля версий git делает это сама, командой git bisect; о git — в главе 40.
Рассказать уточке. В книге «Программист-прагматик» Эндрю Ханта и Дэвида Томаса (1999) есть история про программиста, который держал на столе резиновую уточку и объяснял ей свой код строка за строкой. Звучит как шутка, но работает чаще, чем кажется: чтобы объяснить, надо проговорить каждое предположение — «здесь список не пуст, потому что…» — и на одном из них вы запнётесь. Это та же рекомендация R5, только вслух. Уточку можно заменить коллегой, кошкой или пустым сообщением в чате, которое вы так и не отправите.
Куда дальше
Через четыре года после аварии четыре новых спутника Cluster улетели в космос — на российских «Союзах», по два за раз, — и проработали на орбите много лет. «Ариан-5» после доработок летала до 2023 года и вывела в космос, среди прочего, телескоп «Джеймс Уэбб». Ошибки не исчезли, но с тех пор их ищут иначе.
У вас теперь есть ящик с инструментами надёжности: исключения, проверки, тесты, отладка. А программы, которые мы пишем, становятся длиннее и населённее. Счёт в банке, персонаж игры, кролик на острове — у каждого своё состояние и своё поведение, и таких сущностей тысячи. В словарях и разрозненных функциях им тесно. Вы уже дважды написали загадочное слово class, объявляя свои исключения. В главе 12 мы разберём, что оно значит, и заселим остров кроликами и лисами, чтобы посмотреть, как их численность качается год за годом.