Документация Engee

Одно- и многомерные массивы

В 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...)

Массив Array из одних нулей

ones(T, dims...)

Массив Array из одних единиц

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...)

массив Array со случайными независимо, одинаково (iid [1]) и равномерно распределенными значениями. Для типов с плавающей запятой T значения находятся в полуоткрытом интервале .

randn(T, dims...)

Массив Array со случайными независимо, одинаково и стандартно нормально распределенными значениями

Matrix{T}(I, m, n)

Матрица тожественности m на n. Для I требуется using LinearAlgebra.

range(start, stop, n)

Диапазон из n линейно разнесенных элементов от start до stop

fill!(A, x)

Заполняет массив A значением x

fill(x, dims...)

Массив Array, заполненный значением 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] # Массив значений типа `Int`
3-element Vector{Int64}:
 1
 2
 3

julia> promote(1, 2.3, 4//5) # Это сочетание значений типа Int, Float64 и Rational продвигается до Float64
(1.0, 2.3, 0.8)

julia> [1, 2.3, 4//5] # Это и будет тип элементов данного массива
3-element Vector{Float64}:
 1.0
 2.3
 0.8

julia> Float32[1, 2.3, 4//5] # Указание типа элемента вручную
3-element Vector{Float32}:
 1.0
 2.3
 0.8

julia> []
Any[]

Конкатенация

Если аргументы в квадратных скобках разделяются одинарными точками с запятой (;) или символами разрыва строки, а не запятыми, то их содержимое объединяется по вертикали (сами аргументы не используются как элементы).

julia> [1:2, 4:5] # Имеет запятую, поэтому конкатенация не производится. Элементами являются сами диапазоны
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] # Числа также могут объединяться по горизонтали
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

shorthand for `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] # только скалярные индексы
3

julia> A[[1, 2], [1], [1, 2], [1]] # все индексы являются векторами
2×1×2×1 Array{Int64, 4}:
[:, :, 1, 1] =
 1
 2

[:, :, 2, 1] =
 5
 6

julia> A[[1, 2], [1], [1, 2], 1] # индексы разных типов
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:

  1. К скалярным индексам по умолчанию относятся: * целочисленные значения (не логические); * объекты CartesianIndex{N}, которые действуют как кортежи из N целочисленных значений, охватывающие несколько измерений (подробности см. ниже).

  2. К массивам скалярных индексов относятся:

    • векторы и многомерные массивы целых чисел;

    • пустые массивы, например [], для выбора нуля элементов, например A[[]] (не путать с A[]);

    • диапазоны, например a:c или a:b:c, для выбора непрерывных отрезков или отрезков с заданным шагом от a до c (включительно);

    • любой пользовательский массив скалярных индексов, являющийся подтипом AbstractArray;

    • массивы CartesianIndex{N} (подробности см. ниже).

  3. К объектам, которые представляют массив скалярных индексов и могут быть преобразованы в него функцией 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).

!!! warnings При итерации по всем индексам массива лучше выполнять итерацию по 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] # Опускает четвертое измерение (длиной 1)
19

julia> A[1, 3] # Пытается опустить измерения 3 и 4 (длиной 2 и 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] # Линейное индексирование
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
    # Какие-либо действия с элементом a
end

for i in eachindex(A)
    # Какие-либо действия с элементом i и (или) 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().

Операторы и функции для массивов и векторов

Для массивов поддерживаются следующие операторы:

  1. Унарные арифметические: -, +

  2. Бинарные арифметические: -, +, *, /, \, ^

  3. Сравнения: ==, !=, (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 представляют собой псевдонимы для одномерного и двумерного случаев. Операции с 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 представляют собой псевдонимы для одномерного и двумерного случаев. Помимо операций, требуемых для всех подтипов AbstractArray, для Array реализовано совсем немного специальных операций; библиотека для работы с массивами в основном является универсальной, так что все пользовательские массивы функционируют сходным образом.

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.


1.  iid — с независимым и одинаковым распределением.