Обзор всех деталей индексации в Julia

Введение

В мире программирования, а тем более в науке о данных, очень часто приходится работать со структурами данных, которые представляют собой наборы более мелких элементов. Как и следовало ожидать, с этими коллекциями нужно каким-то образом взаимодействовать извне коллекции — для этого программисты используют индексацию. Индексирование — жизненно важная тема программирования, и его глубина на конкретном языке — это всегда отличное знание. Много раз, когда дело доходит до знакомства и ноу-хау в языке программирования, все сводится к знанию вызовов после их использования в течение некоторого времени.

К счастью, в этой статье я рассмотрю некоторые методы, которые работают очень абстрактно. Это означает, что определение типа, которое содержит типы внутри него, очень расплывчато в отношении того, какие типы могут быть переданы через эту функцию. У Джулии самая большая ошибка — это

Ошибка метода

И мы можем столкнуться с этим и с индексами. Существует основной способ работы с индексами, который уникален для Джулии и невероятно прост, на мой субъективный взгляд на мой опыт работы с ним. По сравнению с другими экосистемами, этот метод кажется очень хорошим. Кроме того, если вы хотите просмотреть блокнот с кодом для этой статьи, вы можете получить его из этого репозитория Github здесь:



Индексация

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

array = [1, 2, 3, 4, 5]
array[1]
1

Теперь, чтобы внести ясность, мы получаем единицу не потому, что мы передали единицу через индекс в этом примере, мы получаем единицу, потому что это первое значение внутри нашего массива. Если бы мы изменили единицу на пять, например:

array = [5, 2, 3, 4, 5]
array[1]
5

Мы также можем сделать индексацию через диапазоны:

array[1:5]
5-element Vector{Int64}:
 1
 2
 3
 4
 5

Если бы эта коллекция была словарем, мы могли бы также индексировать по ключу (каким бы типом он ни был). Если вы хотите узнать больше о различных структурах данных Джулии, вы можете прочитать или посмотреть здесь:



Для полного списка типов, которые должны быть предоставлены, мы можем на самом деле поставить вопросительный знак методу. Метод, вызываемый индексацией, — это метод getindex() из базы. Вызовем на нем браузер документации:

?(getindex)

Мы также можем установить индексы. Это вызовет метод setindex!(), в котором есть точка объяснения, потому что он изменит тип, который мы в него отправляем. Давайте продолжим и импортируем getindex() и setindex!() непосредственно из Base, чтобы мы могли их расширить:

import Base: getindex, setindex!

Теперь, просто используя типичный подход к диспетчеризации Джулии, мы можем настроить любую структуру так, чтобы она имела любой тип индекса, который мы хотим.

mutable struct NotACollection
    x::Int64
end

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

getindex(notcol::NotACollection, y::Int64) = notcol.x
notacol = NotACollection(5)

Теперь если мы вызовем индекс нашего типа, который точно не является коллекцией, мы получим число и никакой возни с ним:

notacol[1]
5

Если бы вы сделали это без предварительного расширения getindex(), этот код вернул бы MethodError. Теперь просто ради интереса проделаем то же самое с setindex!():

setindex!(notcol::NotACollection, y::Int64, val::Int64) = notcol.x = y
notacol[1] = 10
println(notacol[x])

Методы индексации

Теперь давайте рассмотрим некоторые полезные методы, которые можно использовать для работы с базовыми структурами данных и изменения их индексов. Я бы посчитал это важным знанием Джулии, поэтому я очень рад поделиться с вами некоторыми из этих замечательных базовых методов. Вот некоторые из тех инструментов, которые, как я знаю, всегда пригодятся.

размер()

Первый метод — size(). Этот метод эквивалентен shape() в Python. Это даст размеры данного массива или матрицы. Хотя мы не особо углублялись в многомерные массивы, функция size() очень пригодится для работы с многомерными массивами. При этом хранение этого метода и его использования под рукой, безусловно, может применяться к одномерным и многомерным приложениям.

nums = [54, 33, 22, 13]
size(nums)
(4,)

Тип возвращаемого значения — двухэлементный кортеж, который мы можем проиндексировать:

println(typeof(size(nums)))
Tuple{Int64}
println(size(nums)[1])
4

толкать!()

Метод push!() — один из самых полезных методов языка Julia. Что хорошо в этом методе, так это то, что он определен близко к вершине иерархии типов. Это означает, что типы, которые можно передать в push!(), часто очень абстрактны. Эта абстракция означает, что может быть предоставлено множество различных типов. Тем не менее, это имеет некоторые нюансы, о которых, возможно, необходимо знать. Самое замечательное в Джулии то, что мы можем изменить язык.

Метод push!() используется для перемещения новых элементов в структуру. Он работает со словарем, массивом и различными другими типами данных — практически с любым типом данных, который вы можете себе представить. Самое замечательное в push!() то, что вы никогда не знаете, сработает ли он, пока не попробуете, а если он не сработает, вы всегда можете написать свой собственный push!(), чтобы позаботиться об этом. Давайте попробуем это на нашем массиве:

push!(nums, 5)
5-element Vector{Int64}:
 54
 33
 22
 13
  5

На самом деле я написал целую статью, посвященную методу push!(), поэтому, если вам интересно прочитать об этом методе и обо всех его нюансах, вы можете прочитать об этом здесь, если хотите:

добавить!()

Метод append!() очень похож на метод push!(). Как и метод push!(), метод append!() добавляет элемент в заданный массив. Однако ключевое отличие заключается в том, что append!() максимально избегает изменения ордеров, а push!() — нет. Метод append!() также гораздо более определен и специфичен для типов, с которыми он должен использоваться. Как правило, он используется для массивов с простыми типами данных. Давайте посмотрим на пример с нашим массивом:

append!(nums, 5)
6-element Vector{Int64}:
 54
 33
 22
 13
  5
  5

Обратите внимание: поскольку наш массив имеет тип Vector{Int64}, через эту функцию мы можем передавать только целые числа. Мы также могли бы передавать числа, которые можно преобразовать в целые числа, однако в большинстве случаев нам нужно будет преобразовать типы перед их предоставлением.

append!(nums, 5.5)
InexactError: Int64(5.5)

Приведение этого типа приводит к тому, что он работает:

append!(nums, 5.5)

Однако, если эта структура будет содержать несколько типов, лучшим подходом может быть просто приведение всей структуры к содержанию {Any}:

nums = Array{Any}(nums)
append!(nums, 5.5)

фильтр!()

Метод filter в Джулии — еще один, который очень часто оказывается полезным. Это может особенно пригодиться при использовании статистики. Подумайте о масках Pandas DataFrame, которые фактически попали в эту статью о моих любимых функциях Pandas:



Однако filter!() является эквивалентом в Джулии. Тоже вау, больше года назад.

Ко мне и filter!() есть небольшие претензии, и дело не в том, что мне не нравится этот метод — я просто чувствую, что его использование гораздо более раздражает, чем многие другие интерфейсы. Джулия имеет тенденцию преуспевать во многих вещах, которые она делает, и делает вещи очень простыми для программиста, но это тот случай, когда я подумал, что разобраться с этим было сложнее, чем с другим языком, которым является Python. К счастью, синтаксис для изменения, которое я хотел внести, было довольно легко выполнить, и логическое индексирование теперь доступно в пакете, который я сейчас разрабатываю, OddFrames.jl, который вы также можете просмотреть на странице Github здесь:



filter((x) -> x == 5, nums)

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



Мы также коснемся их подробнее чуть позже в этой статье, а пока просто будем наблюдать за нашими отфильтрованными значениями:

filter((x) -> x == 5, nums)
2-element Vector{Any}:
 5
 5

собирать()

Такие методы, как «сбор», часто трудно объяснить, но «сбор» можно использовать для быстрого извлечения значений из какого-либо генератора или процесса. По сути, все, что способно вернуть итерируемый объект. Если бы у нас был диапазон

r = 5:10

И мы хотели получить массив от пяти до десяти, мы бы просто вызвали для него сбор для этого результата.

collect(r)
6-element Vector{Int64}:
  5
  6
  7
  8
  9
 10

найти все()

Метод findall!() — это метод, о котором я могу с уверенностью сказать, что он невероятно полезен. Для этого существует множество призывов, поэтому лучше всего использовать «старый вопросительный знак»:

?(findall)

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

function getindex(od::AbstractOddFrame, col::Symbol)
        pos = findall(x->x==col, od.labels)[1]
        return(od.columns[pos])
end
getindex(od::AbstractOddFrame, col::String) = od[Symbol(col)]
getindex(od::AbstractOddFrame, axis::Int64) = od.columns[axis]
function getindex(od::AbstractOddFrame, mask::BitArray)
        pos = findall(x->x==0, mask)
        od.drop(pos)
end
getindex(z::UnitRange) = [od.labels[i] for i in z]

Этот код представляет собой несколько привязок отправки getindex(). Возможно, вы помните getindex() ранее. OddFrames.jl работает с одномерными массивами внутри массивов, которые могут быть наложены друг на друга, но этого не происходит. Метод findall() используется здесь дважды. Первый случай — это когда индекс вызывается с символом:

od[:column]

В этом примере метод findall() возвращает нам индекс любого экземпляра, равного такому объекту. По какой-то причине во многих кодах Julia, которые я видел, написанных таким образом, не используются пробелы вокруг операторов, и поэтому я принял его — хотя я не думаю, что это правильно, поэтому позвольте мне сделать это немного легче для чтения:

pos = findall(x -> x == col, od.labels)[1]

Все, что мы делаем, это пытаемся найти метку, равную этому значению, мы сначала используем:

x -> x

Это говорит Джулии, что мы хотим создать анонимную функцию. Теперь этот x является переменной, и мы определили функцию, которая будет возвращать BitArray. BitArray — это просто тип массива, который содержит логические значения true/false, обычно в виде единиц и нулей. Единицы и нули занимают только один бит, поэтому это называется BitArray. Возможно, я мог бы объяснить это намного проще, просто сказав, что это двоичный массив. В любом случае, наш массив возвращается прямо из этого:

x == col

И с этим поворотом мы перебираем od.labels и смотрим, совпадает ли значение. Поскольку метки не могут быть одинаковыми в OddFrame, мы всегда знаем, что это будет либо одно значение, либо не будет никакого значения, если оно не найдено в OddFrame. Учитывая эту информацию, я вызываю первый индекс в конце, это просто изменит тип нашего значения с Array{Int64, 1} на Int64. Затем мы просто вызываем этот индекс для столбца в нашем возврате:

function getindex(od::AbstractOddFrame, col::Symbol)
        pos = findall(x->x==col, od.labels)[1] 
       return(od.columns[pos])
end

Забегая немного вперед, мы видим, что то же самое используется для условной маски:

function getindex(od::AbstractOddFrame, mask::BitArray)
        pos = findall(x->x==0, mask)
        od.drop(pos)
end

Единственное отличие состоит в том, что на этот раз findall() используется для сбора позиций, которые нужно отбросить, а затем в методе od.drop() используется следующий метод в этом списке, чтобы избавиться от этого значения.

Для нашего примера ноутбука мы получим каждое значение, равное 5:

z = findall(x -> x == 5, nums)
array = [nums[x] for x in z]

удалить!()

Последний метод, на который я хотел обратить внимание, это метод deleteat!(). Как упоминалось ранее, мы рассмотрим пример этого в OddFrames.jl. Мы также сделаем еще один пример в тетради. Пример OddFrames находится в файле member_func.jl в репозитории OddFrames, еще раз нажмите на этот текст. Именно для этого и используется метод deleteat!() — удаление по определенному индексу.

Я знаю, что кто-то где-то продолжает читать, что как удалить есть, просто знайте, что вы не одиноки. Может быть, мы просто голодны.

Метод прост в использовании, просто укажите коллекцию и индекс:

function _drop(row::Int64, columns::Array)
        [deleteat!(col, row) for col in columns]
end

В этом случае это делается с помощью итеративного цикла и изменяет предоставленный вектор. Хотя от этого есть возврат, он не предназначен для использования — возможно, мне следует вернуться позже, чтобы убедиться, что это поняли. Для нашего примера ноутбука мы удалим весь наш массив:

while length(nums) > 1
    deleteat!(nums, 1)
end
println(nums)
Any[5.5]

Покойся с миром.

Так много методов

Конечно, существует так много методов для работы с такого рода типами, что я никак не могу описать их все, но некоторые из них я нашел невероятно ценными, помимо обычных вещей, таких как length() и sum( ) и т. д. Все они специфичны для Julia, кроме size(), поэтому я думаю, что новый пользователь Julia может найти их полезными для ссылки. Есть даже горизонтальные и вертикальные конкатенации, различные матричные операции и многое другое.

Заключение

Язык Julia имеет довольно приятный способ обработки итераций. У каждого языка есть свой подход, который я могу оценить, но мне это действительно нравится по сравнению с использованием методов класса Python по умолчанию или выполнением тех же задач в R. Мне часто не хватает этих функций всякий раз, когда я использую другие языки, что говорит мне о том, что они скорее всего отличные характеристики.

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