Одно- и многомерные массивы
В Julia, как и в большинстве языков для технических вычислений, отличная реализация массивов. Зачастую в языках для технических вычислений на массивы делается больший упор, чем на другие контейнеры. В Julia массивы не являются чем-то особенным. Библиотека для работы с массивами почти полностью реализована на Julia, поэтому ее производительность зависит от компилятора, как и производительность другого кода, написанного на Julia. По этой причине можно также определять пользовательские типы массивов, наследуя их от AbstractArray
. Дополнительные сведения о реализации пользовательского типа массива см. в разделе руководства, посвященном интерфейсу AbstractArray.
Массив — это коллекция объектов, хранящихся в многомерной таблице. Допустимы нульмерные массивы; см. этот раздел FAQ. В самом общем случае массив может содержать объекты типа Any
. Для большинства вычислительных целей массивы должны содержать объекты более конкретного типа, такого как Float64
или Int32
.
В отличие от многих других языков для технических вычислений, в Julia, как правило, программы необязательно писать в векторизованном стиле для обеспечения производительности. Компилятор Julia использует вывод типов и генерирует оптимизированный код для скалярной индексации массивов, что позволяет писать программы в более удобном и удобочитаемом стиле, не жертвуя производительностью, а иногда и экономя память.
В Julia все аргументы функций передаются по соиспользованию (то есть по указателям). В некоторых языках для технических вычислений массивы передаются по значению. Благодаря этому вызываемый объект не может случайно изменить значение в вызывающем, однако из-за этого сложно избежать ненужного копирования массивов. Согласно соглашению, если имя функции заканчивается символом !
, значит она будет изменять или удалять значение одного или нескольких своих аргументов (сравните, например, функции sort
и sort!
). Вызываемые функции должны явным образом создавать копии входных данных, чтобы те не изменялись. Многие функции, не предполагающие изменения аргументов, реализуются путем вызова функции с тем же именем, но с символом !
в конце, которая возвращает явную копию созданных входных данных.
Основные функции
Функция | Описание |
---|---|
Тип элементов в |
|
Количество элементов в |
|
Количество измерений массива |
|
Кортеж, содержащий измерения массива |
|
Размер массива |
|
Кортеж, содержащий допустимые индексы массива |
|
Диапазон допустимых индексов по измерению |
|
Эффективный итератор для перебора каждой позиции в |
|
Шаг массива (расстояние между линейными индексами соседних элементов) по измерению |
|
Кортеж шагов массива в каждом измерении |
Создание и инициализация
Доступно множество функций для создания и инициализации массивов. В приведенном ниже списке таких функций вызовы с аргументом dims...
могут принимать либо один кортеж с размерами измерений, либо набор размеров измерений, передаваемый в виде переменного числа аргументов. Большинство таких функций также принимают первый аргумент T
, определяющий тип элементов массива. Если аргумент T
не указан, по умолчанию используется тип Float64
.
Функция | Описание |
---|---|
Неинициализированный плотный массив |
|
Массив |
|
Массив |
|
Массив |
|
Массив |
|
Массив с теми же данными, что и в |
|
Копирует массив |
|
Копирует массив |
|
Неинициализированный массив того же типа, что и |
|
Массив с теми же двоичными данными, что и в |
|
массив |
|
Массив |
|
Матрица тожественности |
|
Диапазон из |
|
Заполняет массив |
|
Массив |
Вот несколько примеров, иллюстрирующих различные способы передачи измерений в эти функции.
julia> zeros(Int8, 2, 3)
2×3 Matrix{Int8}:
0 0 0
0 0 0
julia> zeros(Int8, (2, 3))
2×3 Matrix{Int8}:
0 0 0
0 0 0
julia> zeros((2, 3))
2×3 Matrix{Float64}:
0.0 0.0 0.0
0.0 0.0 0.0
Здесь (2, 3)
— это Tuple
, а первый аргумент — тип элементов — является необязательным (по умолчанию Float64
).
Литералы массивов
Массивы можно также создавать напрямую с помощью квадратных скобок. Синтаксис [A, B, C, ...]
создает одномерный массив (то есть вектор), элементами которого являются аргументы, перечисленные через запятую. Тип элементов (eltype
) получившегося массива определяется автоматически на основе типов аргументов внутри скобок. Если все аргументы одного типа, это и будет тип eltype
. Если у всех них есть общий тип продвижения, они преобразуются в этот тип с помощью функции convert
, и этот тип является типом элемента (eltype
) массива. В противном случае создается неоднородный массив, который может содержать что угодно — Vector{Any}
. Это может быть и литерал []
без аргументов. Литерал массива может быть типизирован с помощью синтаксиса T[A, B, C, ...]
, где T
является типом.
julia> [1,2,3] # Массив значений типа `Int`
3-element Vector{Int64}:
1
2
3
julia> promote(1, 2.3, 4//5) # Это сочетание значений типа Int, Float64 и Rational продвигается до Float64
(1.0, 2.3, 0.8)
julia> [1, 2.3, 4//5] # Это и будет тип элементов данного массива
3-element Vector{Float64}:
1.0
2.3
0.8
julia> Float32[1, 2.3, 4//5] # Указание типа элемента вручную
3-element Vector{Float32}:
1.0
2.3
0.8
julia> []
Any[]
Конкатенация
Если аргументы в квадратных скобках разделяются одинарными точками с запятой (;
) или символами разрыва строки, а не запятыми, то их содержимое объединяется по вертикали (сами аргументы не используются как элементы).
julia> [1:2, 4:5] # Имеет запятую, поэтому конкатенация не производится. Элементами являются сами диапазоны
2-element Vector{UnitRange{Int64}}:
1:2
4:5
julia> [1:2; 4:5]
4-element Vector{Int64}:
1
2
4
5
julia> [1:2
4:5
6]
5-element Vector{Int64}:
1
2
4
5
6
Если же аргументы разделяются символами табуляции, пробелами или парами двоеточий, то их содержимое объединяется по горизонтали.
julia> [1:2 4:5 7:8]
2×3 Matrix{Int64}:
1 4 7
2 5 8
julia> [[1,2] [4,5] [7,8]]
2×3 Matrix{Int64}:
1 4 7
2 5 8
julia> [1 2 3] # Числа также могут объединяться по горизонтали
1×3 Matrix{Int64}:
1 2 3
julia> [1;; 2;; 3;; 4]
1×4 Matrix{Int64}:
1 2 3 4
Одинарные точки с запятой (или символы разрыва строки) можно сочетать с пробелами (или символами табуляции) для конкатенации одновременно по горизонтали и вертикали.
julia> [1 2
3 4]
2×2 Matrix{Int64}:
1 2
3 4
julia> [zeros(Int, 2, 2) [1; 2]
[3 4] 5]
3×3 Matrix{Int64}:
0 0 1
0 0 2
3 4 5
julia> [[1 1]; 2 3; [4 4]]
3×2 Matrix{Int64}:
1 1
2 3
4 4
Пробелы (и символы табуляции) имеют приоритет над точками с запятой, поэтому сначала выполняется конкатенация по горизонтали, а затем по вертикали. Однако если для конкатенации по горизонтали используются пары точек с запятой, сначала выполняется конкатенации по вертикали, а затем результат объединяется по горизонтали.
julia> [zeros(Int, 2, 2) ; [3 4] ;; [1; 2] ; 5]
3×3 Matrix{Int64}:
0 0 1
0 0 2
3 4 5
julia> [1:2; 4;; 1; 3:4]
3×2 Matrix{Int64}:
1 1
2 3
4 4
Подобно тому, как ;
и ;;
служат для конкатенации в первом и втором измерениях, можно использовать и больше точек с запятой: общий принцип действия будет аналогичным. Количество точек с запятой в разделителе определяет измерение: ;;;
служит для конкатенации в третьем измерении, ;;;;
— в четвертом и так далее. Чем меньше точек с запятой, тем выше приоритет, поэтому более низкие измерения обычно объединяются в первую очередь.
julia> [1; 2;; 3; 4;; 5; 6;;;
7; 8;; 9; 10;; 11; 12]
2×3×2 Array{Int64, 3}:
[:, :, 1] =
1 3 5
2 4 6
[:, :, 2] =
7 9 11
8 10 12
Как указывалось ранее, пробелы (и символы табуляции), используемые для конкатенации по горизонтали, имеют приоритет над любым количеством точек с запятой. Поэтому при определении массивов высокой размерности можно сначала указать строки, а затем элементы, так что текстовое представление будет соответствовать структуре массива:
julia> [1 3 5
2 4 6;;;
7 9 11
8 10 12]
2×3×2 Array{Int64, 3}:
[:, :, 1] =
1 3 5
2 4 6
[:, :, 2] =
7 9 11
8 10 12
julia> [1 2;;; 3 4;;;; 5 6;;; 7 8]
1×2×2×2 Array{Int64, 4}:
[:, :, 1, 1] =
1 2
[:, :, 2, 1] =
3 4
[:, :, 1, 2] =
5 6
[:, :, 2, 2] =
7 8
julia> [[1 2;;; 3 4];;;; [5 6];;; [7 8]]
1×2×2×2 Array{Int64, 4}:
[:, :, 1, 1] =
1 2
[:, :, 2, 1] =
3 4
[:, :, 1, 2] =
5 6
[:, :, 2, 2] =
7 8
Хотя и пробелы (или символы табуляции), и символы ;;
обозначают конкатенацию во втором измерении, их нельзя использовать вместе в одном выражении массива, если только двойные точки с запятой не служат в качестве символа продолжения строки. В этом случае они позволяют осуществлять конкатенацию по нескольким строкам (символ перевода строки не интерпретируется как конкатенация по вертикали).
julia> [1 2 ;;
3 4]
1×4 Matrix{Int64}:
1 2 3 4
С помощью точек с запятой в конце можно добавить еще одно измерение длиной 1.
julia> [1;;]
1×1 Matrix{Int64}:
1
julia> [2; 3;;;]
2×1×1 Array{Int64, 3}:
[:, :, 1] =
2
3
Более универсальным средством для конкатенации является функция cat
. Ниже приведен ряд вспомогательных функций и соответствующих им кратких форм записи.
Синтаксис | Функция | Описание |
---|---|---|
Конкатенация входных массивов по измерениям |
||
|
Краткая форма записи для вызова `cat(A…; dims=1) |
|
|
shorthand for `cat(A…; dims=2) |
|
|
Конкатенация одновременно по вертикали и по горизонтали |
|
|
Конкатенация одновременно по n измерениям, где количество точек с запятой указывает на измерение для конкатенации |
Типизированные литералы массивов
Массив с элементами определенного типа можно создать, используя синтаксис T[A, B, C, ...]
. В результате будет создан одномерный массив с элементами типа T
: A
, B
, C
и т. д. Например, выражение Any[x, y, z]
создает неоднородный массив, который может содержать любые значения.
Перед выражением конкатенации также можно указать тип, который будут иметь элементы.
julia> [[1 2] [3 4]]
1×4 Matrix{Int64}:
1 2 3 4
julia> Int8[[1 2] [3 4]]
1×4 Matrix{Int8}:
1 2 3 4
Включения
Включения — это универсальный и эффективный способ создания массивов. Синтаксис включения похож на то, как записывается создание множества в математике:
A = [ F(x,y,...) for x=rx, y=ry, ... ]
Смысл этой формы записи в том, что выражение F(x,y,...)
вычисляется для переменных x
, y
и т. д., которые последовательно принимают каждое из значений в указанном списке. Значения можно указывать в виде любого итерируемого объекта, но обычно это диапазоны, например 1:n
или 2:(n-1)
, либо явно заданные массивы значений, например [1.2, 3.4, 5.7]
. В итоге получается N-мерный плотный массив с измерениями, которые являются результатом конкатенации измерений диапазонов переменных rx
, ry
и т. д., причем при каждом вычислении F(x,y,...)
возвращается скалярное значение.
В следующем примере вычисляется средневзвешенное значение текущего элемента и соседних с ним элементов слева и справа по одномерной таблице. :
julia> x = rand(8)
8-element Array{Float64,1}:
0.843025
0.869052
0.365105
0.699456
0.977653
0.994953
0.41084
0.809411
julia> [ 0.25*x[i-1] + 0.5*x[i] + 0.25*x[i+1] for i=2:length(x)-1 ]
6-element Array{Float64,1}:
0.736559
0.57468
0.685417
0.912429
0.8446
0.656511
Как и в случае с литералами массивов, тип получившегося массива зависит от типов вычисленных элементов. Тип также можно указать явным образом перед включением. Например, чтобы результат имел одинарную точность, можно записать такое выражение:
Float32[ 0.25*x[i-1] + 0.5*x[i] + 0.25*x[i+1] for i=2:length(x)-1 ]
Генераторные выражения
Включение можно также записать, не заключая его в скобки. В результате получается объект, называемый генератором. Путем итерации по такому объекту можно получать значения по запросу, не сохраняя массив в памяти заранее (см. раздел Итерация). Например, следующее выражение суммирует последовательность без выделения памяти:
julia> sum(1/n^2 for n=1:1000)
1.6439345666815615
При написании генераторного выражения с несколькими измерениями в списке аргументов генератор должен отделяться от последующих аргументов круглыми скобками:
julia> map(tuple, 1/(i+j) for i=1:2, j=1:2, [1:4;])
ERROR: syntax: invalid iteration specification
ОШИБКА: синтаксис: неверное задание итерации
Все разделенные запятыми выражения после for
считаются диапазонами. Добавив скобки, можно указать третий аргумент для функции map
:
julia> map(tuple, (1/(i+j) for i=1:2, j=1:2), [1 3; 2 4])
2×2 Matrix{Tuple{Float64, Int64}}:
(0.5, 1) (0.333333, 3)
(0.333333, 2) (0.25, 4)
Генераторы реализуются посредством внутренних функций. Как и в других случаях использования внутренних функций в языке, в таких функциях могут захватываться переменные из внешней области. Например, sum(p[i] - q[i] for i=1:n)
захватывает из внешней области три переменные: p
, q
и n
. Захваченные переменные могут влиять на производительность; см. советы по производительности.
Диапазоны в генераторах и включениях могут зависеть от предыдущих диапазонов; для этого используется несколько ключевых слов for
:
julia> [(i,j) for i=1:3 for j=1:i]
6-element Vector{Tuple{Int64, Int64}}:
(1, 1)
(2, 1)
(2, 2)
(3, 1)
(3, 2)
(3, 3)
В таких случаях результат всегда одномерный.
Генерируемые значения можно фильтровать с помощью ключевого слова if
:
julia> [(i,j) for i=1:3 for j=1:i if i+j == 4]
2-element Vector{Tuple{Int64, Int64}}:
(2, 2)
(3, 1)
Индексирование
Синтаксис индексирования n-мерного массива A
имеет следующий общий вид.
X = A[I_1, I_2, ..., I_n]
Здесь I_k
может быть скалярным целым числом, массивом целых чисел или любым другим поддерживаемым индексом. Сюда входят Colon
(:
) для выбора всех индексов во всем измерении, диапазоны вида a:c
или a:b:c
для выбора непрерывного отрезка или отрезка с заданным шагом и массивы логических значений для выбора элементов по индексам true
.
Если все индексы скалярные, то X
— это отдельный элемент из массива A
. В противном случае X
— это массив, количество измерений которого равно сумме размерностей всех индексов.
Например, если все индексы I_k
— векторы, то X
будет иметь форму (length(I_1), length(I_2), ..., length(I_n))
, а в позиции i_1, i_2, ..., i_n
переменной X
будет содержаться значение A[I_1[i_1], I_2[i_2], ..., I_n[i_n]]
.
Пример:
julia> A = reshape(collect(1:16), (2, 2, 2, 2))
2×2×2×2 Array{Int64, 4}:
[:, :, 1, 1] =
1 3
2 4
[:, :, 2, 1] =
5 7
6 8
[:, :, 1, 2] =
9 11
10 12
[:, :, 2, 2] =
13 15
14 16
julia> A[1, 2, 1, 1] # только скалярные индексы
3
julia> A[[1, 2], [1], [1, 2], [1]] # все индексы являются векторами
2×1×2×1 Array{Int64, 4}:
[:, :, 1, 1] =
1
2
[:, :, 2, 1] =
5
6
julia> A[[1, 2], [1], [1, 2], 1] # индексы разных типов
2×1×2 Array{Int64, 3}:
[:, :, 1] =
1
2
[:, :, 2] =
5
6
Обратите внимание: в последних двух случаях размеры получившихся массивов разные.
Если I_1
изменить на двумерную матрицу, X
станет n+1
-мерным массивом вида (size(I_1, 1), size(I_1, 2), length(I_2), ..., length(I_n))
. Введение матрицы добавляет одно измерение.
Пример:
julia> A = reshape(collect(1:16), (2, 2, 2, 2));
julia> A[[1 2; 1 2]]
2×2 Matrix{Int64}:
1 2
1 2
julia> A[[1 2; 1 2], 1, 2, 1]
2×2 Matrix{Int64}:
5 6
5 6
В позиции i_1, i_2, i_3, ..., i_{n+1}
содержится значение по индексу A[I_1[i_1, i_2], I_2[i_3], ..., I_n[i_{n+1}]]
. Все измерения со скалярными индексами отбрасываются. Например, если J
— это массив индексов, то результатом A[2, J, 3]
будет массив размера size(J)
. Его j
-й элемент заполняется значением A[2, J[j], 3]
.
Особым элементом синтаксиса является ключевое слово end
, представляющее последний индекс каждого измерения в скобках, определяемый размером самого глубоко вложенного индексируемого массива. Синтаксис индексирования без ключевого слова end
эквивалентен вызову getindex
:
X = getindex(A, I_1, I_2, ..., I_n)
Пример:
julia> x = reshape(1:16, 4, 4)
4×4 reshape(::UnitRange{Int64}, 4, 4) with eltype Int64:
1 5 9 13
2 6 10 14
3 7 11 15
4 8 12 16
julia> x[2:3, 2:end-1]
2×2 Matrix{Int64}:
6 10
7 11
julia> x[1, [2 3; 4 1]]
2×2 Matrix{Int64}:
5 9
13 1
Присваивание по индексу
Синтаксис присваивания значений элементам n-мерного массива A
имеет следующий общий вид.
A[I_1, I_2, ..., I_n] = X
Здесь I_k
может быть скалярным целым числом, массивом целых чисел или любым другим поддерживаемым индексом. Сюда входят Colon
(:
) для выбора всех индексов во всем измерении, диапазоны вида a:c
или a:b:c
для выбора непрерывного отрезка или отрезка с заданным шагом и массивы логических значений для выбора элементов по индексам true
.
Если все индексы I_k
являются целочисленными, значение в позиции I_1, I_2, ..., I_n
массива A
перезаписывается значением X
, тип которого при необходимости преобразуется с помощью функции convert
в eltype
массива A
.
Если хотя бы один индекс I_k
сам является массивом, выражение X
в правой части также должно быть массивом той же формы, что и результат индексирования A[I_1, I_2, ..., I_n]
, или вектором с тем же количеством элементов. Значение в позиции I_1[i_1], I_2[i_2], ..., I_n[i_n]
массива A
перезаписывается значением X[I_1, I_2, ..., I_n]
, тип которого при необходимости преобразуется. Оператор поэлементного присваивания .=
может использоваться для трансляции X
в выбранные позиции:
A[I_1, I_2, ..., I_n] .= X
Как и в случае с индексированием, ключевое слово end
может представлять последний индекс каждого измерения в скобках, определяемый размером массива, элементам которого присваиваются значения. Синтаксис присваивания по индексу без ключевого слова end
эквивалентен вызову функции setindex!
:
setindex!(A, X, I_1, I_2, ..., I_n)
Пример:
julia> x = collect(reshape(1:9, 3, 3))
3×3 Matrix{Int64}:
1 4 7
2 5 8
3 6 9
julia> x[3, 3] = -9;
julia> x[1:2, 1:2] = [-1 -4; -2 -5];
julia> x
3×3 Matrix{Int64}:
-1 -4 7
-2 -5 8
3 6 -9
Поддерживаемые типы индексов
В выражении A[I_1, I_2, ..., I_n]
каждый элемент I_k
может быть скалярным индексом, массивом скалярных индексов или объектом, который представляет массив скалярных индексов и может быть преобразован в него функцией to_indices
:
-
К скалярным индексам по умолчанию относятся: * целочисленные значения (не логические); * объекты
CartesianIndex{N}
, которые действуют как кортежи изN
целочисленных значений, охватывающие несколько измерений (подробности см. ниже). -
К массивам скалярных индексов относятся:
-
векторы и многомерные массивы целых чисел;
-
пустые массивы, например
[]
, для выбора нуля элементов, напримерA[[]]
(не путать сA[]
); -
диапазоны, например
a:c
илиa:b:c
, для выбора непрерывных отрезков или отрезков с заданным шагом отa
доc
(включительно); -
любой пользовательский массив скалярных индексов, являющийся подтипом
AbstractArray
; -
массивы
CartesianIndex{N}
(подробности см. ниже).
-
-
К объектам, которые представляют массив скалярных индексов и могут быть преобразованы в него функцией
to_indices
, по умолчанию относятся: *Colon()
(:
) — представляет все индексы в измерении или всем массиве; * массивы логических значений для выбора элементов по индексамtrue
(подробности см. ниже).
Некоторые примеры:
julia> A = reshape(collect(1:2:18), (3, 3))
3×3 Matrix{Int64}:
1 7 13
3 9 15
5 11 17
julia> A[4]
7
julia> A[[2, 5, 8]]
3-element Vector{Int64}:
3
9
15
julia> A[[1 4; 3 8]]
2×2 Matrix{Int64}:
1 7
5 15
julia> A[[]]
Int64[]
julia> A[1:2:5]
3-element Vector{Int64}:
1
5
9
julia> A[2, :]
3-element Vector{Int64}:
3
9
15
julia> A[:, 3]
3-element Vector{Int64}:
13
15
17
julia> A[:, 3:3]
3×1 Matrix{Int64}:
13
15
17
Декартовы индексы
Специальный объект CartesianIndex{N}
представляет скалярный индекс, который действует как кортеж из N
целочисленных значений, охватывающий несколько измерений. Пример:
julia> A = reshape(1:32, 4, 4, 2);
julia> A[3, 2, 1]
7
julia> A[CartesianIndex(3, 2, 1)] == A[3, 2, 1] == 7
true
Само по себе это может показаться довольно тривиальным: CartesianIndex
просто объединяет несколько целочисленных значений в одном объекте, представляющем многомерный индекс. Однако в сочетании с другими формами индексирования и итераторами, выдающими объекты CartesianIndex
, может получаться очень элегантный и эффективный код. См. раздел Итерация ниже. Ряд более сложных примеров можно найти в этой записи блога о многомерных алгоритмах и итерации.
Поддерживаются также массивы объектов CartesianIndex{N}
. Такой массив представляет коллекцию скалярных индексов, каждый из которых охватывает N
измерений. Благодаря этому становится возможной форма индексирования, иногда называемая точечной. Например, она обеспечивает доступ сверху к расположенным по диагонали элементам с первой «страницы» массива A
:
julia> page = A[:,:,1]
4×4 Matrix{Int64}:
1 5 9 13
2 6 10 14
3 7 11 15
4 8 12 16
julia> page[[CartesianIndex(1,1),
CartesianIndex(2,2),
CartesianIndex(3,3),
CartesianIndex(4,4)]]
4-element Vector{Int64}:
1
6
11
16
Это можно гораздо проще выразить с помощью точечной трансляции в сочетании с обычным целочисленным индексом (вместо извлечения первой страницы (page
) из массива A
отдельным действием). Такой подход можно также использовать в сочетании с :
для одновременного извлечения диагоналей с двух страниц:
julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), 1]
4-element Vector{Int64}:
1
6
11
16
julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), :]
4×2 Matrix{Int64}:
1 17
6 22
11 27
16 32
|
Логическое индексирование
Индексирование на основе массива логических значений, часто называемое логическим индексированием или индексированием по логической маске, позволяет выбирать элементы по индексам со значением true
. Индексирование по вектору логических значений B
— это по сути то же самое, что и индексирование по вектору целочисленных значений, возвращаемому методом findall(B)
. Аналогичным образом, индексирование по N
-мерному массиву логических значений — это по сути то же самое, что и индексирование по вектору объектов CartesianIndex{N}
со значениями true
. Логический индекс должен представлять собой вектор той же длины, что и индексируемое измерение, либо он должен быть единственным индексом и соответствовать размеру и размерности индексируемого массива. Как правило, эффективнее использовать массивы логических значений в качестве индексов напрямую, чем сначала вызывать метод findall
.
julia> x = reshape(1:16, 4, 4)
4×4 reshape(::UnitRange{Int64}, 4, 4) with eltype Int64:
1 5 9 13
2 6 10 14
3 7 11 15
4 8 12 16
julia> x[[false, true, true, false], :]
2×4 Matrix{Int64}:
2 6 10 14
3 7 11 15
julia> mask = map(ispow2, x)
4×4 Matrix{Bool}:
1 0 0 0
1 0 0 0
0 0 0 0
1 1 0 1
julia> x[mask]
5-element Vector{Int64}:
1
2
4
8
16
Количество индексов
Декартово индексирование
Для индексирования N
-мерного массива обычно используется ровно N
индексов: каждый индекс служит для выбора позиций в своем измерении. Например, в трехмерном массиве A = rand(4, 3, 2)
индекс A[2, 3, 1]
выбирает число во второй строке третьем столбце на первой «странице» массива. Такой подход часто называют декартовым индексированием.
Линейное индексирование
Если указан только один индекс i
, он не представляет позицию в определенном измерении массива. Вместо этого он служит для выбора i
-го элемента во всем массиве, развернутом линейно по столбцам. Такой подход называется линейным индексированием. Массив при этом представляется так, как если бы он был преобразован в одномерный вектор с помощью функции vec
.
julia> A = [2 6; 4 7; 3 1]
3×2 Matrix{Int64}:
2 6
4 7
3 1
julia> A[5]
7
julia> vec(A)[5]
7
Линейный индекс в массиве A
может быть преобразован в CartesianIndex
для декартова индексирования с помощью CartesianIndices(A)[i]
(см. описание типа CartesianIndices
), а набор из N
декартовых индексов может быть преобразован в линейный индекс с помощью LinearIndices(A)[i_1, i_2, ..., i_N]
(см. описание типа LinearIndices
).
julia> CartesianIndices(A)[5]
CartesianIndex(2, 2)
julia> LinearIndices(A)[2, 2]
5
Важно отметить, что эти преобразования существенно различаются в плане производительности. Для преобразования линейного индекса в набор декартовых индексов требуется деление и получение остатка, а для обратного действия — только умножение и сложение. В современных процессорах целочисленное деление может выполняться в 10—50 раз медленнее умножения. В то время как некоторые массивы, например базовый тип Array
, занимают линейную область памяти и напрямую используют линейный индекс в своей реализации, другие, например Diagonal
, требуют полного набора декартовых индексов для поиска элементов (для определения стиля индексирования можно использовать IndexStyle
).
!!! warnings
При итерации по всем индексам массива лучше выполнять итерацию по eachindex(A)
, а не по 1:length(A)
. Это будет не только быстрее в случаях, когда A
— это IndexCartesian
, но при этом также будут поддерживаться массивы с пользовательским индексированием, такие как OffsetArrays. Если нужны только значения, то лучше просто итерировать массив напрямую, т. е. for a in A
.
Опускаемые и дополнительные индексы
Помимо линейного индексирования, в некоторых ситуациях N
-мерный массив может индексироваться с использованием более чем или менее чем N
индексов.
Индексы могут опускаться, если последние неиндексируемые измерения имеют единичную длину. Иными словами, последние индексы можно опустить, только если они имеют единственное возможное значение в границах допустимого интервала в выражении индексирования. Например, четырехмерный массив размера (3, 4, 2, 1)
можно индексировать с использованием трех индексов, так как опускаемое измерение (четвертое) имеет единичную длину. Имейте в виду, что линейное индексирование имеет приоритет над этим правилом.
julia> A = reshape(1:24, 3, 4, 2, 1)
3×4×2×1 reshape(::UnitRange{Int64}, 3, 4, 2, 1) with eltype Int64:
[:, :, 1, 1] =
1 4 7 10
2 5 8 11
3 6 9 12
[:, :, 2, 1] =
13 16 19 22
14 17 20 23
15 18 21 24
julia> A[1, 3, 2] # Опускает четвертое измерение (длиной 1)
19
julia> A[1, 3] # Пытается опустить измерения 3 и 4 (длиной 2 и 1)
ERROR: BoundsError: attempt to access 3×4×2×1 reshape(::UnitRange{Int64}, 3, 4, 2, 1) with eltype Int64 at index [1, 3]
julia> A[19] # Линейное индексирование
19
Можно опустить все индексы с помощью выражения A[]
— это простой способ получить единственный элемент в массиве и одновременно проверить, был ли в нем только один элемент.
Аналогичным образом, можно указать более чем N
индексов, если все индексы сверх размерности массива равны 1
(или, выражаясь более формально, являются первым и единственным элементом axes(A, d)
, где d
— номер соответствующего измерения). Например, это позволяет индексировать векторы как одностолбцовые матрицы:
julia> A = [8,6,7]
3-element Vector{Int64}:
8
6
7
julia> A[2,1]
6
Итерация
Для итерации по всему массиву рекомендуются следующие способы:
for a in A
# Какие-либо действия с элементом a
end
for i in eachindex(A)
# Какие-либо действия с элементом i и (или) A[i]
end
Первая конструкция применяется, когда требуется значение каждого элемента, но не его индекс. Во второй конструкции i
будет иметь тип Int
, если A
— массив с быстрым линейным индексированием, или тип CartesianIndex
в противном случае:
julia> A = rand(4,3);
julia> B = view(A, 1:3, 2:3);
julia> for i in eachindex(B)
@show i
end
i = CartesianIndex(1, 1)
i = CartesianIndex(2, 1)
i = CartesianIndex(3, 1)
i = CartesianIndex(1, 2)
i = CartesianIndex(2, 2)
i = CartesianIndex(3, 2)
В отличие от |
Стили массивов
При создании пользовательского типа AbstractArray
можно указать, что используется быстрое линейное индексирование, следующим образом:
Base.IndexStyle(::Type{<:MyArray}) = IndexLinear()
В результате итерация с помощью eachindex
по массиву MyArray
будет производиться с использованием целочисленных значений. Если не указать этот стиль, используется значение по умолчанию IndexCartesian()
.
Операторы и функции для массивов и векторов
Для массивов поддерживаются следующие операторы:
-
Унарные арифметические:
-
,+
-
Бинарные арифметические:
-
,+
,*
,/
,\
,^
-
Сравнения:
==
,!=
,≈
(isapprox
),≉
Для удобства векторизации математических и других операций в Julia поддерживается точечный синтаксис f.(args...)
, например sin.(x)
или min.(x,y)
. Он обеспечивает поэлементные операции с массивами или сочетаниями массивов и скалярных значений (операция трансляции). Дополнительным преимуществом является возможность объединения в один цикл при использовании в сочетании с другими точечными вызовами, например sin.(cos.(x))
.
Кроме того, все бинарные операторы имеют точечную версию, которую можно применять к массивам (и сочетаниям массивов и скалярных значений) в таких операциях трансляции с объединением, например z .== sin.(x .* y)
.
Обратите внимание, что операторы сравнения, например ==
, применяются к целым массивам, выдавая один логический результат. Для поэлементного сравнения используйте точечные операторы, например .==
. (В случае с такими операциями сравнения, как <
, к массивам применима только поэлементная версия .<
.)
Обратите также внимание на разницу между вызовом max.(a,b)
, который транслирует (broadcast
) функцию max
на a
и b
поэлементно, и maximum(a)
, который находит наибольшее значение в a
. Точно так же различаются вызовы min.(a,b)
и minimum(a)
.
Трансляция
Иногда бывает нужно выполнить поэлементную бинарную операцию применительно к массивам разных размеров, например прибавить вектор к каждому столбцу матрицы. Репликация вектора до размера матрицы — неэффективный способ.
julia> a = rand(2,1); A = rand(2,3);
julia> repeat(a,1,3)+A
2×3 Array{Float64,2}:
1.20813 1.82068 1.25387
1.56851 1.86401 1.67846
Когда измерения большие, расходуется много ресурсов, поэтому в Julia имеется функция broadcast
, которая расширяет отдельные измерения в аргументах массива до размеров соответствующих измерений другого массива без использования дополнительной памяти, после чего применяет указанную функцию поэлементно:
julia> broadcast(+, a, A)
2×3 Array{Float64,2}:
1.20813 1.82068 1.25387
1.56851 1.86401 1.67846
julia> b = rand(1,2)
1×2 Array{Float64,2}:
0.867535 0.00457906
julia> broadcast(+, a, b)
2×2 Array{Float64,2}:
1.71056 0.847604
1.73659 0.873631
Точечные операторы, такие как .+
и .*
, эквивалентны вызовам broadcast
(за тем исключением, что они объединяются, как описывалось выше). Также существует функция broadcast!
для явного указания места, в которое должна выполняться трансляция (для ее вызова с объединением можно также использовать оператор присваивания .=
). По сути, вызов f.(args...)
эквивалентен вызову broadcast(f, args...)
, обеспечивая удобный синтаксис для трансляции любой функции (точечный синтаксис). Вложенные точечные вызовы f.(...)
(включая вызовы .+
и т. д.) автоматически объединяются в один вызов broadcast
.
Кроме того, применимость функции broadcast
не ограничивается массивами (см. документацию по функции). Она также работает со скалярными значениями, кортежами и другими коллекциями. По умолчанию только некоторые типы аргументов считаются скалярными, включая, помимо прочего, Number
, String
, Symbol
, Type
, Function
и некоторые стандартные одинарные объекты, такие как missing
и nothing
. Все остальные аргументы итерируются или индексируются поэлементно.
julia> convert.(Float32, [1, 2])
2-element Vector{Float32}:
1.0
2.0
julia> ceil.(UInt8, [1.2 3.4; 5.6 6.7])
2×2 Matrix{UInt8}:
0x02 0x04
0x06 0x07
julia> string.(1:3, ". ", ["First", "Second", "Third"])
3-element Vector{String}:
"1. First"
"2. Second"
"3. Third"
Иногда требуется предотвратить трансляцию при итерации по элементам контейнера (например, массива), который обычно допускает трансляцию. Если поместить такой контейнер в другой контейнер (например, в элемент Tuple
), при трансляции он будет обрабатываться как одно значение.
julia> ([1, 2, 3], [4, 5, 6]) .+ ([1, 2, 3],)
([2, 4, 6], [5, 7, 9])
julia> ([1, 2, 3], [4, 5, 6]) .+ tuple([1, 2, 3])
([2, 4, 6], [5, 7, 9])
Реализация
Базовый тип массива в Julia — абстрактный тип AbstractArray{T,N}
. Он параметризуется по количеству измерений N
и типу элементов T
. AbstractVector
и AbstractMatrix
представляют собой псевдонимы для одномерного и двумерного случаев. Операции с AbstractArray
определяются с помощью высокоуровневых операторов и функций так, что они не зависят от того, как массив хранится в памяти. Как правило, они работают правильно для любой конкретной реализации массива.
К типу AbstractArray
относится все, хотя бы отдаленно напоминающее массив, причем реализация может существенно отличаться от традиционных массивов. Например, элементы могут вычисляться по запросу, а не храниться в памяти. Однако любой конкретный тип AbstractArray{T,N}
обычно должен реализовывать как минимум функцию size(A)
(возвращающую кортеж элементов типа Int
), метод getindex(A,i)
и функцию getindex(A,i1,...,iN)
. Изменяемые массивы также должны реализовывать функцию setindex!
. Желательно, чтобы временная сложность этих операций была близка к константной, иначе некоторые функции для работы с массивами могут выполняться неожиданно долго. Конкретные типы, как правило, также должны предоставлять метод similar(A,T=eltype(A),dims=size(A))
, который служит для размещения в памяти идентичного массива для функции copy
и других внешних операций. Вне зависимости от внутреннего представления AbstractArray{T,N}
T
— это тип объекта, возвращаемого при целочисленном индексировании (A[1, ..., 1]
при непустом A
), а N
должно быть длиной кортежа, возвращаемого функцией size
. Дополнительные сведения об определении пользовательских реализаций AbstractArray
см. в руководстве по интерфейсу массива в главе, посвященной интерфейсам.
DenseArray
— это абстрактный подтип типа AbstractArray
для всех массивов, элементы которых хранятся в виде непрерывной последовательности по столбцам (см. дополнительные замечания в советах по производительности). Array
— это конкретный экземпляр DenseArray
; Vector
и Matrix
представляют собой псевдонимы для одномерного и двумерного случаев. Помимо операций, требуемых для всех подтипов AbstractArray
, для Array
реализовано совсем немного специальных операций; библиотека для работы с массивами в основном является универсальной, так что все пользовательские массивы функционируют сходным образом.
SubArray
— это специализация типа AbstractArray
, которая использует для индексирования ту же область памяти, которую занимает исходный массив, вместо его копирования. Объект SubArray
создается с помощью функции view
, которая вызывается так же, как getindex
(с массивом и набором индексов в качестве аргументов). Результат функции view
выглядит так же, как результат getindex
, но данные остаются на месте. Функция view
сохраняет входные векторы индексов в объекте SubArray
, который затем можно использовать для индексирования исходного массива непрямым образом. Если поместить макрос @views
перед выражением или блоком кода, любой срез массива array[...]
в этом выражении будет преобразован для создания представления SubArray
.
BitArray
— это компактные «упакованные» логические массивы, в которых хранится по одному биту на каждое логическое значение. Их можно использовать так же, как массивы Array{Bool}
(для хранения логического значения в которых требуется один байт), и преобразовывать в такие массивы либо наоборот с помощью Array(bitarray)
и BitArray(array)
соответственно.
Массив с заданным шагом — это такой массив, элементы которого хранятся в памяти с определенным интервалом (шагом). Массив с заданным шагом можно передать вместе с поддерживаемым типом элементов во внешнюю библиотеку (не относящуюся к Julia), например BLAS или LAPACK, просто передав его указатель (pointer
) и шаг для каждого измерения. stride(A, d)
— это расстояние между элементами в измерении d
. Например, элементы массива встроенного типа Array
, возвращаемого вызовом rand(5,7,2)
, размещаются в памяти непрерывно по столбцам. Это означает, что шаг в первом измерении, то есть расстояние между элементами одного столбца, равно 1
:
julia> A = rand(5,7,2);
julia> stride(A,1)
1
Шаг во втором измерении — это расстояние между элементами в одной строке с пропуском элементов столбца (5
). Аналогичным образом, расстояние между двумя «страницами» (в третьем измерении) требует пропуска 5*7 == 35
элементов. Шаги (strides
) массива представляют собой кортеж этих трех чисел:
julia> strides(A)
(1, 5, 35)
В данном случае количество элементов, пропускаемых в памяти, соответствует количеству пропускаемых линейных индексов. Это верно только для непрерывных массивов, таких как Array
(и других подтипов DenseArray
), но в общем случае не так. Представления с индексами-диапазонами — хороший пример несвязных массивов с заданным шагом. Рассмотрим пример V = @view A[1:3:4, 2:2:6, 2:-1:1]
. Представление V
ссылается на ту же область памяти, что и массив A
, но некоторые элементы пропускаются и переупорядочиваются. Шаг первого измерения V
равен 3
, так как из исходного массива выбирается только каждая третья строка:
julia> V = @view A[1:3:4, 2:2:6, 2:-1:1];
julia> stride(V, 1)
3
Аналогичным образом, из исходного массива A
выбирается каждый второй столбец, поэтому при переходе между индексами во втором измерении должны пропускаться два столбца из пяти элементов:
julia> stride(V, 2)
10
Третье измерение интереснее, потому что порядок здесь обратный! Для перехода от первой «страницы» ко второй необходимо следовать в памяти в обратном направлении, поэтому шаг в этом измерении отрицательный!
julia> stride(V, 3)
-35
Это означает, что pointer
для представления V
на самом деле указывает на середину блока памяти, занимаемого массивом A
, и ссылается на элементы в памяти как в прямом, так и в обратном направлении. Дополнительные сведения об определении собственных массивов с заданным шагом см. в руководстве по интерфейсу для массивов с заданным шагом. StridedVector
и StridedMatrix
— это удобные псевдонимы для многих встроенных типов массивов, которые считаются массивами с заданным шагом. Они позволяют выбирать конкретные реализации, которые вызывают специализированные, оптимизированные функции BLAS и LAPACK, используя только указатель и количество шагов массива.
Важно подчеркнуть, что шаги массива связаны со смещением в памяти, а не с индексированием. Если вам требуется преобразование линейного (одиночного) индексирования в декартово (множественное) или наоборот, см. описание типов LinearIndices
и CartesianIndices
.