6.6. אינדוקס וחיתוך¶
אל ndarray ניגשים בארבע דרכים: אינדקסים בודדים, פרוסות, מסכות בוליאניות, וצורות ההשמה של כל אחת מהן.
6.6.1. איברים בודדים¶
אינדוקס בסוגריים מרובעים מחזיר את הערך במיקום הנתון:
a = np.arange(10, dtype=np.uint8)
print(a[0], a[-1]) # 0 9
print(a[1], a[-2]) # 1 8
אינדקסים שליליים נספרים מהסוף, בדיוק כמו עבור list של פייתון. אינדקס מחוץ לטווח מעורר IndexError.
עבור מערכים מדרגה גבוהה יותר, כל ציר מקבל אינדקס. האינדקסים הולכים בתוך מערך אחד של סוגריים, מופרדים בפסיקים:
m = np.arange(9, dtype=np.uint8).reshape((3, 3))
print(m[1, 1]) # 4
print(m[2, 0]) # 6
כאשר מסופקים פחות אינדקסים ממספר הצירים, הצירים שלא אונדקסו נשארים שלמים. התוצאה היא תצוגה מדרגה מצומצמת של המקור:
print(m[0]) # the first row, as a 1-D view of m
6.6.2. פרוסות¶
פרוסה start:stop:step מחזירה תצוגה של המערך. התצוגה חולקת את חוצץ הנתונים הבסיסי עם המקור; כתיבה דרך התצוגה כותבת אל המקור:
a = np.arange(10, dtype=np.uint8)
v = a[::2] # array([0, 2, 4, 6, 8], dtype=uint8)
v[0] = 99
print(a)
# array([99, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)
כאשר נדרש חוצץ עצמאי, copy() מייצר אחד במפורש.
החיתוך מתרחב באופן טבעי לממדים גבוהים יותר. כל ציר מקבל פרוסה משלו:
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], dtype=np.uint8)
m[0] # first row
m[0, :2] # first two elements of row 0
m[:, 0] # column 0 (still 2-D in ulab)
m[-1] # last row
m[::2, ::2] # every other row, every other column
ערבוב של מספר שלם (אינדקס יחיד, משמיט את הציר) ופרוסה (שומרת על הציר) מותר וכך נכתבת בדרך כלל גישה לשורה בודדת / עמודה בודדת.
6.6.3. מסכות בוליאניות¶
מערך בוליאני בעל אותה צורה כמו המקור בוחר איברים שבהם המסכה היא True. אינדוקס בוליאני פועל כיום על מערכים חד-ממדיים; קלטים מדרגה גבוהה יותר מעוררים NotImplementedError
a = np.arange(9, dtype=np.float)
mask = a < 5
print(a[mask])
פלט:
array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)
המסכה היא ndarray בוליאני bool רגיל, כך שכל ביטוי המניב כזה עובד:
b = np.array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=np.uint8)
a = np.arange(9, dtype=np.uint8)
print(a[a * a > np.sin(b) * 100.0])
אינדוקס בוליאני מחזיר העתק. האיברים הנבחרים נמצאים בכל מיקום שבו המסכה היא True – ולא בצעד קבוע דרך המקור – ולכן אין מתאר (descriptor) שתצוגה יכולה להשתמש בו כדי לפנות אליהם, והתוצאה מתממשת לתוך חוצץ משלה.
6.6.4. אינדוקס במערך מספרים שלמים¶
העברת רשימה או מערך של אינדקסים בסוגריים בוחרת את אותם איברים בצעד אחד:
a = np.array([10, 20, 30, 40, 50], dtype=np.uint8)
a[[0, 2, 4]]
# array([10, 30, 50], dtype=uint8)
התוצאה היא העתק; האיברים הנבחרים אינם חולקים עוד אחסון עם המקור. אותה צורה עובדת בצד שמאל של השמה:
a[[0, 2, 4]] = 0
# array([0, 20, 0, 40, 0], dtype=uint8)
take() (מכוסה ב-בחירה וסידור מחדש) היא צורת הפונקציה של אותה פעולה ומקבלת מילת מפתח out= לשימוש ללא הקצאה בלולאת זרימה.
6.6.5. השמה לפרוסה¶
פרוסות ומסכות מופיעות בצד שמאל של השמה כמו גם בצד ימין. הצד הימני יכול להיות סקלר, מערך אחר, או תצוגה:
m = np.zeros((3, 3), dtype=np.uint8)
m[0] = 1 # whole row 0 set to 1
m[:, 2] = 3 # whole column 2 set to 3
m[1, 1:3] = [7, 8] # row 1, columns 1 and 2
מסכות בוליאניות בצד שמאל מחליפות את האיברים העונים על התנאי:
a = np.arange(9, dtype=np.uint8)
a[a < 3] = 99
# array([99, 99, 99, 3, 4, 5, 6, 7, 8], dtype=uint8)
a = np.arange(9, dtype=np.uint8)
b = np.array(range(9)) + 12
a[b < 15] = b[b < 15]
# array([12, 13, 14, 3, 4, 5, 6, 7, 8], dtype=uint8)
6.6.6. מדוע השמה לפרוסה חשובה במצלמה¶
השמה לפרוסה כותבת דרך מערך שכבר קיים. שום מערך חדש אינו מוקצה. זה ההבדל בין:
out = a + b # makes a new array the size of a
out = out * 2 # makes another new array
לבין:
out[:] = a # writes into the existing out
out += b # in place
out *= 2 # in place
הגרסה הראשונה מבקשת מהמצלמה RAM בשווי שני מערכים חדשים; הגרסה השנייה אינה מבקשת דבר. על מיקרו-בקר עם RAM מוגבל ההבדל הזה הוא לעיתים קרובות ההבדל בין סקריפט שרץ בנוחות לבין כזה שנגמר לו הזיכרון.
ביצועים מכסה את התבנית בפירוט. הכלל החשוב לעת עתה הוא שהשמה לפרוסה, אופרטורי החשבון במקום (+=, *=, …), ומילת המפתח out= בפונקציות אוניברסליות הם שלושת הכלים שהופכים עדכונים ללא הקצאה לאפשריים.