Многомерные массивы
Julia, как и большинство языков технических вычислений, предоставляет реализацию массивов первого класса. Большинство языков технических вычислений уделяют много внимания реализации массивов в ущерб другим контейнерам. Julia не рассматривает массивы каким-либо особым образом. Библиотека массивов реализована практически полностью на самом языке Julia и получает производительность от компилятора, как и любой другой код, написанный на Julia. Таким образом, также возможно определить пользовательские типы массивов, унаследовав от AbstractArray. Более подробную информацию о реализации пользовательского типа массива см. в разделе руководства по интерфейсу AbstractArray.
Массив — это коллекция объектов, хранящихся в многомерной сетке. В самом общем случае массив может содержать объекты типа Any. Для большинства вычислительных целей массивы должны содержать объекты более конкретного типа, например, Float64 или Int32.
В общем случае, в отличие от многих других языков технических вычислений, Julia не ожидает, что программы будут написаны в векторизованном стиле для повышения производительности. Компилятор Julia использует выведение типов и генерирует оптимизированный код для скалярной индексации массивов, что позволяет писать программы в удобном и читаемом стиле, не жертвуя производительностью и иногда используя меньше памяти.
Во всех аргументах функций Julia используется передача по ссылке (т. е. по указателям). Некоторые языки технических вычислений передают массивы по значению, и хотя это предотвращает случайное изменение значения вызывающим функцией, это затрудняет избегание ненужного копирования массивов. По соглашению, имя функции, оканчивающееся на ! означает, что она будет изменять или уничтожать значение одного или нескольких аргументов (например, сравните sort и sort!). Вызывающие функции должны делать явные копии, чтобы гарантировать, что они не изменяют входные данные, которые не намерены изменять. Многие не изменяющие функции реализуются путём вызова функции с тем же именем и добавленным ! в конце на явной копии ввода и возврата этой копии.
Основные функции
| Функция | Описание |
|---|---|
eltype(A) |
тип элементов, содержащихся в A
|
length(A) |
число элементов в A
|
ndims(A) |
число измерений A
|
size(A) |
кортеж, содержащий размеры A
|
size(A,n) |
размер A по измерению n
|
axes(A) |
кортеж, содержащий допустимые индексы A
|
axes(A,n) |
диапазон, выражающий допустимые индексы по измерению n
|
eachindex(A) |
эффективный итератор для посещения каждой позиции в A
|
stride(A,k) |
шаг (расстояние линейного индекса между смежными элементами) по измерению k
|
strides(A) |
кортеж шагов по каждому измерению |
Создание и инициализация
Предлагается множество функций для создания и инициализации массивов. В следующем списке функций вызовы с аргументом dims... могут принимать либо единственный кортеж размеров измерений, либо серию размеров измерений, передаваемых в качестве переменного числа аргументов. Большинство этих функций также принимают первый вход T, который является типом элементов массива. Если тип T опущен, он по умолчанию будет Float64.
| Функция | Описание |
|---|---|
Array{T}(undef, dims...) |
неинициализированная плотная Array
|
zeros(T, dims...) |
массив из нулей |
ones(T, dims...) |
массив из единиц |
trues(dims...) |
массив BitArray со всеми значениями true
|
falses(dims...) |
массив со всеми значениями BitArray false
|
reshape(A, dims...) |
массив, содержащий те же данные, что и A, но с другими измерениями |
copy(A) |
копирование A
|
deepcopy(A) |
копирование A, рекурсивно копируя его элементы |
similar(A, T, dims...) |
неинициализированный массив того же типа, что и A (плотный, разреженный и т. д.), но с указанным типом элементов и размерами. Второй и третий аргументы являются необязательными, по умолчанию принимая тип элементов и размеры A в случае пропуска. |
reinterpret(T, A) |
массив с теми же двоичными данными, что и A, но с типом элементов T
|
rand(T, dims...) |
массив со случайными, независимыми и одинаково распределёнными значениями в полуоткрытом интервале $[0, 1)$ |
randn(T, dims...) |
массив со случайными, независимыми и стандартно нормально распределенными значениями |
Matrix{T}(I, m, n) |
m-на-n единичная матрица. Требует using LinearAlgebra для I. |
range(start, stop=stop, length=n) |
диапазон линейно распределенных элементов от start до stop
|
fill!(A, x) |
заполнение массива A значением x
|
fill(x, dims...) |
массив, заполненный значением x
|
Чтобы увидеть различные способы передачи измерений в эти функции, рассмотрите следующие примеры:
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}; это включает в себя литерал [], где не указано никаких аргументов.
julia> [1,2,3] # An array of `Int`s
3-element Vector{Int64}:
1
2
3
julia> promote(1, 2.3, 4//5) # This combination of Int, Float64 and Rational promotes to Float64
(1.0, 2.3, 0.8)
julia> [1, 2.3, 4//5] # Thus that's the element type of this Array
3-element Vector{Float64}:
1.0
2.3
0.8
julia> []
Any[]
Конкатенация
Если аргументы в квадратных скобках разделены точками с запятой (;) или новыми строками вместо запятых, то их содержимое вертикально конкатенируется вместо использования аргументов в качестве элементов самих по себе.
julia> [1:2, 4:5] # Has a comma, so no concatenation occurs. The ranges are themselves the elements
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] # Numbers can also be horizontally concatenated
1×3 Matrix{Int64}:
1 2 3
Использование точек с запятой (или новых строк) и пробелов (или табуляции) может комбинироваться для одновременной горизонтальной и вертикальной конкатенации.
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
В более общем случае, конкатенация может быть выполнена с помощью функции cat. Эти синтаксические конструкции являются сокращениями для вызовов функций, которые сами являются удобными функциями:
| Синтаксис | Функция | Описание |
|---|---|---|
cat |
конкатенация входных массивов вдоль размерности(ей) k
|
|
[A; B; C; ...] |
vcat |
сокращение для `cat(A...; dims=1) |
[A B C ...] |
hcat |
сокращение для `cat(A...; dims=2) |
[A B; C D; ...] |
hvcat |
одновременная вертикальная и горизонтальная конкатенация |
Массивы с типом элементов
Массив с определённым типом элементов может быть создан с использованием синтаксиса 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] # all scalar indices
3
julia> A[[1, 2], [1], [1, 2], [1]] # all vector indices
2×1×2×1 Array{Int64, 4}:
[:, :, 1, 1] =
1
2
[:, :, 2, 1] =
5
6
julia> A[[1, 2], [1], [1, 2], 1] # a mix of index types
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: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
Декартовы индексы
Специальный объект 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
CartesianIndex и массивы CartesianIndex несовместимы со словом end для представления последнего индекса измерения. Не используйте end в выражениях индексирования, которые могут содержать либо CartesianIndex или массивы этих объектов.
Логическое индексирование
Часто упоминаемое как логическое индексирование или индексирование с логической маской, индексирование с помощью булевого массива выбирает элементы в тех индексах, где его значения равны true. Индексирование с помощью булевого вектора B по существу эквивалентно индексированию вектором целых чисел, который возвращается findall(B). Аналогично, индексирование с помощью N-мерного булевого массива по существу эквивалентно индексированию вектором CartesianIndex{N}s, где его значения равны 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, чтобы определить, какой из них используется). Таким образом, при итерации по всему массиву лучше итерироваться по eachindex(A), а не по 1:length(A). Последнее не только будет намного быстрее в случаях, где A является IndexCartesian, но также будет поддерживать OffsetArrays.
Пропущенные и дополнительные индексы
В дополнение к линейному индексированию 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] # Omits the fourth dimension (length 1)
19
julia> A[1, 3] # Attempts to omit dimensions 3 & 4 (lengths 2 and 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] # Linear indexing
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
# Do something with the element a
end
for i in eachindex(A)
# Do something with i and/or 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)
В отличие от for i = 1:length(A), итерация с помощью eachindex обеспечивает эффективный способ итерации по любому типу массива.
Свойства массивов
Если вы пишете пользовательский тип 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 поэлементно над 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! для указания явного назначения (которое также можно получить с помощью .= присваивания) fusion style. Фактически, f.(args...) эквивалентно broadcast(f, args...), предоставляя удобный синтаксис для вещания любой функции (синтаксис точки). Вложенные "вызовы с точкой" f.(...) (включая вызовы .+ и так далее) автоматически сливаются в один вызов broadcast.
Кроме того, broadcast не ограничивается массивами (см. документацию функции); он также обрабатывает скаляры, кортежи и другие коллекции. По умолчанию, только некоторые типы аргументов считаются скалярами, включая (но не ограничиваясь) Numbers, Strings, Symbols, Types, Functions и некоторые общие одиночные значения, такие как 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 — псевдонимы для случаев с 1 и 2 измерениями. Операции над объектами 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 — псевдонимы для случаев с 1 и 2 измерениями. Очень мало операций реализовано специально для Array за пределами тех, которые необходимы для всех AbstractArrays; большая часть библиотеки массивов реализована обобщенным образом, что позволяет всем пользовательским массивам вести себя аналогичным образом.
SubArray — это специализация AbstractArray для выполнения индексирования путем совместного использования памяти с исходным массивом, а не путем его копирования. SubArray создается с помощью функции view, которая вызывается так же, как и getindex (с массивом и набором аргументов индекса). Результат view выглядит так же, как и результат getindex, за исключением того, что данные остаются на месте. view сохраняет входные векторы индексов в объекте SubArray , который в дальнейшем можно использовать для косвенного индексирования исходного массива. Разместив макрос @views перед выражением или блоком кода, любой срез array[...] в этом выражении будет преобразован для создания представления SubArray вместо этого.
BitArray — это экономичные «упакованные» булевы массивы, которые хранят по одному биту на каждое булево значение. Их можно использовать аналогично Array{Bool} массивам (которые хранят по одному байту на каждое булево значение), и их можно преобразовать в/из последних с помощью Array(bitarray) и BitArray(array) соответственно.
Массив «строчный» (strided), если он хранится в памяти с определенными интервалами (шагами) между его элементами. Строчный массив с поддерживаемым типом элемента можно передать во внешнюю (не 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.
- 1iid, независимые и одинаково распределенные.
© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.6.0/manual/arrays/