[Перевод] Почему Python так хорош в научных вычислениях
Несколько дней назад (Оригинал заметки был опубликован 12 сентября 2017. — Здесь и далее прим. переводчика), я заметил этот твит в своей ленте:
I 'still' program in C. Why? Hint: it’s not about performance. I wrote an essay to elaborate… appearing at Onward! https://t.co/pzxjfvUs5B
— Stephen Kell (@stephenrkell) September 5, 2017
Я 'всё ещё' программирую на Си. Почему? Подсказка: дело не в производительности. Написал эссе с разбором… появится на Onward!
(Onward! — одна из конференций в составе SPLASH, посвящённая обсуждению новых идей и парадигм в программировании и размышлениям о программном обеспечении.)
Это показалось хорошим чтением на выходные — им оно и было. Главный аргумент, который приводит автор: язык Си остаётся непревзойдённым как язык системной интеграции, потому что разрешает взаимодействовать с «чужим» кодом, то есть кодом, написанным независимо и возможно даже на других языках, вплоть до ассемблера. Фактически, Си — один из немногих языков программирования, позволяющих иметь дело с любыми данными на уровне байтов. Большинство более «современных» языков запрещают такое взаимодействие во имя безопасности: вся память, к которой вы можете получить доступ — это память, выделенная с помощью безопасной среды исполнения языка. Как следствие, вы застреваете в его замкнутой вселенной.
Системная интеграция — несомненно важный аспект работы с программным обеспечением, который часто упускают из виду. И это особенно верно для научных вычислений (scientific computing), где прикладное программное обеспечение с фиксированным набором функций встречается редко. Чтобы решить научную задачу часто требуется собрать множество кусочков программ в сильно зависящее от конкретной проблемы целое, которое, возможно, будет запущено всего пару раз (смотрите также мой более ранний пост на эту тему). Это в точности та задача, которой занимается системная интеграция: собрать из кусочков единое целое, при необходимости используя связующий код. В вычислительной науке (computational science) связующий код принимает форму скриптов, потоков работ (workflows) и, в последнее время, блокнотов (notebooks). C технической точки зрения это заметно отличается от системной интеграции на уровне операционной системы, на которую ссылается Стивен Келл, но функционально это то же самое.
Статья Стивена напомнила мне о давнем плане написать в блог о том, почему Python так успешен в научных вычислениях, несмотря на его репутацию языка с плохой производительностью. Так что… вот она.
Разумеется, причин успеха Python много, но одна из них — он отлично справляется с задачами системной интеграции. Python обладает двумя особенностями, которые я считаю важными в этом деле, и которые не поддерживаются многими другими языками. Одна — типы данных, явно разработанные для связывания (interfacing); другая — утиная типизация в сочетании с маленьким, но гибким набором стандартных интерфейсов.
Первый тип данных Python, разработанный для связывания в контексте научных вычислений — старый добрый массив NumPy, который на деле оказывается старше NumPy, будучи представлен в 1995 году его предшественником, Numeric. Массивы — это один из типов данных, являющихся «хлебом насущным» научных вычислений, вплоть до того что это единственный тип, доступный в языках вроде Fortran 77 или APL. Реализация массивов в Numeric была разработана для использования с той же схемой расположения данных, что и в Фортране с Си, чтобы обеспечить взаимодействие с библиотеками на Фортране и Си, доминировавшими в научных вычислениях в 1995 году (и до сих пор, хоть и в меньшей степени). За Numeric и, позднее, NumPy всегда стояла идея использовать Python как связующий язык для библиотек на Фортране и Си, и добиваться скорости, делегируя критичные по времени операции коду, написанному на этих языках.
Второй тип данных Python, спроектированный для связывания — это memoryview, связанный с buffer protocol. Здесь Python ближе всего подбирается к Си-образному доступу к памяти. Буферный протокол позволяет разным типам данных Python получать доступ к внутренностям друг друга на уровне байтов. Типичным примером использования может быть тип данных изображения (например из Pillow), с доступом к представлению изображения в памяти через тип массива (например из NumPy), что позволяет реализовать алгоритмы работы с изображениями в виде операций над массивами.
Третий и наименее известный тип данных Python для связывания — это capsule, заменяющий более ранний CObject. Капсулы существуют исключительно на благо написанных на Си модулей Python, которые с помощью связующего кода на Python могут обмениваться друг с другом непрозрачными данными, даже несмотря на то, что сам связующий код не может как-либо проверить или обработать данные. Типичный пример: обернуть указатели на функцию на языке Си в объект Python так, чтобы связующий код на Python — скрипт, например, — мог передать функцию на Си из одного модуля коду на Си в другом модуле.
Все эти интерфейсные типы данных служат посредниками между кодом на Python и Си, хотя зачастую системный интегратор на Python вообще не подозревает об использовании кода на Си. Другая особенность Python для системной интеграции, утиная типизация со стандартными интерфейсами, способствует связыванию независимо написанных модулей Python. Под «стандартными интерфейсами» я понимаю интерфейсы последовательности (sequence) и словаря (dictionary), а также стандартные имена методов для перегрузки операторов.
Чтобы увидеть, почему эта особенность важна, посмотрим на статически типизированные языки, в которых она намеренно отсутствует. В качестве конкретного примера возьмите многомерные массивы Java. Они не являются частью языка или стандартной библиотеки, но могут быть реализованы поверх них с разумными усилиями. Фактически существует несколько реализаций на Java, из которых вы можете выбирать. В этом и кроется проблема. Предположим, вы хотите использовать библиотеку для быстрого преобразования Фурье (БПФ), основанную на реализации массивов «A», вместе с библиотекой линейной алгебры, основанной на реализации массивов «B». Не повезло — массивы из «A» и «B» имеют разные типы, так что вы не можете использовать выходные данные БПФ как вход для системы решения линейных уравнений. Не имеет значения, что в основе лежат одни и те же абстракции, и даже что реализации имеют много общего. Для компилятора Java типы не совпадают, и точка.
Python не полностью свободен от этой проблемы. Вполне можно писать код на Python или код в модуле на Си, который ожидает точный тип данных в качестве входа, а иначе выбрасывает исключение. Но для кода на Python это будет считаться плохим стилем, и в модулях на Си для Python тоже, за исключением тех, где требуется производительность или совместимость с другим кодом на Си. Там, где это возможно, от программистов на Python ожидается использование стандартных интерфейсов для работы с данными. Например, итерация и индексирование работают одинаково для массивов и встроенных списков. Для операций, не поддерживаемых стандартными интерфейсами, от программистов на Python ожидается использование методов Python, также подлежащих утиной типизации. На практике, независимо реализованные типы Python намного более интероперабельны, чем независимо реализованные типы Java. В конкретном случае n-мерных массивов, у Python был шанс принятия подавляющим большинством единой реализации, что связано с вопросами скорее социальными и историческими, чем техническими.
Наконец, даже несмотря на то, что Python — довольно хороший выбор для системной интеграции в научных вычислениях, разумеется есть ограничения, как раз того рода, что описывает Стивен Келл в своём эссе: сочетание кода Python с кодом на других языках с управляемой памятью, скажем, R или Julia, требует много труда, и даже после этого остаётся хрупким, потому что требуются ухищрения, основанные на недокументированных деталях реализации. Подозреваю, что единственным решением может быть появление нейтральных по отношению к языкам объектов данных, поддерживающих сборку мусора и предоставляемых как сервис уровня операционной системы, сохраняющий возможность неуправляемого (unmanaged) доступа на уровне байтов, а-ля Си. Самая близкая из существующих технологий, о которой мне известно — CLR от Microsoft, более известная под коммерческим названием .NET. Её реализация теперь имеет открытый исходный код и работает на множестве платформ, но её происхождение «только для Windows» и прочные связи с огромной майкрософтовской библиотекой являются препятствием для принятия в традиционно Unix-центричном сообществе людей, занимающихся научными вычислениями.