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 مؤقتًا لنوع المصفوفة الجديد الخاص بك، حيث إن الكود الذي يقوم بفرض افتراضات غير صحيحة يستخدم هذه الوظائف بشكل متكرر.