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 можно выразить очень согласованными способами, что делает язык легким для изучения, легким для чтения и, в конце концов, также прост в использовании.

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