Python против Julia: это также касается согласованности
Основными преимуществами Julia перед Python, безусловно, являются его скорость и такие концепции, как множественная диспетчеризация. Но это еще не все: в повседневном использовании постоянство играет важную роль.
Насколько легко (или сложно) изучать и использовать язык программирования, в значительной степени зависит от того, насколько последовательно можно использовать и применять различные конструкции этого языка. Высокая степень согласованности также помогает избежать ошибок, что делает ее важным аспектом для создания качественного программного обеспечения. В этой статье я покажу, насколько хорошо Джулия и Python работают в этой области.
Используя диапазоны и индексирование в качестве примера, я сначала проиллюстрирую, как эти два языка различаются в использовании этих понятий, а затем объясню их обоснование.
Диапазоны
В программировании диапазон чисел, определяемый нижней и верхней границами, является часто используемой концепцией. Давайте посмотрим, как это применяется в обоих языках.
Диапазон целых чисел
Если нам нужен, скажем, диапазон целых чисел от 4 до 10, это выражается следующим образом:
Python: range(4,11) Julia: 4:10
В Julia нижняя граница и верхняя граница включаются при указании диапазона, тогда как в Python нижняя граница включает, а верхняя граница исключает. Таким образом, мы должны указать верхнюю границу 11, если нам нужны числа до 10 в Python.
Диапазон элементов из массива
Если у нас есть массив a
и мы хотим извлечь элементы с 4-го по 10-й, мы должны написать:
Python: a[3:10] Julia: a[4:10]
В Python индексация массивов (списков) начинается с нуля, поэтому 4-й элемент имеет индекс 3. И снова верхняя граница исключающая. Таким образом, чтобы указать, что нам нужны элементы до 10-го элемента (имеющего индекс 9), мы должны поместить туда 10.
Как мы видим здесь, в Юлии используется то же самое выражение, что и выше. И это не только одно и то же выражение, но и тот же (диапазон) объект. Его можно использовать «автономно», как указано выше, или передать в качестве аргумента массиву для индексации.
Диапазон для извлечения случайных чисел из
Следующие выражения используются для генерации случайных чисел в диапазоне от 4 до 10:
Python: randint(4,10) Julia: rand(4:10)
В Python верхняя граница в этом случае является инклюзивной, что отклоняется от общепринятой концепции. В Julia мы передаем точно такой жеобъект диапазона, представленный выше, в качестве аргумента функции rand
.
Представляем размер шага
Если нам не нужен элемент каждый в пределах диапазона, но, например, только каждый второй элемент, это можно указать на обоих языках с помощью размера шага (в этом примере размер 2 вместо 1). Итак, чтобы получить каждое второе число от 4 до 10, напишем:
Python: range(4,11,2) Julia: 4:2:10
В Julia эта расширенная версия также является просто объектом диапазона. Таким образом, его можно использовать в любом месте, где может быть указан объект диапазона, особенно в случаях, описанных выше, для указания поддиапазона массива или для указания диапазона, из которого берутся случайные числа.
Julia: a[4:2:10] rand(4:2:10)
Это невозможно в Python.
Другие виды диапазонов
Конечно, существуют не только диапазоны целых чисел. В следующем примере показан диапазон из трех почасовых меток времени 8 апреля 2022 года (с 12:00 до 14:00):
Python: pandas.data_range(start='08/04/2022 12:00', periods=3, freq='H') Julia: DateTime(2022,4,8,12):Hour(1):DateTime(2022,4,8,14)
Как видно, в Julia используется тот же базовый синтаксис (lower:step:upper
), что и в случае целочисленных диапазонов.
В Julia это можно применить и к другим типам данных, например к символам. Здесь мы указываем диапазон символов от «d» до «k»:
Julia: 'd':'k'
Это невозможно в Python.
Индексация
Второй пример, используемый в этой статье для демонстрации того, насколько последовательно (или непоследовательно) применяется концепция, — это индексация структур данных, подобных двумерным массивам.
Доступ к элементу в матрице
Чтобы получить доступ к элементу во 2-й строке и 4-м столбце матрицы m
(двумерный массив), мы пишем:
Python: m[1][3] Julia: m[2,4]
В Python используется список списков (поскольку в базовой библиотеке нет n-мерных массивов). Поэтому индексирование такой структуры представляет собой двухэтапный процесс (и требует в два раза больше скобок). И снова, поскольку в Python индексация начинается с нуля, 2-я строка имеет индекс 1, а 4-й столбец имеет индекс 3.
Доступ к элементу в DataFrame
Чтобы получить элемент во 2-й строке и 4-м столбце DataFrame df
(который представляет собой двумерную структуру данных, похожую на таблицу базы данных), мы должны написать:
Python: df.iloc[1,3] Julia: df[2,4]
У Джулии это точно такое же обозначение, как и выше.
В отличие от списка списков, используемого для представления матрицы в Python, DataFrame представляет собой настоящую двумерную структуру. Поэтому индексы можно записывать внутри одной пары скобок.
Доступ к ряду элементов в DataFrame
У каждого столбца в DataFrame есть имя. Эти имена также можно использовать для ссылки на столбец. Итак, если нам нужны элементы из строк с 4 по 10 в столбцах с именами «A» и «B», мы пишем:
Python: df.loc[3:10,[“A”,“B”]] Julia: df[4:10,[“A”,“B”]]
Присвоить значение элементу матрицы
Чтобы присвоить значение 5 элементу матрицы во 2-й строке и 4-м столбце матрицы m
, мы должны написать:
Python: m[1][3] = 5 Julia: m[2,4] = 5
Присвоить значение элементу в DataFrame
Если мы хотим присвоить значение 5 элементу во 2-й строке и 4-м столбце DataFrame df
, используются следующие выражения:
Python: df.iat[1,3] = 5 Julia: df[2,4] = 5
Чтобы присвоить это значение элементу во 2-й строке столбца «А», мы пишем:
Python: df.at[1,”A”] = 5 Julia: df[2,”A”] = 5
Краткое содержание
Для индексации матрицы в Python (которая представляет собой список списков) используется запись, состоящая из двух пар скобок, тогда как для DataFrames одна пара скобок в сочетании с должен использоваться вызов метода. Существует свой метод для каждого вида индексации (iloc, loc, iat, at
).
В Julia есть одно общее обозначение для всех этих случаев: два индекса всегда помещаются между парой квадратных скобок. Вот и все.
Но почему?
Я думаю, очевидно, что более высокая степень согласованности в Julia делает язык более легким для изучения, чтения и использования. Поэтому возникает вопрос: если преимущества настолько очевидны, то почему бы не сделать это на Python (или каком-то другом языке программирования с меньшей степенью согласованности)?
Короткий ответ: потому что они не могут!
Более длинный ответ
Теперь к более длинному ответу: если мы заглянем за кулисы, то, например. доступ к элементу в матрице в Julia (например, m[2,4]
) преобразуется в вызов функции getindex
следующим образом:
getindex(m, 2, 4)
Обозначение со скобками - это просто синтаксический сахар (включенный перегрузкой оператора) для этого вызова функции. Это справедливо для всех примеров:
Каждый из этих вызовов функций выполняется с аргументами разных типов:
В зависимости от типов данных, используемых для аргументов, вызывается соответствующая реализация getindex
. Возможно, вы уже догадались: это знаменитая концепция множественной отправки в действии!
Таким образом, множественная диспетчеризация, в конце концов, облегчает единообразное использование одной функции (getindex
), которую можно применять в множестве похожих вариантов. Кроме того, он облачается в красивую одежду (обозначение в скобках); но это просто обертка для лучшей читабельности. Ядро основано на множественной диспетчеризации. Таким образом, языки программирования, в которых нет этой концепции, не могут предложить такую степень согласованности.
На данный момент в базовой библиотеке только Юлии 220 вариантов getindex
. Так что это действительно позволяет широко применять эту концепцию.
Присвоить значение
Примеры, в которых значение присваивается элементу матричной структуры, работают аналогичным образом. Вместо getindex
они используют setindex!
. Так что здесь тот же механизм.
За кулисами диапазонов
Теперь к примерам, где использовались диапазоны: Все приведенные выше выражения, определяющие диапазон, представляют объекты типа UnitRange
(с размером шага по умолчанию 1) или StepRange
(с использованием других размеров шага). Оба типа являются подтипами абстрактного типа AbstractRange
, как показывает следующая иерархия типов:
Так например
4:10
— это объект типаUnitRange{Int64}
(Int64
в фигурных скобках говорит нам, что нижняя и верхняя границы диапазона относятся к этому целочисленному типу),4:2:10
объект типаStepRange{Int64, Int64}
(здесь границы, а также шаги имеют типInt64
) иDateTime(2022,4,8,12):Hour(1):DateTime(2022,4,8,14)
имеет типStepRange{DateTime, Hour}
.
И тут снова в игру вступает множественная отправка:
- Доступ к диапазону элементов массива с помощью
a[4:10]
преобразуется, как мы узнали выше, вgetindex(a, 4:10)
. Итак, существует реализацияgetindex
, которая принимает подтипAbstractRange
в качестве аргумента индекса (помимо «классического» индексирования с использованием целого числа и многих других вариаций). - Вытягивание случайного числа из диапазона чисел с использованием
rand(4:10)
означает, что существует реализацияrand
(среди других версий), которая принимает подтипAbstractRange
в качестве аргумента.
Так что, в конце концов, все сводится к применению множественной диспетчеризации (которая по своей сути опирается на систему типов Джулии, которая может быть произвольно расширена пользовательскими типами).
И еще синтаксический сахар
Для тех, кому интересно: обозначение диапазонов с использованием двоеточий, таких как 4:10
или 4:2:10
, также является просто синтаксически более приятной версией обычного вызова функции (опять же на основе перегрузки оператора). В данном случае это функция range
.
Таким образом, 4:10
на самом деле равно range(4, 10)
, а 4:2:10
эквивалентно range(4, 10, step = 2)
(размер шага является необязательным аргументом ключевого слова, поэтому в этом примере необходимо ключевое слово step
).
Заключение
Используя такие примеры, как индексирование и диапазоны, мы увидели, что в Julia можно выразить очень согласованными способами, что делает язык легким для изучения, легким для чтения и, в конце концов, также прост в использовании.
Эта согласованность была достигнута с помощью нескольких мощных концепций, таких как множественная диспетчеризация, расширяемая система типов и перегрузка операторов, которые были тщательно разработаны, чтобы работать без проблем вместе.