Arrays with custom indices
Conventionnellement, les tableaux de Julia sont indexés à partir de 1, tandis que certains autres langages commencent la numérotation à 0, et d'autres encore (par exemple, Fortran) vous permettent de spécifier des indices de départ arbitraires. Bien qu'il y ait beaucoup de mérite à choisir une norme (c'est-à-dire 1 pour Julia), il existe certains algorithmes qui se simplifient considérablement si vous pouvez indexer en dehors de la plage 1:size(A,d)
(et pas seulement 0:size(A,d)-1
, non plus). Pour faciliter de tels calculs, Julia prend en charge des tableaux avec des indices arbitraires.
L'objectif de cette page est de répondre à la question : "que dois-je faire pour prendre en charge de tels tableaux dans mon propre code ?" Tout d'abord, abordons le cas le plus simple : si vous savez que votre code n'aura jamais besoin de gérer des tableaux avec un indexage non conventionnel, espérons que la réponse est "rien." L'ancien code, sur des tableaux conventionnels, devrait fonctionner essentiellement sans modification tant qu'il utilisait les interfaces exportées de Julia. Si vous trouvez plus pratique de forcer vos utilisateurs à fournir des tableaux traditionnels où l'indexation commence à un, vous pouvez ajouter
Base.require_one_based_indexing(arrays...)
où arrays...
est une liste des objets de tableau que vous souhaitez vérifier pour tout ce qui viole l'indexation à base 1.
Generalizing existing code
En résumé, les étapes sont :
- Sure! Please provide the Markdown content or text you'd like me to translate.
- remplacer
1:length(A)
pareachindex(A)
, ou dans certains casLinearIndices(A)
- remplacer les allocations explicites comme
Array{Int}(undef, size(B))
parsimilar(Array{Int}, axes(B))
Ceci est décrit plus en détail ci-dessous.
Things to watch out for
Parce que l'indexation non conventionnelle brise les hypothèses de nombreuses personnes selon lesquelles tous les tableaux commencent l'indexation à 1, il y a toujours un risque que l'utilisation de tels tableaux déclenche des erreurs. Les bogues les plus frustrants seraient des résultats incorrects ou des segfaults (crashes totaux de Julia). Par exemple, considérons la fonction suivante :
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
Ce code suppose implicitement que les vecteurs sont indexés à partir de 1 ; si dest
commence à un index différent de src
, il y a une chance que ce code déclenche une erreur de segmentation. (Si vous obtenez des erreurs de segmentation, pour aider à localiser la cause, essayez d'exécuter julia avec l'option --check-bounds=yes
.)
Using axes
for bounds checks and loop iteration
axes(A)
(évoquant size(A)
) renvoie un tuple d'objets AbstractUnitRange{<:Integer}
, spécifiant la plage des indices valides le long de chaque dimension de A
. Lorsque A
a un indexage non conventionnel, les plages peuvent ne pas commencer à 1. Si vous souhaitez simplement la plage pour une dimension particulière d
, il existe axes(A, d)
.
Base implémente un type de plage personnalisé, OneTo
, où OneTo(n)
signifie la même chose que 1:n
mais dans une forme qui garantit (via le système de types) que l'index inférieur est 1. Pour tout nouveau type AbstractArray
, c'est celui qui est retourné par défaut par axes
, et cela indique que ce type de tableau utilise un indexage "conventionnel" basé sur 1.
Pour la vérification des limites, notez qu'il existe des fonctions dédiées checkbounds
et checkindex
qui peuvent parfois simplifier de tels tests.
Linear indexing (LinearIndices
)
Certains algorithmes sont le plus souvent écrits de manière pratique (ou efficace) en termes d'un seul index linéaire, A[i]
, même si A
est multidimensionnel. Quelle que soit l'origine des indices du tableau, les indices linéaires vont toujours de 1:length(A)
. Cependant, cela soulève une ambiguïté pour les tableaux unidimensionnels (a.k.a., AbstractVector
) : est-ce que v[i]
signifie un index linéaire, ou un index cartésien avec les indices natifs du tableau ?
Pour cette raison, votre meilleure option peut être de parcourir le tableau avec eachindex(A)
, ou, si vous avez besoin que les indices soient des entiers séquentiels, d'obtenir la plage d'indices en appelant LinearIndices(A)
. Cela renverra axes(A, 1)
si A est un AbstractVector, et l'équivalent de 1:length(A)
sinon.
Par cette définition, les tableaux unidimensionnels utilisent toujours l'indexation cartésienne avec les indices natifs du tableau. Pour aider à faire respecter cela, il convient de noter que les fonctions de conversion d'index lanceront une erreur si la forme indique un tableau unidimensionnel avec une indexation non conventionnelle (c'est-à-dire, est un Tuple{UnitRange}
plutôt qu'un tuple de OneTo
). Pour les tableaux avec une indexation conventionnelle, ces fonctions continuent de fonctionner comme toujours.
En utilisant axes
et LinearIndices
, voici une façon de réécrire 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
Le stockage est souvent alloué avec Array{Int}(undef, dims)
ou similar(A, args...)
. Lorsque le résultat doit correspondre aux indices d'un autre tableau, cela peut ne pas toujours suffire. Le remplacement générique pour de tels motifs est d'utiliser similar(storagetype, shape)
. storagetype
indique le type de comportement "conventionnel" sous-jacent que vous souhaitez, par exemple, Array{Int}
ou BitArray
ou même dims->zeros(Float32, dims)
(qui allouerait un tableau rempli de zéros). shape
est un tuple de Integer
ou de valeurs AbstractUnitRange
, spécifiant les indices que vous souhaitez que le résultat utilise. Notez qu'un moyen pratique de produire un tableau rempli de zéros qui correspond aux indices de A est simplement zeros(A)
.
Voyons quelques exemples explicites. Tout d'abord, si A
a des indices conventionnels, alors similar(Array{Int}, axes(A))
finirait par appeler Array{Int}(undef, size(A))
, et retournerait donc un tableau. Si A
est un type AbstractArray
avec un indexage non conventionnel, alors similar(Array{Int}, axes(A))
devrait retourner quelque chose qui "se comporte comme" un Array{Int}
mais avec une forme (y compris les indices) qui correspond à A
. (L'implémentation la plus évidente consiste à allouer un Array{Int}(undef, size(A))
et ensuite à le "wrapper" dans un type qui déplace les indices.)
Notez également que similar(Array{Int}, (axes(A, 2),))
allouerait un AbstractVector{Int}
(c'est-à-dire, un tableau 1-dimensionnel) qui correspond aux indices des colonnes de A
.
Writing custom array types with non-1 indexing
La plupart des méthodes que vous devrez définir sont standard pour tout type AbstractArray
, voir Abstract Arrays. Cette page se concentre sur les étapes nécessaires pour définir un indexage non conventionnel.
Custom AbstractUnitRange
types
Si vous écrivez un type de tableau non indexé à partir de 1, vous voudrez spécialiser axes
afin qu'il renvoie un UnitRange
, ou (peut-être mieux) un AbstractUnitRange
personnalisé. L'avantage d'un type personnalisé est qu'il "signale" le type d'allocation pour des fonctions comme similar
. Si nous écrivons un type de tableau pour lequel l'indexation commencera à 0, nous voulons probablement commencer par créer un nouvel AbstractUnitRange
, ZeroRange
, où ZeroRange(n)
est équivalent à 0:n-1
.
En général, vous ne devriez probablement pas exporter ZeroRange
de votre package : il peut y avoir d'autres packages qui implémentent leur propre ZeroRange
, et avoir plusieurs types distincts de ZeroRange
est (peut-être contre-intuitivement) un avantage : ModuleA.ZeroRange
indique que similar
devrait créer un type ModuleA.ZeroArray
, tandis que ModuleB.ZeroRange
indique un type ModuleB.ZeroArray
. Ce design permet une coexistence pacifique entre de nombreux types de tableaux personnalisés différents.
Notez que le paquet Julia CustomUnitRanges.jl peut parfois être utilisé pour éviter d'avoir à écrire votre propre type ZeroRange
.
Specializing axes
Une fois que vous avez votre type AbstractUnitRange
, spécialisez ensuite axes
:
Base.axes(A::ZeroArray) = map(n->ZeroRange(n), A.size)
où ici nous imaginons que ZeroArray
a un champ appelé size
(il y aurait d'autres façons de l'implémenter).
Dans certains cas, la définition de secours pour axes(A, d)
:
axes(A::AbstractArray{T,N}, d) where {T,N} = d <= N ? axes(A)[d] : OneTo(1)
peut ne pas être ce que vous voulez : vous devrez peut-être le spécialiser pour renvoyer autre chose que OneTo(1)
lorsque d > ndims(A)
. De même, dans Base
, il existe une fonction dédiée axes1
qui est équivalente à axes(A, 1)
mais qui évite de vérifier (au moment de l'exécution) si ndims(A) > 0
. (C'est purement une optimisation de performance.) Elle est définie comme :
axes1(A::AbstractArray{T,0}) where {T} = OneTo(1)
axes1(A::AbstractArray) = axes(A)[1]
Si le premier de ces cas (le cas zéro-dimensionnel) pose problème pour votre type de tableau personnalisé, assurez-vous de le spécialiser de manière appropriée.
Specializing similar
Étant donné votre type personnalisé ZeroRange
, vous devez également ajouter les deux spécialisations suivantes pour 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
Les deux devraient allouer votre type de tableau personnalisé.
Specializing reshape
Optionnellement, définissez une méthode
Base.reshape(A::AbstractArray, shape::Tuple{ZeroRange,Vararg{ZeroRange}}) = ...
et vous pouvez reshape
un tableau de sorte que le résultat ait des indices personnalisés.
For objects that mimic AbstractArray but are not subtypes
has_offset_axes
dépend de la définition de axes
pour les objets sur lesquels vous l'appelez. S'il y a une raison pour laquelle vous n'avez pas de méthode axes
définie pour votre objet, envisagez de définir une méthode.
Base.has_offset_axes(obj::MyNon1IndexedArraylikeObject) = true
Cela permettra au code qui suppose un indexage basé sur 1 de détecter un problème et de lancer une erreur utile, plutôt que de renvoyer des résultats incorrects ou de provoquer un segfault en julia.
Catching errors
Si votre nouveau type de tableau déclenche des erreurs dans d'autres codes, une étape de débogage utile peut consister à commenter @boundscheck
dans votre implémentation de getindex
et setindex!
. Cela garantira que chaque accès aux éléments vérifie les limites. Ou, redémarrez julia avec --check-bounds=yes
.
Dans certains cas, il peut également être utile de désactiver temporairement size
et length
pour votre nouveau type de tableau, car le code qui fait des hypothèses incorrectes utilise souvent ces fonctions.