Одномерные и многомерные массивы
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:12, 2, 3, 2)
2×3×2 reshape(::UnitRange{Int64}, 2, 3, 2) with eltype Int64:
[:, :, 1] =
1 3 5
2 4 6
[:, :, 2] =
7 9 11
8 10 12
julia> x[:, [true false; false true; true false]]
2×3 Matrix{Int64}:
1 5 9
2 6 10
julia> mask = map(ispow2, x)
2×3×2 Array{Bool, 3}:
[:, :, 1] =
1 0 0
1 1 0
[:, :, 2] =
0 0 0
1 0 0
julia> x[mask]
4-element Vector{Int64}:
1
2
4
8
julia> x[vec(mask)] == x[mask] # we can also index with a single Boolean vector
true
Количество индексов
Декартово индексирование
Обычный способ индексирования многомерного массива 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.11/manual/arrays/