Многомерные массивы
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) |
диапазон n линейно расположенных элементов от start до stop
|
fill!(A, x) |
заполнить массив A значением x
|
fill(x, dims...) |
массив, заполненный значением Array 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]
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 |
конкатенация входных массивов по измерению(ям) k
|
|
[A; B; C; ...] |
vcat |
синтаксический сахар для `cat(A...; dims=1) |
[A B C ...] |
hcat |
синтаксический сахар для `cat(A...; dims=2) |
[A B; C D; ...] |
hvcat |
одновременная вертикальная и горизонтальная конкатенация |
[A; C;; B; D;;; ...] |
hvncat |
одновременная конкатенация 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] # 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}ов, где его значения 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 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 являются псевдонимами для случаев с 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 за пределами тех, что требуются для всех AbstractArray-объектов; большая часть библиотеки массивов реализована обобщенно, что позволяет всем пользовательским массивам вести себя аналогично.
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.
- 1iid, независимые и одинаково распределённые.
© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.7.0/manual/arrays/