Arrays with custom indices
تقليديًا، يتم فهرسة مصفوفات جوليا بدءًا من 1، بينما تبدأ بعض اللغات الأخرى في الترقيم من 0، وتسمح لغات أخرى (مثل فورتران) بتحديد فهارس بدء عشوائية. بينما هناك الكثير من الفوائد في اختيار معيار (أي 1 لجوليا)، هناك بعض الخوارزميات التي تبسط بشكل كبير إذا كان بإمكانك الفهرسة خارج النطاق 1:size(A,d)
(وليس فقط 0:size(A,d)-1
، أيضًا). لتسهيل مثل هذه الحسابات، تدعم جوليا المصفوفات ذات الفهارس العشوائية.
الغرض من هذه الصفحة هو معالجة السؤال، "ماذا يجب أن أفعل لدعم مثل هذه المصفوفات في شيفرتي الخاصة؟" أولاً، دعنا نتناول أبسط حالة: إذا كنت تعلم أن شيفرتك لن تحتاج أبداً إلى التعامل مع مصفوفات ذات فهرسة غير تقليدية، نأمل أن تكون الإجابة "لا شيء." يجب أن تعمل الشيفرة القديمة، على المصفوفات التقليدية، بشكل أساسي دون تغيير طالما أنها كانت تستخدم الواجهات المصدرة من جوليا. إذا وجدت أنه من الأكثر ملاءمة أن تجبر مستخدميك على تقديم مصفوفات تقليدية حيث يبدأ الفهرس من واحد، يمكنك إضافة
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 Arabic.
- استبدل
1:length(A)
بـeachindex(A)
، أو في بعض الحالات بـLinearIndices(A)
- استبدل التخصيصات الصريحة مثل
Array{Int}(undef, size(B))
بـsimilar(Array{Int}, axes(B))
هذه موصوفة بمزيد من التفصيل أدناه.
Things to watch out for
لأن الفهرسة غير التقليدية تكسر افتراضات العديد من الأشخاص بأن جميع المصفوفات تبدأ الفهرسة من 1، هناك دائمًا فرصة أن استخدام مثل هذه المصفوفات سيؤدي إلى حدوث أخطاء. ستكون أكثر الأخطاء إحباطًا هي النتائج غير الصحيحة أو segfaults (الأعطال الكاملة لجوليا). على سبيل المثال، اعتبر الدالة التالية:
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
، فهناك احتمال أن يتسبب هذا الرمز في حدوث خطأ في الوصول إلى الذاكرة. (إذا واجهت أخطاء في الوصول إلى الذاكرة، للمساعدة في تحديد السبب حاول تشغيل جوليا مع الخيار --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 implements a custom range type, OneTo
, where OneTo(n)
means the same thing as 1:n
but in a form that guarantees (via the type system) that the lower index is 1. For any new AbstractArray
type, this is the default returned by axes
, and it indicates that this array type uses "conventional" 1-based indexing.
لإجراء فحص الحدود، لاحظ أن هناك دوال مخصصة 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)
بخلاف ذلك.
بموجب هذا التعريف، تستخدم المصفوفات ذات البعد الواحد دائمًا الفهرسة الكارتيزية باستخدام مؤشرات المصفوفة الأصلية. للمساعدة في فرض ذلك، من الجدير بالذكر أن دوال تحويل الفهارس ستقوم بإلقاء خطأ إذا كانت الشكل تشير إلى مصفوفة ذات بعد واحد مع فهرسة غير تقليدية (أي، هي 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
. يسمح هذا التصميم بالتعايش السلمي بين العديد من أنواع المصفوفات المخصصة المختلفة.
لاحظ أن حزمة جوليا 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}}) = ...
ويمكنك إعادة تشكيل
مصفوفة بحيث تكون النتيجة تحتوي على فهارس مخصصة.
For objects that mimic AbstractArray but are not subtypes
has_offset_axes
يعتمد على وجود axes
معرف للأشياء التي تستدعيها. إذا كان هناك سبب ما يجعلك لا تمتلك طريقة axes
معرفة لكائنك، فكر في تعريف طريقة
Base.has_offset_axes(obj::MyNon1IndexedArraylikeObject) = true
سيسمح هذا للكود الذي يفترض وجود فهرسة تبدأ من 1 بالكشف عن مشكلة وإلقاء خطأ مفيد، بدلاً من إرجاع نتائج غير صحيحة أو التسبب في تعطل جوليا.
Catching errors
إذا كان نوع المصفوفة الجديد لديك يسبب أخطاء في كود آخر، فإن خطوة تصحيح مفيدة يمكن أن تكون تعليق @boundscheck
في تنفيذ getindex
و setindex!
الخاص بك. سيضمن ذلك أن كل وصول إلى العناصر يتحقق من الحدود. أو، أعد تشغيل جوليا مع --check-bounds=yes
.
في بعض الحالات، قد يكون من المفيد أيضًا تعطيل size
و length
مؤقتًا لنوع المصفوفة الجديد الخاص بك، حيث إن الكود الذي يقوم بفرض افتراضات غير صحيحة يستخدم هذه الوظائف بشكل متكرر.