Многомерные массивы
Julia, как и большинство языков технических вычислений, предоставляет реализацию массивов первого класса. Большинство языков технических вычислений уделяют много внимания реализации массивов в ущерб другим контейнерам. Julia не рассматривает массивы каким-то особенным образом. Библиотека массивов реализована почти полностью на самом языке Julia и получает производительность от компилятора, как и любой другой код, написанный на Julia. Таким образом, также можно определять пользовательские типы массивов, наследуя от AbstractArray. Подробнее о реализации пользовательского типа массива см. в разделе руководства по интерфейсу AbstractArray.
Массив — это коллекция объектов, хранящихся в многомерной сетке. В общем случае массив может содержать объекты типа Any. Для большинства вычислительных целей массивы должны содержать объекты более конкретного типа, такие как Float64 или Int32.
В общем, в отличие от многих других языков технических вычислений, Julia не ожидает, что программы будут написаны в векторизованном стиле для повышения производительности. Компилятор Julia использует вывод типов и генерирует оптимизированный код для скалярной индексации массивов, что позволяет писать программы удобным и читаемым стилем, не жертвуя производительностью и иногда используя меньше памяти.
Во всех функциях Julia аргументы передаются по ссылке (т.е. по указателям). Некоторые языки технических вычислений передают массивы по значению, и хотя это предотвращает случайное изменение вызываемыми функциями значения в вызывающей функции, это затрудняет избежание нежелательного копирования массивов. По соглашению, имя функции, оканчивающееся на ! , указывает, что она будет изменять или уничтожать значение одного или нескольких своих аргументов (например, сравните sort и sort!). Вызываемые функции должны выполнять явные копии, чтобы убедиться, что они не изменяют входные данные, которые они не намерены изменять. Многие не изменяющие функции реализуются путем вызова функции с тем же именем и добавленным ! в конце на явной копии входных данных и возвращают эту копию.
Основные функции
| Функция | Описание |
|---|---|
eltype(A) |
тип элементов, содержащихся в A
|
length(A) |
количество элементов в A
|
ndims(A) |
количество измерений A
|
size(A) |
кортеж, содержащий размеры A
|
size(A,n) |
размер A по измерению n
|
axes(A) |
кортеж, содержащий допустимые индексы A
|
axes(A,n) |
диапазон, выражающий допустимые индексы по измерению n
|
eachindex(A) |
эффективный итератор для посещения каждой позиции в A
|
stride(A,k) |
шаг (линейное расстояние между соседними элементами) по измерению k
|
strides(A) |
кортеж шагов по каждому измерению |
Создание и инициализация
Предоставляется много функций для создания и инициализации массивов. В следующем списке таких функций вызовы с аргументом dims... могут принимать либо один кортеж размеров измерений, либо серию размеров измерений, передаваемых в качестве переменного числа аргументов. Большинство этих функций также принимают первый входной T, который является типом элементов массива. Если тип T опущен, он будет по умолчанию Float64.
| Функция | Описание |
|---|---|
Array{T}(undef, dims...) |
неинициализированная плотная Array
|
zeros(T, dims...) |
массив со всеми нулями |
ones(T, dims...) |
массив со всеми единицами |
trues(dims...) |
массив BitArray со всеми значениями true
|
falses(dims...) |
массив со всеми значениями BitArray false
|
reshape(A, dims...) |
массив, содержащий те же данные, что и A, но с другими размерами |
copy(A) |
копия A
|
deepcopy(A) |
копия A, рекурсивно копирующая ее элементы |
similar(A, T, dims...) |
неинициализированный массив того же типа, что и A (плотный, разреженный и т.д.), но с указанным типом элемента и размерами. Второй и третий аргументы являются необязательными, по умолчанию принимая тип элемента и размеры A , если они опущены. |
reinterpret(T, A) |
массив с теми же двоичными данными, что и A, но с типом элемента T
|
rand(T, dims...) |
массив со случайными, независимыми и равномерно распределенными значениями в полуоткрытом интервале $[0, 1)$ |
randn(T, dims...) |
массив со случайными, независимыми и стандартно нормально распределенными значениями |
Matrix{T}(I, m, n) |
m-на-n единичная матрица. Требуется using LinearAlgebra для I. |
range(start, stop=stop, length=n) |
диапазон n линейно расположенных элементов от start до stop
|
fill!(A, x) |
заполнить массив A значением x
|
fill(x, dims...) |
массив, заполненный значением Array x
|
Чтобы увидеть различные способы передачи размеров этим функциям, рассмотрите следующие примеры:
julia> zeros(Int8, 2, 3)
2×3 Array{Int8,2}:
0 0 0
0 0 0
julia> zeros(Int8, (2, 3))
2×3 Array{Int8,2}:
0 0 0
0 0 0
julia> zeros((2, 3))
2×3 Array{Float64,2}:
0.0 0.0 0.0
0.0 0.0 0.0
Здесь, (2, 3) является Tuple, а первый аргумент — тип элемента — является необязательным, по умолчанию Float64.
Литералы массивов
Массивы также могут быть непосредственно созданы с использованием квадратных скобок; синтаксис [A, B, C, ...] создает одномерный массив (т.е. вектор), содержащий аргументы, разделенные запятыми, в качестве элементов. Тип элемента (eltype) результирующего массива автоматически определяется типами аргументов в фигурных скобках. Если все аргументы имеют один и тот же тип, то это и есть его eltype. Если все они имеют общий тип повышения, то они преобразуются к этому типу с помощью convert, и этот тип является типом массива eltype. В противном случае создается гетерогенный массив, который может содержать все — Vector{Any} — включая литерал [] , когда аргументов нет.
julia> [1,2,3] # An array of `Int`s
3-element Array{Int64,1}:
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 Array{Float64,1}:
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 Array{UnitRange{Int64},1}:
1:2
4:5
julia> [1:2; 4:5]
4-element Array{Int64,1}:
1
2
4
5
julia> [1:2
4:5
6]
5-element Array{Int64,1}:
1
2
4
5
6
Аналогично, если аргументы разделены табуляцией или пробелами, то их содержимое горизонтально объединяется.
julia> [1:2 4:5 7:8]
2×3 Array{Int64,2}:
1 4 7
2 5 8
julia> [[1,2] [4,5] [7,8]]
2×3 Array{Int64,2}:
1 4 7
2 5 8
julia> [1 2 3] # Numbers can also be horizontally concatenated
1×3 Array{Int64,2}:
1 2 3
Использование точек с запятой (или новых строк) и пробелов (или табуляции) может быть комбинировано для объединения как горизонтально, так и вертикально одновременно.
julia> [1 2
3 4]
2×2 Array{Int64,2}:
1 2
3 4
julia> [zeros(Int, 2, 2) [1; 2]
[3 4] 5]
3×3 Array{Int64,2}:
0 0 1
0 0 2
3 4 5
В более общем случае, объединение может быть выполнено с помощью функции cat. Эти синтаксические конструкции являются сокращениями для вызовов функций, которые сами по себе являются удобными функциями:
| Синтаксис | Функция | Описание |
|---|---|---|
cat |
объединяет входные массивы по размерности(ям) k
|
|
[A; B; C; ...] |
vcat |
сокращение для `cat(A...; dims=1) |
[A B C ...] |
hcat |
сокращение для `cat(A...; dims=2) |
[A B; C D; ...] |
hvcat |
одновременное вертикальное и горизонтальное объединение |
Массивы фиксированного типа
Массив с определенным типом элементов может быть создан с использованием синтаксиса T[A, B, C, ...]. Это создаст одномерный массив с типом элемента T, инициализированный элементами A, B, C, и так далее. Например, Any[x, y, z] создает разнородный массив, который может содержать любые значения.
Синтаксис объединения аналогичным образом может быть префиксным типом для указания типа элемента результата.
julia> [[1 2] [3 4]]
1×4 Array{Int64,2}:
1 2 3 4
julia> Int8[[1 2] [3 4]]
1×4 Array{Int8,2}:
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 Array{Tuple{Float64,Int64},2}:
(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 Array{Tuple{Int64,Int64},1}:
(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 Array{Tuple{Int64,Int64},1}:
(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 Array{Int64,2}:
1 2
1 2
julia> A[[1 2; 1 2], 1, 2, 1]
2×2 Array{Int64,2}:
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 Array{Int64,2}:
6 10
7 11
julia> x[1, [2 3; 4 1]]
2×2 Array{Int64,2}:
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, преобразуя при необходимости к 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 Array{Int64,2}:
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 Array{Int64,2}:
-1 -4 7
-2 -5 8
3 6 -9
Поддерживаемые типы индексов
В выражении A[I_1, I_2, ..., I_n], каждый I_k может быть скалярным индексом, массивом скалярных индексов или объектом, представляющим массив скалярных индексов и который может быть преобразован в такой с помощью to_indices:
- Скалярный индекс. По умолчанию он включает:
- Целые числа, не являющиеся булевыми
-
CartesianIndex{N}ы, которые ведут себя какN-кортеж целых чисел, охватывающих несколько измерений (см. подробности ниже)
- Массив скалярных индексов. Это включает:
- Векторы и многомерные массивы целых чисел
- Пустые массивы, такие как
[], которые не выбирают элементов - Диапазоны, такие как
a:cилиa:b:c, которые выбирают смежные или с шагом подмножества отaдоc(включительно) - Любой пользовательский массив скалярных индексов, являющийся подтипом
AbstractArray - Массивы
CartesianIndex{N}(см. подробности ниже)
- Объект, представляющий массив скалярных индексов и который может быть преобразован в него с помощью
to_indices. По умолчанию он включает:-
Colon()(:), который представляет все индексы в рамках всего измерения или всего массива - Массивы булевых значений, которые выбирают элементы в их
trueиндексах (см. подробности ниже)
-
Некоторые примеры:
julia> A = reshape(collect(1:2:18), (3, 3))
3×3 Array{Int64,2}:
1 7 13
3 9 15
5 11 17
julia> A[4]
7
julia> A[[2, 5, 8]]
3-element Array{Int64,1}:
3
9
15
julia> A[[1 4; 3 8]]
2×2 Array{Int64,2}:
1 7
5 15
julia> A[[]]
Int64[]
julia> A[1:2:5]
3-element Array{Int64,1}:
1
5
9
julia> A[2, :]
3-element Array{Int64,1}:
3
9
15
julia> A[:, 3]
3-element Array{Int64,1}:
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 Array{Int64,2}:
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 Array{Int64,1}:
1
6
11
16
Это можно выразить гораздо проще с помощью точечного векторизованного умножения и объединения его с обычным целочисленным индексом (вместо выделения первой page из A как отдельного шага). Это даже можно объединить с : для одновременного выделения обеих диагоналей с двух страниц:
julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), 1]
4-element Array{Int64,1}:
1
6
11
16
julia> A[CartesianIndex.(axes(A, 1), axes(A, 2)), :]
4×2 Array{Int64,2}:
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 Array{Int64,2}:
2 6 10 14
3 7 11 15
julia> mask = map(ispow2, x)
4×4 Array{Bool,2}:
1 0 0 0
1 0 0 0
0 0 0 0
1 1 0 1
julia> x[mask]
5-element Array{Int64,1}:
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 Array{Int64,2}:
2 6
4 7
3 1
julia> A[5]
7
julia> vec(A)[5]
7
Линейный индекс в массиве A можно преобразовать в CartesianIndex для декартова индексирования с помощью CartesianIndices(A)[i] (см. CartesianIndices), а набор N декартовых индексов можно преобразовать в линейный индекс с помощью LinearIndices(A)[i_1, i_2, ..., i_N] (см. LinearIndices).
julia> CartesianIndices(A)[5] CartesianIndex(2, 2) julia> LinearIndices(A)[2, 2] 5
Важно отметить, что существует очень большая асимметрия в производительности этих преобразований. Преобразование линейного индекса в набор декартовых индексов требует деления и нахождения остатка, в то время как обратное преобразование - это просто умножения и сложения. В современных процессорах целочисленное деление может быть в 10-50 раз медленнее, чем умножение. В то время как некоторые массивы, такие как Array сам, реализуются с использованием линейного блока памяти и напрямую используют линейный индекс в своих реализациях, другие массивы, такие как Diagonal, нуждаются в полном наборе декартовых индексов для выполнения своего поиска (см. IndexStyle, чтобы узнать, какой из них используется). Поэтому при итерации по всему массиву гораздо лучше использовать итерацию по eachindex(A), а не 1:length(A). Это не только будет намного быстрее в тех случаях, когда A является IndexCartesian, но и будет поддерживать OffsetArrays.
Пропущенные и дополнительные индексы
Помимо линейного индексирования, N-мерный массив может быть индексирован с меньшим или большим количеством N индексов в определенных ситуациях.
Индексы могут быть опущены, если все последующие измерения, в которые не производится индексирование, имеют длину один. Другими словами, последующие индексы могут быть опущены только в том случае, если для этих опущенных индексов существует только одно возможное значение для индексирования в пределах границ. Например, четырехмерный массив размером (3, 4, 2, 1) может быть индексирован только тремя индексами, так как размерность, которая пропущена (четвертое измерение), имеет длину один. Обратите внимание, что линейное индексирование имеет приоритет перед этим правилом.
julia> A = reshape(1:24, 3, 4, 2, 1)
3×4×2×1 reshape(::UnitRange{Int64}, 3, 4, 2, 1) with eltype Int64:
[:, :, 1, 1] =
1 4 7 10
2 5 8 11
3 6 9 12
[:, :, 2, 1] =
13 16 19 22
14 17 20 23
15 18 21 24
julia> A[1, 3, 2] # Omits the fourth dimension (length 1)
19
julia> A[1, 3] # Attempts to omit dimensions 3 & 4 (lengths 2 and 1)
ERROR: BoundsError: attempt to access 3×4×2×1 reshape(::UnitRange{Int64}, 3, 4, 2, 1) with eltype Int64 at index [1, 3]
julia> A[19] # Linear indexing
19
При пропуске *всех* индексов с A[], эта семантика предоставляет простой способ получить единственный элемент в массиве и одновременно убедиться, что там был только один элемент.
Аналогично, может быть предоставлено более N индексов, если все индексы, выходящие за пределы размерности массива, 1 (или, более обобщенно, являются первым и единственным элементом axes(A, d) где d это номер конкретной размерности). Это позволяет индексировать векторы как матрицы с одним столбцом, например:
julia> A = [8,6,7]
3-element Array{Int64,1}:
8
6
7
julia> A[2,1]
6
Итерация
Рекомендуемые способы итерации по всему массиву
for a in A
# Do something with the element a
end
for i in eachindex(A)
# Do something with i and/or A[i]
end
Первый конструкт используется, когда вам нужно значение, а не индекс каждого элемента. Во втором конструкте i будет Int, если A это тип массива с быстрым линейным индексированием; в противном случае это будет CartesianIndex.
julia> A = rand(4,3);
julia> B = view(A, 1:3, 2:3);
julia> for i in eachindex(B)
@show i
end
i = CartesianIndex(1, 1)
i = CartesianIndex(2, 1)
i = CartesianIndex(3, 1)
i = CartesianIndex(1, 2)
i = CartesianIndex(2, 2)
i = CartesianIndex(3, 2)
В отличие от for i = 1:length(A), итерация с помощью eachindex предоставляет эффективный способ итерации по любому типу массива.
Свойства массивов
Если вы пишете пользовательский тип AbstractArray, вы можете указать, что он имеет быстрое линейное индексирование с помощью
Base.IndexStyle(::Type{<:MyArray}) = IndexLinear()
Эта настройка заставит итерацию eachindex по MyArray использовать целые числа. Если вы не укажете это свойство, используется значение по умолчанию IndexCartesian().
Массивы и векторизованные операторы и функции
Для массивов поддерживаются следующие операторы:
- Унарная арифметика –
-,+ - Бинарная арифметика –
-,+,*,/,\,^ - Сравнение –
==,!=,≈(isapprox),≉
Для обеспечения удобной векторизации математических и других операций Julia предоставляет синтаксис точки f.(args...), например sin.(x) или min.(x,y), для поэлементных операций над массивами или смесями массивов и скаляров (операция распространения); эти операции обладают дополнительным преимуществом «слияния» в один цикл при сочетании с другими вызовами точек, например sin.(cos.(x)).
Кроме того, *каждый* бинарный оператор поддерживает точечную версию, которая может применяться к массивам (и комбинациям массивов и скаляров) в таких слитых операциях распространения, например z .== sin.(x .* y).
Обратите внимание, что сравнения, такие как ==, работают с целыми массивами, предоставляя один булевый ответ. Используйте операторы с точкой, такие как .==, для элементных сравнений. (Для операций сравнения, таких как <, только элементная .< версия применима к массивам.)
Также обратите внимание на разницу между max.(a,b), который broadcast max по элементам над a и b, и maximum(a), который находит наибольшее значение в a. Такая же взаимосвязь существует для min.(a,b) и minimum(a).
Вещание
Иногда бывает полезно выполнять бинарные операции по элементам над массивами разных размеров, например, добавлять вектор к каждому столбцу матрицы. Неэффективный способ сделать это — продублировать вектор до размера матрицы:
julia> a = rand(2,1); A = rand(2,3);
julia> repeat(a,1,3)+A
2×3 Array{Float64,2}:
1.20813 1.82068 1.25387
1.56851 1.86401 1.67846
Это расточительно, когда размерности становятся большими, поэтому Julia предоставляет broadcast, который расширяет одноэлементные размерности в аргументах массива, чтобы соответствовать соответствующей размерности в другом массиве без использования дополнительной памяти, и применяет заданную функцию по элементам:
julia> broadcast(+, a, A)
2×3 Array{Float64,2}:
1.20813 1.82068 1.25387
1.56851 1.86401 1.67846
julia> b = rand(1,2)
1×2 Array{Float64,2}:
0.867535 0.00457906
julia> broadcast(+, a, b)
2×2 Array{Float64,2}:
1.71056 0.847604
1.73659 0.873631
Операторы с точкой, такие как .+ и .* эквивалентны вызовам broadcast (за исключением того, что они объединяются, как описано выше). Также существует функция broadcast! для указания явного назначения (к которому также можно получить доступ с помощью объединяющего способа с помощью .= присваивания). На самом деле, f.(args...) эквивалентно broadcast(f, args...), предоставляя удобный синтаксис для вещания любой функции (синтаксис точки). Вложенные "вызовы с точкой" f.(...) (включая вызовы .+ и т.д.) автоматически объединяются в один вызов broadcast.
Кроме того, broadcast не ограничивается массивами (см. документацию функции); она также обрабатывает скаляры, кортежи и другие коллекции. По умолчанию в качестве скаляров рассматриваются только некоторые типы аргументов, включая (но не ограничиваясь) Numberы, Stringы, Symbolы, Typeы, Functionы и некоторые общие одиночные элементы, такие как missing и nothing. Все остальные аргументы перебираются или индексируются по элементам.
julia> convert.(Float32, [1, 2])
2-element Array{Float32,1}:
1.0
2.0
julia> ceil.(UInt8, [1.2 3.4; 5.6 6.7])
2×2 Array{UInt8,2}:
0x02 0x04
0x06 0x07
julia> string.(1:3, ". ", ["First", "Second", "Third"])
3-element Array{String,1}:
"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!. Рекомендуется, чтобы эти операции имели сложность, близкую к постоянной, или, технически, сложность Õ(1), иначе некоторые функции массивов могут быть неожиданно медленными. Конкретные типы также обычно должны предоставлять метод similar(A,T=eltype(A),dims=size(A)), который используется для выделения похожего массива для copy и других операций вне места. Независимо от того, как AbstractArray{T,N} представлен внутри, T — это тип объекта, возвращаемого индексацией по целому числу (A[1, ..., 1], когда A не пусто), а N должно быть длиной кортежа, возвращаемого функцией size. Для получения более подробной информации о определении собственных реализаций AbstractArray см. руководство по интерфейсу массива в главе про интерфейсы.
DenseArray — это абстрактный подтип AbstractArray, предназначенный для включения всех массивов, где элементы хранятся непрерывно в порядке «строка-столбец» (см. дополнительные замечания в разделе «Рекомендации по производительности»). Тип Array является конкретным экземпляром DenseArray; Vector и Matrix являются псевдонимами для 1-мерного и 2-мерного случаев. Очень мало операций реализованы специально для Array сверх тех, которые необходимы для всех AbstractArray; большая часть библиотеки массивов реализована обобщенным образом, что позволяет всем настраиваемым массивам вести себя аналогично.
SubArray — это специализация AbstractArray, которая выполняет индексацию, используя совместное использование памяти с исходным массивом, а не копируя его. SubArray создается с помощью функции view, которая вызывается так же, как getindex (с массивом и рядом аргументов индекса). Результат view выглядит так же, как результат getindex, за исключением того, что данные остаются на месте. view сохраняет входные векторы индекса в объекте SubArray, который позднее может использоваться для косвенной индексации исходного массива. Разместив макрос @views перед выражением или блоком кода, любой срез array[...] в этом выражении будет преобразован для создания представления SubArray вместо этого.
BitArrayы — это экономичные по памяти «упакованные» булевы массивы, которые хранят по одному биту на булево значение. Их можно использовать аналогично массивам Array{Bool} (которые хранят по одному байту на булево значение), и их можно преобразовать в последние и из них с помощью Array(bitarray) и BitArray(array) соответственно.
Массив «строко-столбцовый», если он хранится в памяти с хорошо определенными интервалами (шагами) между его элементами. Строко-столбцовый массив с поддерживаемым типом элементов может передаваться во внешнюю (не-Julia) библиотеку, например BLAS или LAPACK, просто передав его pointer и шаг для каждого измерения. stride(A, d) — это расстояние между элементами вдоль измерения d. Например, встроенный Array возвращаемый rand(5,7,2) имеет элементы, расположенные непрерывно в порядке «строка-столбец». Это означает, что шаг первого измерения — интервал между элементами в том же столбце — равен 1:
julia> A = rand(5,7,2); julia> stride(A,1) 1
Шаг второго измерения — интервал между элементами в той же строке, пропуская столько элементов, сколько их в одном столбце (5). Аналогично, переход между двумя «страницами» (в третьем измерении) требует пропуска 5*7 == 35 элементов. strides этого массива — кортеж из этих трех чисел вместе:
julia> strides(A) (1, 5, 35)
В этом конкретном случае количество пропускаемых элементов в памяти соответствует количеству пропускаемых линейных индексов. Это справедливо только для непрерывных массивов, таких как Array (и других подтипов DenseArray), и неверно в общем случае. Представления с индексами диапазона — хороший пример непрерывных строко-столбцовых массивов; рассмотрите V = @view A[1:3:4, 2:2:6, 2:-1:1]. Это представление V ссылается на ту же память, что и A но пропускает и переупорядочивает некоторые из его элементов. Шаг первого измерения V равен 3, потому что мы выбираем только каждую третью строку из исходного массива:
julia> V = @view A[1:3:4, 2:2:6, 2:-1:1]; julia> stride(V, 1) 3
Это представление аналогично выбирает каждый другой столбец из нашего исходного A — и, следовательно, ему нужно пропустить эквивалент двух столбцов из пяти элементов при перемещении между индексами во втором измерении:
julia> stride(V, 2) 10
Третье измерение интересно, потому что его порядок обратный! Таким образом, чтобы перейти от первой «страницы» ко второй, он должен перемещаться обратно в памяти, и поэтому его шаг в этом измерении отрицателен!
julia> stride(V, 3) -35
Это означает, что pointer для V фактически указывает в середину блока памяти A, и он ссылается на элементы как вперед, так и назад в памяти. Подробнее о определении собственных строенных массивов см. руководство по интерфейсу строенных массивов. StridedVector и StridedMatrix — удобные псевдонимы для многих встроенных типов массивов, которые считаются строенными массивами, позволяя им отправлять запросы на выбор специализированных реализаций, которые вызывают высоконастроенные и оптимизированные функции BLAS и LAPACK, используя только указатель и шаги.
Стоит подчеркнуть, что шаги представляют собой смещения в памяти, а не индексацию. Если вы хотите преобразовать линейную (индексацию с одним индексом) и декартову (индексацию с несколькими индексами) индексацию, см. LinearIndices и CartesianIndices.
- 1iid, независимые и одинаково распределённые.
© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.5.3/manual/arrays/