6.7. المشاهد والنسخ

إنّ المشهد هو نافذة ثانية على كتلة البيانات نفسها التي للمصدر. ولا تُنسَخ أيّ بيانات؛ فالمشهد يحمل واصفًا جديدًا (له شكله وخطواته ونوع بياناته الخاص) لكنه يشارك المخزن المؤقت. والمشاهد مجانية في جوهرها.

أمّا النسخة فتطلب من الكاميرا مخزنًا مؤقتًا جديدًا وتسير عبر المصدر مالئةً إياه. والنسخ تكلّف كلًّا من الوقت وذاكرة RAM.

تُنتج معظم طرق تغيير الشكل مشاهد. وتُنتج معظم الطرق المحوِّلة للبيانات نسخًا. ومعرفة أيٍّ منها هو أيٌّ تحدّد ما إذا كانت حلقة ساخنة ستنفد ذاكرة RAM الكاميرا.

6.7.1. إعادة التشكيل

تُعيد reshape() مصفوفة بالشكل المطلوب. ويجب أن يبقى العدد الإجمالي للعناصر دون تغيير وإلّا يُطلَق ValueError

a = np.arange(12, dtype=np.uint8)
m = a.reshape((3, 4))

النتيجة هي مشهد -- إذ يشارك m وa البيانات. والكتابة عبر m[0, 0] = 99 تغيّر a[0] أيضًا.

إنّ إسناد صفّ (tuple) جديد إلى shape هو اختصار للعملية نفسها:

a = np.arange(9)
a.shape = (3, 3)

6.7.2. التبديل (Transpose)

تعكس transpose() (أو الاختصار .T) المحاور. ومنفَّذة عن طريق عكس الخطوات -- ولا تُنقَل أيّ بيانات:

m = np.arange(6, dtype=np.uint8).reshape((2, 3))
t = m.T                  # shape (3, 2), shares m's buffer

إنّ المشهد المبدَّل لا يسير عبر كتلة البيانات بشكل متجاور. فقراءة t أعلاه صفًا صفًا تزور مواضع الذاكرة 0, 3, 1, 4, 2, 5، وليس الترتيب الأساسي 0, 1, 2, 3, 4, 5 الذي تُرتَّب به البايتات. وتتعامل العمليات الحسابية وعمليات الاختزال العادية مع ذلك على نحو جيد -- إذ تخطو عبر الخطوات -- لكنّ tobytes() لا تستطيع ذلك، لأنها تعيد المخزن المؤقت الأساسي مباشرةً دون نسخ. والبايتات التي يحملها المخزن المؤقت لا تطابق الترتيب الذي يفرضه شكل المشهد، لذا تُطلِق الطريقة ValueError على أيّ مشهد غير متجاور. وعندما تكون البايتات مطلوبة بالترتيب المبدَّل، افرض أولًا نسخة متجاورة جديدة:

bytes_out = t.copy().tobytes()

6.7.3. Flatten و flat

تُعيد flatten() نسخة أحادية البعد من المصفوفة:

f = m.flatten()          # new dense 1-D ndarray

مرّر order='C' (الافتراضي) للسير في المحور الأخير أولًا أو order='F' للسير في المحور الأول أولًا:

m = np.arange(6, dtype=np.uint8).reshape((2, 3))
# m = [[0, 1, 2],
#      [3, 4, 5]]
m.flatten()              # array([0, 1, 2, 3, 4, 5], dtype=uint8)
m.flatten(order='F')     # array([0, 3, 1, 4, 2, 5], dtype=uint8)

إنّ flat هي صيغة المتكرِّر (iterator). وهي تُنتج كل عنصر من أيّ ndarray بأيّ رتبة على هيئة أعداد قياسية، دون تخصيص نسخة مسطّحة:

for x in m.flat:
    print(x)

عندما يحتاج التطبيق إلى السير عبر كل عنصر، فضّل flat؛ وعندما يحتاج إلى مخزن مؤقت كثيف أحادي البعد لتمريره إلى دالة أخرى، استخدم flatten().

6.7.4. التكرار

إنّ تكرار مصفوفة أحادية البعد يُنتج أعدادًا قياسية؛ وتكرار مصفوفة أعلى رتبة يُنتج مشاهد بـ (n-1) بعدًا:

m = np.array([[0, 1, 2], [3, 4, 5]], dtype=np.uint8)
for row in m:
    print(row)               # array([0, 1, 2]), array([3, 4, 5])

الصفوف التي يُنتجها تكرار مصفوفة هي مشاهد، لذا فإنّ تعديلها يعدّل المصدر.

6.7.5. النسخ

إنّ copy() هي الطريقة الصريحة للحصول على ndarray مستقل لا تؤثر تعديلاته في الأصل. إذ يُخصَّص مخزن مؤقت جديد ويُسار عبر المصدر إليه:

c = a.copy()

تُعيد tobytes() كائن bytearray يشارك الذاكرة مع كتلة بيانات المصفوفة. والكتابة عبر الـ bytearray تعدّل المصفوفة في المكان. وتُطلِق ValueError إذا لم تكن المصفوفة كثيفة (مشهد مقطَّع، أو تبديل، ...).

تُعيد tolist() المحتويات على هيئة list خاصة بـ Python ربما تكون متداخلة. وهي مفيدة لتسلسُل (serialising) النتائج الصغيرة؛ ومكلِّفة للكبيرة منها، لأنّ كل عنصر يصبح كائن Python منفصلًا.

6.7.6. أيّ العمليات تُعيد أيًّا

القاعدة الكاملة:

تُعيد العمليات التالية مشاهد:

  • التقطيع -- a[1:5]، وa[::2]، وm[:, 0]؛

  • فهرسة المحور الواحد لمصفوفة أعلى رتبة -- m[0]؛

  • تكرار مصفوفة بـ n من الأبعاد؛

  • reshape()، عندما يكون التخطيط المطلوب متوافقًا؛

  • transpose() / .T؛

  • frombuffer()؛

  • asarray()، عندما يتطابق نوع البيانات (dtype).

تُعيد العمليات التالية نسخًا:

  • copy()؛

  • flatten()؛

  • الفهرسة المنطقية -- a[mask]؛

  • العمليات الحسابية -- a + b، وa * 2، وnp.sin(a)؛

  • array() -- تنسخ دائمًا، حتى من مصفوفة أخرى؛

  • concatenate().

لا تلجأ إلى نسخة صريحة إلّا عندما تكون هناك حاجة فعلية إلى مخزن مؤقت مستقل. وعلى كاميرا ذات ذاكرة RAM محدودة، يكون الفرق بين المشهد والنسخة غالبًا هو الفرق بين شيفرة تتّسع وشيفرة لا تتّسع.