6.5. الشكل والخطوات (strides)

إنّ البيانات داخل ndarray هي كتلة واحدة محزومة من الأعداد. والواصف الذي يتقدّم تلك الكتلة هو الذي يقرّر كيف تُقرأ تلك الكتلة المسطّحة بوصفها موترًا (tensor).

6.5.1. ما الذي يسجّله الواصف

خمس قيم تصف كيفية قراءة كتلة البيانات بوصفها موترًا:

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint8)

a.ndim       # 2     - number of dimensions
a.shape      # (2, 3)- length along each dimension
a.itemsize   # 1     - bytes per element (from dtype)
a.size       # 6     - total number of elements
a.strides    # (3, 1)- step pattern through the buffer

تطبع الدالة المساعِدة ndinfo() كلها بالإضافة إلى موقع المخزن المؤقت الأساسي في استدعاء واحد. والمصفوفتان اللتان يتطابق موقعا مخزنيهما المؤقتين تتشاركان الذاكرة:

np.ndinfo(a)
# class: ndarray
# shape: (2, 3)
# strides: (3, 1)
# itemsize: 1
# data pointer: 0x...
# type: uint8

6.5.2. شرح الخطوات (strides)

إنّ الخطوة (stride) هي عدد البايتات التي يجب تخطّيها في كتلة البيانات للتحرك عنصرًا واحدًا على طول محور معين. وبالنسبة للمصفوفة uint8 ذات الأبعاد 2x3 أعلاه، تكون الخطوات (3, 1): فالتحرك للأسفل صفًا واحدًا يقفز 3 بايتات، والتحرك يمينًا عمودًا واحدًا يقفز بايتًا واحدًا. وهذا يماثل القول بأنّ الصفوف مخزَّنة متلاصقة، من اليسار إلى اليمين:

memory: [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ]
          ^ row 0          ^ row 1
          <------- 3 bytes ---->

لقراءة a[i, j]، يحسب numpy القيمة i * strides[0] + j * strides[1] من بداية كتلة البيانات ويقرأ itemsize بايتًا من هناك. وتمتدّ الصيغة نفسها إلى أيّ عدد من الأبعاد.

يُسمّى هذا التخطيط -- الصفوف مخزَّنة متلاصقة طرفًا إلى طرف، مع تغيّر المحور الأخير بأسرع وتيرة على طول الذاكرة -- ترتيبًا رئيسي الصفوف (row-major). وكل مصفوفة يخصّصها numpy على الكاميرا تستخدم هذا التخطيط.

6.5.3. للترتيب رئيسي الصفوف عواقب

ينتج عن "الصفوف مخزَّنة متلاصقة" أمران يهمّان عند تشكيل مخزن مؤقت على الكاميرا.

المحور الأخير متجاور. إنّ السير من a[0, 0] إلى a[0, 1] يلمس البايت التالي. أمّا السير من a[0, 0] إلى a[1, 0] فيقفز عبر صف كامل.

المحور الأخير هو المحور السريع للعمليات الحسابية على المصفوفة كاملةً. يسير numpy على الكاميرا دائمًا في المحور الأخير في أعمق مستوى، بصرف النظر عن أيّ محور يصادف أن يكون أطول. أمّا مكتبة numpy على سطح المكتب فتعيد ترتيب حلقاتها بصمت لتضع المحور الأطول في أعمق مستوى؛ والكاميرا لا تفعل ذلك، لذا فإنّ اختيار تخطيط كان numpy على سطح المكتب سيتجاوزه يكلّف وقتًا هنا. إنّ np.sum(m, axis=1) يطوي المحور الأخير ويعمل في الاتجاه المتجاور؛ بينما np.sum(m, axis=0) لا يفعل ذلك. وعندما يكون لدى التطبيق خيار في كيفية تخطيط مخزن مؤقت، ضع المحور الطويل أخيرًا حتى تبقى العمليات على طوله في الحلقة الداخلية.

إذا بدأ التخطيط خاطئًا، فإنّ transpose() (أو الاختصار .T) تصلحه دون نسخ البيانات -- إذ تبدّل الخطوات فحسب:

a = b.T            # now iterates fast

تتضمّن الأداء المناقشة الكاملة للأداء.

6.5.4. إعادة التشكيل، والتبديل، والتقطيع -- تعديلات على الواصف

إنّ أيّ عملية لا تفعل سوى إعادة كتابة الواصف هي مجانية. إنّ reshape يبدّل shape وstrides جديدين على كتلة البيانات نفسها. وtranspose يعكس الخطوات. وa[::2] يضاعف خطوة. ويُعيد كلٌّ منها مشهدًا للمخزن المؤقت الأساسي نفسه.

أمّا أيّ شيء يتعيّن عليه السير عبر البيانات وكتابة مخزن مؤقت جديد فهو نسخة. والقاعدة الآن هي أنّ تعديلات الواصف مجانية وأنّ السير عبر البيانات ليس كذلك.

6.5.5. ملاحظة حول ndim

بُني numpy على الكاميرا بحدّ أقصى مدعوم لـ ndim يبلغ 4. والعمليات التي قد تُنتج مصفوفة أعلى رتبة تُطلِق ValueError. والغالبية العظمى من العمل على جانب الكاميرا تكون أحادية البعد أو ثنائية البعد، لذا نادرًا ما يكون هذا الحدّ مشكلة.