Космическая наука с Python

Космическая наука с Python — проект «Астероид» (часть 3)

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

Предисловие

Это 23-я часть моей серии руководств по Python Космические науки с Python. Все коды, показанные в обучающих сессиях, загружены на GitHub. Наслаждайтесь!

TDD и сейчас?

В прошлый раз мы обсуждали концепцию Test Driven Development, коротко: TDD. TDD поможет нам разработать библиотеку Python для нашего проекта, которая с самого начала обеспечит меньшее количество ошибок, более высокую надежность (и качество) и ремонтопригодность. Конечно, мы разработаем новые численные модели и смоделируем сложную «вычислительную цепочку» для определения возможности обнаружения околоземных объектов (ОСЗ). Однако основные функции, такие как преобразование видимой величины объекта в соответствующую освещенность, могут быть легко охвачены подходом TDD.



Проект «Астероид (часть 2) — разработка через тестирование
Часть 22 серии руководств продолжается второй частью нашего научного проекта. Прежде чем мы углубимся в Python…towardsdatascience.com»



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

Обратите внимание: сегодня мы имеем дело с довольно простой задачей, чтобы получить представление о TDD. Любому продвинутому пользователю Python может быть скучно, но рассмотрите возможность использования концепции TDD и экстраполируйте ее на другие проблемы, с которыми вы можете столкнуться!

Давайте углубимся в это

Прежде чем приступить к программированию, убедитесь, что у вас есть виртуальная среда Python (по крайней мере версии 3.8). Я рекомендую также установить IDE, например Spyder, а также часто используемые пакеты для научной работы, такие как NumPy, Pandas, Matplotlib, scikit-learn и так далее. Вы можете рассмотреть мою самую первую статью о настройке и установке некоторых важных пакетов:



Наконец, нам нужна библиотека для тестирования. Unittest — это стандартная библиотека тестирования, которая регистрируется при установке Python. Для нашего урока TDD мы будем использовать pytest:



Прежде всего, мы создаем папку библиотеки Python с именем-заполнителем, называемым mylib.

В этой папке нам нужно создать несколько папок и файлов, чтобы выполнить минимальные требования для работающей библиотеки Python.

Сначала мы создаем папку с именем tests. pytest требует определенных имен папок и файлов для автоматического определения тестов и их выполнения. В каталоге tests имена файлов Python должны начинаться с префикса test_, а внутри этих файлов подпрограммы/функции тестирования также начинаются с test_. . Это довольно просто запомнить, а также приводит к чистой среде тестирования. Конечно, можно также установить индивидуальные имена и соглашения, но при вызове pytest необходимо передавать необычные модификации или необходимо написать файл конфигурации для набора тестов.

Добавляется вторая папка с именем general. В этом каталоге мы разместим некоторый производственный код, который можно использовать для различных основных функций библиотеки. На данный момент нет причин добавлять эту папку, поскольку мы еще не проводили никаких тестов. Ну, технически TDD просит нас не создавать код до того, как будет создан какой-либо модульный тест. Создание необработанной структуры папок совершенно нормально.

Наконец, нам нужно создать файлы __init__.py в каждом каталоге, чтобы Python мог идентифицировать содержимое этой библиотеки. Файл инициализации mylib содержит информацию:

Внутри общие и тесты есть файлы инициализации, которые соответственно импортируют/загружают файлы. Наш самый первый скрипт в общих должен содержать простые векторные операции. Файл должен быть vec.py. Соответствующий тестовый файл в tests должен называться test_general_vec.py. Общая структура каталогов выглядит так:

mylib
+-- __init__.py
+-- general
|   +-- __init__.py
|   +-- vec.py
+-- tests
|   +-- __init__.py
|   +-- test_general_vec.py

Наша первая задача: мы хотим создать функцию, которая вычисляет угол p между двумя векторами a и b:

Мы выделяем несколько вычислительных шагов, которые довольно легко понять:

  • Arccos применяется к дроби
  • Числитель: скалярное произведение двух векторов
  • Знаменатель:умножение длин (соответственно называемых нормой) двух векторов. Поскольку мы применяем это уравнение в геометрическом контексте, мы используем евклидову норму

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

Начнем с функции нормы. Мы назовем его vec_norm и поместим в general.vec. Первый тест охватывает вектор со значениями [1,0, 0,0]. По-видимому, евклидова норма этого вектора равна 1:

Мы можем выполнять модульные тесты в Spyder с помощью плагина (Spyder Blog Post) или в терминале. Мы остаемся с терминалом в этом сеансе. Внутри mylib мы запускаем pytest:

Судя по всему, модульный тест провалился. Это неудивительно, поскольку в сообщении об ошибке говорится, что в mylib.general.vec нет «vec_norm». До сих пор мы ничего не реализовали. Итак, давайте переключимся на mylib.general.vec и добавим функцию. Далее, вычисление евклидовой нормы совершенно очевидно. Не надо притворяться:

Теперь pytest прошел успешно! Идеально! Мы закончили, верно? Ну… нет. Наша реализация охватывает только двумерные векторы, и мы не тестировали менее очевидные случаи. Итак, сначала мы добавим несколько менее очевидных тестов и попробуем подойти к коду более общим образом:

Обратите внимание: после добавления нового теста необходимо выполнить pytest! Итак, в этом случае после добавления строк 9/10 и 12/13 был вызван pytest.

Теперь мы добавляем трехмерный вектор в нашу тестовую процедуру:

Результат pytest:

Наш подход в vec.py не был оптимальным и годился только для двумерных векторов. Давайте попробуем общий подход:

Мы полностью изменили нашу функцию! Теперь функция выполняет итерацию по элементам вектора и суммирует их квадраты результатов. Но обеспечивает ли новая рефакторинговая версия такие же результаты? Именно здесь TDD демонстрирует одну из своих сильных сторон: мы уже написали несколько тестов и добавили еще один. Нет необходимости беспокоиться. Если тесты пройдены: Отлично. Если тесты не проходят: Что ж, у нас есть рабочая версия для двумерных векторов…

Успех!

Для нашего варианта использования мы останемся с евклидовой нормой. Но благодаря подходу TDD мы можем легко расширить нашу функцию, чтобы предоставить больше стандартных функций, таких как:

  • Максимальная норма
  • Суммарная норма
  • P Норма
  • и так далее…

Фактически норма P с p=2 соответствует евклидовой норме. Итак, давайте еще больше обобщим нашу функцию! Добавим еще один тест…

… это не удается, поэтому мы немного редактируем нашу функцию …

Примечание. Поскольку наши первые тесты не включают параметр norm, нам нужно определить значение по умолчанию (здесь: p2) в строке 3, иначе первые тесты не пройдут. Теперь оператор if проверяет запрошенную векторную норму. На данный момент мы реализовали только норму P. Не стесняйтесь добавлять дополнительные подпрограммы нормы, такие как максимальная норма, иначе один оператор if окажется ненужным. Кроме того, норма P не может быть p=0 или отрицательной; а также значения больше 10 становятся проблемой (проверьте логику в строке 6)! Попробуйте улучшить функцию, реализуя логику с использованием подхода TDD.

Теперь, когда мы написали нашу первую функцию на основе TDD, небольшая подчистка поможет нам понять код в долгосрочной перспективе. PEP8 и Numpy Docstrings — это возможные способы создания понятного и устойчивого кода. Наша функция становится:

Та же процедура очистки применима и к процедурам тестирования. Пожалуйста, найдите время, чтобы сделать тестовые сценарии Python совместимыми с PEP8 и Numpdy Docstring.

Нашей следующей функцией будет скалярное произведение двух векторов. На этот раз мы немного ускоряем процесс. Во-первых, мы добавляем новую функцию тестирования в строки с 22 по 26 и создаем пример точечного продукта. Результат определяется вручную.

Давайте выполним pytest. Это не удается, так как у нас еще нет функции:

Мы создаем новую функцию в general/vec.py с именем vec_dotprod и реализуем очевидное решение для этой процедуры (строка 46). pytest не завершается ошибкой, и обе функции передаются! Мы проверяем функцию вторым и третьим тестом и очищаем функцию в соответствии с упомянутыми стандартами.

Наконец, мы можем определить нашу самую последнюю функцию для вычисления угла между двумя векторами. Мы вызываем функцию vec_angle, добавляем тест, и, конечно, тест не проходит при первом запуске. Поскольку у нас есть уравнение (см. выше), у нас есть очевидное решение, и мы можем реализовать его соответствующим образом. Однако мы решили вернуть углы в радианах!

Модульный тест вычисления угла может выглядеть так:

Реализуем функцию…

… и pytest не возвращает никаких ошибок. Не стесняйтесь добавлять больше тестов в качестве упражнения! Похоже, наша реализация наконец завершена. Мы сделали это, первый сеанс кодирования на основе подхода TDD.

Но некоторые вещи могут стать проблемой в долгосрочной перспективе. Давайте посмотрим на наш последний результат тестирования:

По умолчанию pytest проверяет все функции. Все тесты проходят в течение 0,02 секунды. Однако представьте себе более обширные и сложные функции, выполнение которых занимает несколько минут. Мы не хотим тестировать все функции заново, если не добавляем новые модульные тесты к старым (уже пройденным) функциям. Так как же мы можем выполнять только определенные процедуры тестирования?

К счастью, pytest предоставляет больше функций (я настоятельно рекомендую документацию), что позволяет нам тестировать отдельные функции без запуска всего каталога тестирования.



Установив так называемые маркеры, мы можем выполнить pytest для отдельных функций и классов. Эти маркеры являются декораторами Python и «украшают» каждую функцию тестирования. Наш набор тестов становится:

Как видите, мы импортируем pytest (строка 3) и определяем декораторы/маркеры в строках 5, 24 и 39 соответственно. Теперь нам нужно зарегистрировать эти маркеры в файле конфигурации с именем pytest.ini, который хранится в основном каталоге нашей библиотеки:

Без этого файла подпрограмма pytest вызывала бы предупреждения с подсказкой для настройки индивидуально установленных маркеров.

Теперь тестируем только одну функцию. Скажем: vec_norm. Наша команда pytest принимает вид (где -m указывает на использование маркеров, а vec_norm — имя зарегистрированного маркера):

pytest -m vec_norm

Только 1 тест пройден, а 2 были отменены, как и требовалось.

Заключение и перспективы

Разработка через тестирование… это кажется утомительным, верно? Ну… да. В начале требуется много дисциплины, чтобы следовать этому подходу. Но в долгосрочной перспективе человек создает функции для своего проекта, которые являются прочным фундаментом. Проверенный, безошибочный (по крайней мере, для примеров и тестов) и надежный. Не нужно беспокоиться о возможных негативных последствиях и воздействии на свои коды. Вы что-то изменили? Или вы добавили новые функции? Ваш коллега что-то изменил, и вы не уверены в результатах? Нет проблем: просто выполните уже существующие процедуры тестирования, и ваши внутренние ощущения станут измеримыми.

Ну и что дальше?

С этого момента мы будем развивать нашу библиотеку NEO Python. Мы начнем с основных функций (вычисление звездной величины, реализация настроек телескопа, загрузка самых последних данных об ОСЗ и модели и т. д.). Мы не будем рассматривать каждый шаг проекта так подробно, как в этом примере TDD. Однако мы будем использовать TDD для обеспечения определенного качества и надежности кода.

А пока я подумаю над названием библиотеки и настрою для нее дополнительный репозиторий на GitHub.

Быть в курсе,

Томас