Arrays with custom indices
Обычно массивы Julia индексируются с 1, в то время как в некоторых других языках нумерация начинается с 0, а в других (например, Fortran) вы можете указывать произвольные начальные индексы. Хотя есть много преимуществ в выборе стандарта (т.е. 1 для Julia), есть некоторые алгоритмы, которые значительно упрощаются, если вы можете индексировать вне диапазона 1:size(A,d)
(и не только 0:size(A,d)-1
, тоже). Чтобы облегчить такие вычисления, Julia поддерживает массивы с произвольными индексами.
Цель этой страницы - ответить на вопрос: "что мне нужно сделать, чтобы поддерживать такие массивы в своем коде?" Сначала давайте рассмотрим самый простой случай: если вы знаете, что ваш код никогда не будет нуждаться в обработке массивов с нетрадиционным индексированием, надеюсь, ответ будет "ничего". Старый код, работающий с обычными массивами, должен функционировать практически без изменений, если он использовал экспортируемые интерфейсы Julia. Если вам удобнее просто заставить ваших пользователей предоставлять традиционные массивы, где индексация начинается с одного, вы можете добавить
Base.require_one_based_indexing(arrays...)
где arrays...
— это список объектов массива, которые вы хотите проверить на наличие чего-либо, что нарушает индексацию с 1.
Generalizing existing code
В качестве обзора, шаги следующие:
- Sure! Please provide the Markdown content or text that you would like me to translate into Russian.
- замените
1:length(A)
наeachindex(A)
, или в некоторых случаях наLinearIndices(A)
- замените явные выделения, такие как
Array{Int}(undef, size(B))
, наsimilar(Array{Int}, axes(B))
Эти вопросы описаны более подробно ниже.
Things to watch out for
Поскольку неконвенциональная индексация нарушает предположения многих людей о том, что все массивы начинаются с индекса 1, всегда существует вероятность того, что использование таких массивов вызовет ошибки. Наиболее раздражающими ошибками будут неправильные результаты или сегментационные ошибки (полные сбои Julia). Например, рассмотрим следующую функцию:
function mycopy!(dest::AbstractVector, src::AbstractVector)
length(dest) == length(src) || throw(DimensionMismatch("vectors must match"))
# OK, now we're safe to use @inbounds, right? (not anymore!)
for i = 1:length(src)
@inbounds dest[i] = src[i]
end
dest
end
Этот код неявно предполагает, что векторы индексируются с 1; если dest
начинается с другого индекса, чем src
, существует вероятность, что этот код вызовет ошибку сегментации. (Если вы получите ошибки сегментации, чтобы помочь определить причину, попробуйте запустить julia с опцией --check-bounds=yes
.)
Using axes
for bounds checks and loop iteration
axes(A)
(напоминает size(A)
) возвращает кортеж объектов AbstractUnitRange{<:Integer}
, указывающий диапазон допустимых индексов вдоль каждого измерения A
. Когда A
имеет нестандартную индексацию, диапазоны могут не начинаться с 1. Если вам нужен только диапазон для конкретного измерения d
, существует axes(A, d)
.
Base реализует пользовательский тип диапазона, OneTo
, где OneTo(n)
означает то же самое, что и 1:n
, но в форме, которая гарантирует (через систему типов), что нижний индекс равен 1. Для любого нового типа AbstractArray
это значение по умолчанию, возвращаемое axes
, и оно указывает на то, что этот тип массива использует "традиционную" индексацию, начинающуюся с 1.
Для проверки границ обратите внимание, что существуют специальные функции checkbounds
и checkindex
, которые иногда могут упростить такие тесты.
Linear indexing (LinearIndices
)
Некоторые алгоритмы удобнее (или эффективнее) записывать в терминах одного линейного индекса, A[i]
, даже если A
является многомерным. Независимо от родных индексов массива, линейные индексы всегда варьируются от 1:length(A)
. Однако это вызывает неоднозначность для одномерных массивов (также известный как AbstractVector
): означает ли v[i]
линейную индексацию или декартову индексацию с родными индексами массива?
По этой причине вашим лучшим вариантом может быть итерация по массиву с помощью eachindex(A)
, или, если вам нужны индексы в виде последовательных целых чисел, получить диапазон индексов, вызвав LinearIndices(A)
. Это вернет axes(A, 1)
, если A является AbstractVector, и эквивалент 1:length(A)
в противном случае.
Согласно этому определению, 1-мерные массивы всегда используют декартово индексирование с родными индексами массива. Чтобы помочь в этом, стоит отметить, что функции преобразования индексов вызовут ошибку, если форма указывает на 1-мерный массив с нетрадиционным индексированием (т.е. является Tuple{UnitRange}
, а не кортежем OneTo
). Для массивов с традиционным индексированием эти функции продолжают работать так же, как и всегда.
Используя axes
и LinearIndices
, вот один из способов, как вы могли бы переписать mycopy!
:
function mycopy!(dest::AbstractVector, src::AbstractVector)
axes(dest) == axes(src) || throw(DimensionMismatch("vectors must match"))
for i in LinearIndices(src)
@inbounds dest[i] = src[i]
end
dest
end
Allocating storage using generalizations of similar
Хранение часто выделяется с помощью Array{Int}(undef, dims)
или similar(A, args...)
. Когда результат должен соответствовать индексам другого массива, этого может быть недостаточно. Общей заменой для таких шаблонов является использование similar(storagetype, shape)
. storagetype
указывает на тип "обычного" поведения, которое вы хотите, например, Array{Int}
или BitArray
или даже dims->zeros(Float32, dims)
(что выделит массив, заполненный нулями). shape
— это кортеж значений Integer
или AbstractUnitRange
, указывающий индексы, которые вы хотите, чтобы результат использовал. Обратите внимание, что удобный способ получения массива, заполненного нулями, который соответствует индексам A, — это просто zeros(A)
.
Давайте рассмотрим несколько явных примеров. Во-первых, если A
имеет обычные индексы, то similar(Array{Int}, axes(A))
в конечном итоге вызовет Array{Int}(undef, size(A))
и, таким образом, вернет массив. Если A
является типом AbstractArray
с необычной индексацией, то similar(Array{Int}, axes(A))
должен вернуть что-то, что "ведет себя как" Array{Int}
, но с формой (включая индексы), которая соответствует A
. (Самая очевидная реализация - выделить Array{Int}(undef, size(A))
, а затем "обернуть" его в тип, который сдвигает индексы.)
Обратите внимание, что similar(Array{Int}, (axes(A, 2),))
выделит AbstractVector{Int}
(т.е. одномерный массив), который соответствует индексам столбцов A
.
Writing custom array types with non-1 indexing
Большинство методов, которые вам нужно будет определить, являются стандартными для любого типа AbstractArray
, см. Abstract Arrays. Эта страница сосредоточена на шагах, необходимых для определения нестандартной индексации.
Custom AbstractUnitRange
types
Если вы пишете массив с индексами, начинающимися не с 1, вам нужно будет специализированно определить axes
, чтобы он возвращал UnitRange
, или (возможно, лучше) пользовательский AbstractUnitRange
. Преимущество пользовательского типа заключается в том, что он "сигнализирует" тип выделения для таких функций, как similar
. Если мы создаем тип массива, для которого индексация начинается с 0, нам, вероятно, следует начать с создания нового AbstractUnitRange
, ZeroRange
, где ZeroRange(n)
эквивалентно 0:n-1
.
В общем, вам, вероятно, не следует экспортировать ZeroRange
из вашего пакета: могут быть и другие пакеты, которые реализуют свои собственные ZeroRange
, и наличие нескольких различных типов ZeroRange
является (возможно, противоречиво) преимуществом: ModuleA.ZeroRange
указывает на то, что similar
должен создавать тип ModuleA.ZeroArray
, в то время как ModuleB.ZeroRange
указывает на тип ModuleB.ZeroArray
. Этот дизайн позволяет мирному сосуществованию множества различных пользовательских типов массивов.
Обратите внимание, что пакет Julia CustomUnitRanges.jl иногда можно использовать, чтобы избежать необходимости писать свой собственный тип ZeroRange
.
Specializing axes
Как только у вас будет ваш тип AbstractUnitRange
, затем специализированный axes
:
Base.axes(A::ZeroArray) = map(n->ZeroRange(n), A.size)
где здесь мы предполагаем, что ZeroArray
имеет поле под названием size
(существуют и другие способы реализации этого).
В некоторых случаях запасное определение для axes(A, d)
:
axes(A::AbstractArray{T,N}, d) where {T,N} = d <= N ? axes(A)[d] : OneTo(1)
может не соответствовать вашим ожиданиям: вам может потребоваться специализированная функция, чтобы вернуть что-то другое, кроме OneTo(1)
, когда d > ndims(A)
. Точно так же в Base
есть специальная функция axes1
, которая эквивалентна axes(A, 1)
, но которая избегает проверки (во время выполнения), является ли ndims(A) > 0
. (Это чисто оптимизация производительности.) Она определена как:
axes1(A::AbstractArray{T,0}) where {T} = OneTo(1)
axes1(A::AbstractArray) = axes(A)[1]
Если первый из этих случаев (нулевое измерение) вызывает проблемы для вашего пользовательского типа массива, обязательно специализированно его обработайте.
Specializing similar
Учитывая ваш пользовательский тип ZeroRange
, вы также должны добавить следующие две специализации для similar
:
function Base.similar(A::AbstractArray, T::Type, shape::Tuple{ZeroRange,Vararg{ZeroRange}})
# body
end
function Base.similar(f::Union{Function,DataType}, shape::Tuple{ZeroRange,Vararg{ZeroRange}})
# body
end
Оба этих варианта должны выделить ваш пользовательский тип массива.
Specializing reshape
По желанию, определите метод
Base.reshape(A::AbstractArray, shape::Tuple{ZeroRange,Vararg{ZeroRange}}) = ...
и вы можете reshape
массив так, чтобы результат имел пользовательские индексы.
For objects that mimic AbstractArray but are not subtypes
has_offset_axes
зависит от наличия axes
, определенных для объектов, на которых вы его вызываете. Если по какой-то причине у вас нет метода axes
, определенного для вашего объекта, рассмотрите возможность определения метода
Base.has_offset_axes(obj::MyNon1IndexedArraylikeObject) = true
Это позволит коду, который предполагает индексацию с единицы, обнаружить проблему и выдать полезную ошибку, а не возвращать неправильные результаты или вызывать ошибку сегментации в julia.
Catching errors
Если ваш новый тип массива вызывает ошибки в другом коде, одним из полезных шагов для отладки может быть закомментирование @boundscheck
в вашей реализации getindex
и setindex!
. Это обеспечит проверку границ для каждого доступа к элементам. Или перезапустите julia с --check-bounds=yes
.
В некоторых случаях может быть полезно временно отключить size
и length
для вашего нового типа массива, так как код, который делает неверные предположения, часто использует эти функции.