Одномерные и многомерные массивы
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
Заключительные точки с запятой также могут использоваться для добавления одномерных измерений с trailing длиной 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)
В таких случаях результат всегда 1-мерный.
Сгенерированные значения могут быть отфильтрованы с помощью ключевого слова 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[], этот семантический элемент обеспечивает простой idiom для извлечения единственного элемента в массиве и одновременного обеспечения того, что был только один элемент.
Аналогично, можно предоставить более 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()
Это приведет к тому, что итерация 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) соответственно.
Массив считается «шагированным» (strided), если он хранится в памяти с определёнными расстояниями (шагами) между элементами. Шагированный массив с поддерживаемым типом элементов можно передать во внешнюю (не-Julia) библиотеку, такую как BLAS или LAPACK, просто передав его pointer и шаг для каждой размерности. stride(A, d) — это расстояние между элементами вдоль размерности d. Например, встроенный Array, возвращаемый rand(5,7,2), имеет элементы, расположенные по порядку столбцов. Это означает, что шаг первой размерности — расстояние между элементами в одном столбце — равен 1:
julia> A = rand(5,7,2); julia> stride(A,1) 1
Шаг второй размерности — это расстояние между элементами в одной строке, пропуская столько элементов, сколько их в одном столбце (5). Аналогично, переход между двумя «страницами» (в третьей размерности) требует пропуска 5*7 == 35 элементов. strides этого массива — кортеж из этих трёх чисел:
julia> strides(A) (1, 5, 35)
В этом конкретном случае, количество пропущенных элементов в памяти совпадает с количеством пропущенных линейных индексов. Это верно только для непрерывных массивов, таких как Array (и других DenseArray подтипов), и неверно в общем случае. Виды с индексами диапазона — хороший пример непрерывных шагированных массивов; рассмотрите V = @view A[1:3:4, 2:2:6, 2:-1:1]. Этот вид V ссылается на ту же память, что и A , но пропускает и переупорядочивает некоторые из своих элементов. Шаг первой размерности V равен 3 , так как мы выбираем только каждую третью строку из исходного массива:
julia> V = @view A[1:3:4, 2:2:6, 2:-1:1]; julia> stride(V, 1) 3
Аналогично, этот вид выбирает каждую вторую колонку из нашего исходного A — и, следовательно, ему необходимо пропустить эквивалент двух столбцов по пять элементов при перемещении между индексами во второй размерности:
julia> stride(V, 2) 10
Третья размерность интересна тем, что её порядок обращён! Таким образом, чтобы перейти от первой «страницы» ко второй, необходимо двигаться назад в памяти, поэтому её шаг в этой размерности отрицательный!
julia> stride(V, 3) -35
Это означает, что pointer для V фактически указывает в середину блока памяти A, и он ссылается на элементы как вперёд, так и назад в памяти. Подробную информацию о определении собственных шагированных массивов см. в руководстве по интерфейсу шагированных массивов. StridedVector и StridedMatrix — удобные псевдонимы для многих встроенных типов массивов, которые считаются шагированными массивами, позволяя им отправлять вызовы на специализированные реализации, которые вызывают высоконастроенные и оптимизированные функции BLAS и LAPACK, используя только указатель и шаги.
Стоит подчеркнуть, что шаги относятся к смещениям в памяти, а не к индексации. Если вам нужно преобразовать между линейной (одиночный индекс) и декартовой (многомерный индекс) индексацией, см. LinearIndices и CartesianIndices.
- 1iid, независимые и одинаково распределённые.
© 2009–2023 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.9/manual/arrays/