Одномерные и многомерные массивы
Julia, как и большинство языков технического вычисления, предоставляет реализацию массивов первого класса. Большинство языков технического вычисления уделяют много внимания реализации массивов в ущерб другим контейнерам. Julia не обрабатывает массивы каким-либо особым образом. Библиотека массивов реализована почти полностью в самой Julia и получает производительность от компилятора, как и любой другой код, написанный на Julia. Таким образом, также возможно определить пользовательские типы массивов, унаследовав от AbstractArray. Более подробную информацию об имплементации пользовательского типа массива см. в разделе руководства по интерфейсу AbstractArray.
Массив — это коллекция объектов, хранящихся в многомерной сетке. Разрешены нульмерные массивы, см. эту запись в FAQ. В общем случае массив может содержать объекты типа 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...) |
массив со случайными, независимыми и равномерно распределенными значениями. Для типов с плавающей точкой T, значения лежат в полуоткрытом интервале $[0, 1)$. |
randn(T, dims...) |
массив со случайными, независимыми и стандартно нормально распределенными значениями |
Matrix{T}(I, m, n) |
матрица m×n единичного порядка. Требуется using LinearAlgebra для I. |
range(start, stop, n) |
диапазон из n линейно распределённых элементов от start до stop
|
fill!(A, x) |
заполнение массива A значением x
|
fill(x, dims...) |
массив, заполненный значением x. В частности, fill(x) создает нульмерный 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}; это включает литерал [] без указанных аргументов. Литералы массивов могут быть типизированными с помощью синтаксиса T[A, B, C, ...], где T — тип.
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> Float32[1, 2.3, 4//5] # Specify element type manually
3-element Vector{Float32}:
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[[]](не путать с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
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. Если нужны только значения, лучше итерировать массив напрямую, т. е. 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] # 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 предоставляет эффективный способ итерации по любому типу массива. Кроме того, это также поддерживает обобщённые массивы с пользовательской индексацией, такие как OffsetArrays.
Свойства массивов
Если вы создаёте пользовательский тип AbstractArray, вы можете указать, что он имеет быструю линейную индексацию, используя
Base.IndexStyle(::Type{<:MyArray}) = IndexLinear()
Это заставит итерацию по 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 не ограничен массивами (см. документацию функции); он также обрабатывает скаляры, кортежи и другие коллекции. По умолчанию в качестве скаляров рассматриваются только некоторые типы аргументов, включая (но не ограничиваясь) 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) имеет свои элементы, расположенные по столбцам (column-major order). Это означает, что шаг первой размерности — расстояние между элементами в одном столбце — равен 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–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.10/manual/arrays/