Для разработчиков
Написание кода, поддерживающего OffsetArrays, обычно не вызывает трудностей. В большинстве случаев можно использовать следующие рекомендации:
-
заменяйте многие использования
size
наaxes
; -
заменяйте
1:length(A)
наeachindex(A)
, или, если вам нужен целочисленный индекс, наLinearIndices(A)
; -
заменяйте явные выделения типа
Array{Int}(undef, size(B))
наsimilar(Array{Int}, axes(B))
.
Более подробную информацию можно найти в документации разработчика Julia. Самые острые проблемы, как правило, связаны с осями. Более подробная информация, касающаяся OffsetArrays.jl, приведена ниже.
Основные принципы
Как работает OffsetArrays? Фундаментальный принцип очень прост: OffsetArray
является просто оболочкой «родительского» массива с указанием смещения индекса:
julia> oa = OffsetArray([1 2; 3 4], 0:1, 5:6)
2×2 OffsetArray(::Matrix{Int64}, 0:1, 5:6) with eltype Int64 with indices 0:1×5:6:
1 2
3 4
julia> parent(oa)
2×2 Matrix{Int64}:
1 2
3 4
julia> oa.offsets
(-1, 4)
Итак, parent(oa)
— это исходный массив, из которого мы его построили, а oa.offsets
— кортеж, каждый элемент которого кодирует индексное смещение, которое нужно применить по соответствующей оси. Когда вы индексируете oa[i,j]
, он «переводит» индексы i,j
обратно в индексы родительского массива, а затем возвращает значение в родительском массиве.
Оси OffsetArrays
Внутреннее вычисление смещения осуществляется с помощью типа IdOffsetRange
:
julia> ax = axes(oa, 2)
OffsetArrays.IdOffsetRange(values=5:6, indices=5:6)
По аналогии с Base.IdentityUnitRange
, ax[x] == x
всегда выполняется.
julia> ax[5]
5
julia> ax[1]
ERROR: BoundsError: attempt to access 2-element OffsetArrays.IdOffsetRange{Int64, Base.OneTo{Int64}} with indices 5:6 at index [1]
[...]
Согласно этому свойству они становятся собственными осями:
julia> axes(ax)
(OffsetArrays.IdOffsetRange(values=5:6, indices=5:6),)
julia> axes(ax[ax])
(OffsetArrays.IdOffsetRange(values=5:6, indices=5:6),)
Этот пример индексирования является идемпотентным. Это полезная характеристика для обеспечения «фундаментальной аксиомы» обобщенного индексирования, что a[ax][i] == a[ax[i]]
:
julia> oa2 = OffsetArray([5, 10, 15, 20], 0:3)
4-element OffsetArray(::Vector{Int64}, 0:3) with eltype Int64 with indices 0:3:
5
10
15
20
julia> ax2 = axes(oa2, 1)
OffsetArrays.IdOffsetRange(values=0:3, indices=0:3)
julia> oa2[2]
15
julia> oa2[ax2][2]
15
julia> oa2[ax2[2]]
15
IdOffsetRange
применяют смещение как к значениям, так и к индексам диапазона, а в остальном сохраняют родительский диапазон.
Существуют обстоятельства, при которых построение определенного типа |
В будущем этот пакет будет различать построение и преобразование:
- построение (или *принуждение*) всегда будет успешным, даже если для этого придется изменить оси результата (примеры: `RangeType(rng)`, `typeof(rng1)(rng2)`)
-
преобразование будет успешным, только если оно может сохранить и значения, и оси (примеры:
convert(RangeType, rng)
,oftype(rng1, rng2)
)Хотя сейчас их действие эквивалентно (преобразование в настоящее время осуществляет принуждение), разработчикам рекомендуется «обезопасить» свой код на будущее, выбрав поведение, подходящее для каждого случая использования.
Заключение в оболочку других типов массивов смещений
OffsetArray
может заключить в оболочку любой подтип AbstractArray
, включая те, которые не используют индексирование на основе 1
. Однако такие массивы должны удовлетворять фундаментальной аксиоме идемпотентного индексирования, чтобы все компоненты работали правильно. Другими словами, ось массива смещений должна иметь те же значения, что и ее собственная ось. Это свойство встроено в OffsetArray
, если родительский объект использует индексирование на основе 1, но пользователь должен убедиться в его корректности в случае, если необходимо заключить в оболочку тип, использующий индексы со смещением.
Продемонстрируем это на примере создания пользовательского типа диапазона, основанного на 0, который мы инкапсулируем в OffsetArray
:
julia> struct ZeroBasedRange{T,A<:AbstractRange{T}} <: AbstractRange{T}
a :: A
function ZeroBasedRange(a::AbstractRange{T}) where {T}
@assert !Base.has_offset_axes(a)
new{T, typeof(a)}(a)
end
end;
julia> Base.parent(A::ZeroBasedRange) = A.a;
julia> Base.first(A::ZeroBasedRange) = first(A.a);
julia> Base.length(A::ZeroBasedRange) = length(A.a);
julia> Base.last(A::ZeroBasedRange) = last(A.a);
julia> Base.size(A::ZeroBasedRange) = size(A.a);
julia> Base.axes(A::ZeroBasedRange) = map(x -> 0:x-1, size(A.a));
julia> Base.getindex(A::ZeroBasedRange, i::Int) = A.a[i + 1];
julia> Base.step(A::ZeroBasedRange) = step(A.a);
julia> function Base.show(io::IO, A::ZeroBasedRange)
show(io, A.a)
print(io, " with indices $(axes(A,1))")
end;
Это определение ZeroBasedRange
, похоже, имеет правильные индексы, например:
julia> z = ZeroBasedRange(1:4)
1:4 with indices 0:3
julia> z[0]
1
julia> z[3]
4
Однако здесь не используется идемпотентное индексирование, так как ось ZeroBasedRange
не является собственной осью.
julia> axes(z, 1)
0:3
julia> axes(axes(z, 1), 1)
Base.OneTo(4)
Это приведет к осложнениям в некоторых функциях, например LinearIndices
, которые склонны неявно предполагать идемпотентное индексирование. В этом случае LinearIndices
для z
не совпадет с осью.
julia> LinearIndices(z)
4-element LinearIndices{1, Tuple{UnitRange{Int64}}}:
1
2
3
4
Инкапсулирование такого типа в OffsetArray
может привести к неожиданным ошибкам.
julia> zo = OffsetArray(z, 1);
julia> axes(zo, 1)
OffsetArrays.IdOffsetRange(values=1:4, indices=2:5)
julia> Array(zo)
ERROR: BoundsError: attempt to access 4-element UnitRange{Int64} at index [5]
[...]
Преобразование Array
ошибочно, несмотря на то, что zo
имеет индексы, основанные на 1. Функция axes(zo, 1)
указывает на базовую проблему — значения и индексы оси различны. Можно проверить, что ось zo
не является собственной осью:
julia> axes(zo, 1)
OffsetArrays.IdOffsetRange(values=1:4, indices=2:5)
julia> axes(axes(zo, 1), 1)
OffsetArrays.IdOffsetRange(values=2:5, indices=2:5)
В этом случае ошибку можно исправить, определив, что функция axes
для ZeroBasedRange
является идемпотентной, например, используя оболочку OffsetArrays.IdentityUnitRange
:
julia> Base.axes(A::ZeroBasedRange) = map(x -> OffsetArrays.IdentityUnitRange(0:x-1), size(A.a))
julia> axes(zo, 1)
OffsetArrays.IdOffsetRange(values=1:4, indices=1:4)
При таком новом определении значения и индексы оси идентичны, что делает индексирование идемпотентным. Теперь преобразование в Array
работает, как и ожидалось:
julia> Array(zo)
4-element Vector{Int64}:
1
2
3
4
Предупреждения
Поскольку IdOffsetRange
ведет себя совершенно иначе, чем обычный тип UnitRange
, есть несколько случаев, о которых вам следует знать, особенно при работе с многомерными массивами.
Одним из них является getindex
:
julia> Ao = zeros(-3:3, -3:3); Ao[:] .= 1:49;
julia> Ao[-3:0, :] |> axes # первое измерение не сохраняет смещения
(OffsetArrays.IdOffsetRange(values=1:4, indices=1:4), OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3))
julia> Ao[-3:0, -3:3] |> axes # ни одно из измерений не сохраняет смещения
(Base.OneTo(4), Base.OneTo(7))
julia> Ao[axes(Ao)...] |> axes # смещения сохраняются
(OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3), OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3))
julia> Ao[:] |> axes # Это линейное индексирование
(Base.OneTo(49),)
Обратите внимание, что при передаче UnitRange
смещения в соответствующем измерении не сохранятся. Сначала это может показаться странным, но, поскольку здесь соблюдается правило a[ax][i] == a[ax[i]]
, ошибки нет.
julia> I = -3:0; # UnitRange всегда начинается с индекса 1
julia> Ao[I, 0][1] == Ao[I[1], 0]
true
julia> ax = axes(Ao, 1) # ax начинается с индекса -3
OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3)
julia> Ao[ax, 0][1] == Ao[ax[1], 0]
true
Использование пользовательских типов осей
Хотя в качестве индексов для построения OffsetArray
могут использоваться самые разные AbstractUnitRange
, предоставляемые Base
, иногда бывает удобно определить собственные типы. Конструктор OffsetArray
принимает любой тип, который может быть преобразован в AbstractUnitRange
. Этот процесс выполняется в два этапа. Предположим, что вызываемым конструктором является OffsetArray(A, indstup)
, где indstup
— это кортеж (Tuple
) индексов.
-
На первом этапе конструктор вызывает
to_indices(A, axes(A), indstup)
, чтобы снизить уровеньindstup
до кортежа (Tuple
) изAbstractUnitRange
. На этом шаге, помимо всего прочего,Colon
преобразуются в диапазоны осей. Пользовательские типы могут расширятьBase.to_indices(A, axes(A), indstup)
с желаемым преобразованиемindstup
вTuple{Vararg{AbstractUnitRange{Int}}}
, если это осуществимо. -
На втором шаге результат, полученный на предыдущем шаге, снова обрабатывается для преобразования в кортеж (
Tuple
) изAbstractUnitRange
, чтобы выполнить те моменты, которые не удалось реализовать на первом шаге. На этом этапе может быть задан дополнительный параметр настройки: тип может быть преобразован либо в одинAbstractUnitRange{Int}
, либо в кортеж (Tuple
) из них. Тип может указывать, какой из этих двух вариантов поведения желателен, расширяяOffsetArrays.AxisConversionStyle
. Примером типа, который обрабатывается на этом этапе, являетсяCartesianIndices
, который преобразуется в кортеж (Tuple
) изAbstractUnitRange
.
Например, вот несколько пользовательских типов, которые упрощают индексирование на основе нуля:
julia> struct ZeroBasedIndexing end
julia> Base.to_indices(A, inds, ::Tuple{ZeroBasedIndexing}) = map(x -> 0:length(x)-1, inds)
julia> a = zeros(3, 3);
julia> oa = OffsetArray(a, ZeroBasedIndexing());
julia> axes(oa)
(OffsetArrays.IdOffsetRange(values=0:2, indices=0:2), OffsetArrays.IdOffsetRange(values=0:2, indices=0:2))
В этом примере нам пришлось определить действие to_indices
, так как у типа ZeroBasedIndexing
не было привычной иерархии. Все становится еще проще, если мы выделяем подтип AbstractUnitRange
. В этом случае нам нужно определить first
и length
для пользовательского диапазона, чтобы использовать его в качестве оси:
julia> struct ZeroTo <: AbstractUnitRange{Int}
n :: Int
ZeroTo(n) = new(n < 0 ? -1 : n)
end
julia> Base.first(::ZeroTo) = 0
julia> Base.length(r::ZeroTo) = r.n + 1
julia> oa = OffsetArray(zeros(2,2), ZeroTo(1), ZeroTo(1));
julia> axes(oa)
(OffsetArrays.IdOffsetRange(values=0:1, indices=0:1), OffsetArrays.IdOffsetRange(values=0:1, indices=0:1))
Обратите внимание, что индексирование на основе нуля также можно получить с помощью предопределенного типа OffsetArrays.Origin
.