6.7. View และ copy

view คือหน้าต่างที่สองบนบล็อกข้อมูลเดียวกับแหล่งที่มา ไม่มีการคัดลอกข้อมูล; view เก็บ descriptor ใหม่ (shape, stride, และ dtype ของตัวเอง) แต่แบ่งปัน buffer ต้นทุนของ view แทบเป็นศูนย์

copy ขอ cam ให้สร้าง buffer ใหม่และเดินผ่านแหล่งที่มาเพื่อเติมข้อมูล copy มีต้นทุนทั้งเวลาและ RAM

เมธอดส่วนใหญ่ที่เปลี่ยนรูปร่างจะสร้าง view ส่วนใหญ่ที่แปลงข้อมูลจะสร้าง copy การรู้ว่าอย่างไหนเป็นอย่างไหนจะกำหนดว่า hot loop จะทำให้ cam หมด RAM หรือไม่

6.7.1. Reshape

reshape() คืนค่า array ตาม shape ที่ขอ จำนวน elements ทั้งหมดต้องไม่เปลี่ยนแปลง มิฉะนั้นจะยก ValueError

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

ผลลัพธ์คือ view -- m และ a แบ่งปันข้อมูล การเขียน m[0, 0] = 99 จะเปลี่ยน a[0] ด้วย

การกำหนด tuple ใหม่ให้กับ shape คือ shorthand สำหรับการดำเนินการเดียวกัน:

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

6.7.2. Transpose

transpose() (หรือทางลัด .T) กลับแกน ทำโดยการกลับ stride -- ไม่มีการเคลื่อนย้ายข้อมูล:

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

view ที่ transpose แล้วไม่เดินผ่านบล็อกข้อมูลอย่างต่อเนื่อง การอ่าน t ด้านบนทีละแถวจะเยี่ยมชมตำแหน่งหน่วยความจำ 0, 3, 1, 4, 2, 5 ไม่ใช่ลำดับ 0, 1, 2, 3, 4, 5 ของ byte ที่วางในพื้นฐาน การคำนวณและ reduction ธรรมดาจัดการได้ดี -- พวกมันก้าวผ่าน stride -- แต่ tobytes() ไม่สามารถทำได้ เพราะมันส่งคืน buffer พื้นฐานโดยตรงโดยไม่คัดลอก ไบต์ที่ buffer เก็บไม่ตรงกับลำดับที่ shape ของ view บอกเป็นนัย ดังนั้นเมธอดจะยก ValueError บน view ที่ไม่ต่อเนื่อง เมื่อต้องการไบต์ในลำดับที่ transpose แล้ว ให้บังคับสร้าง copy ที่ต่อเนื่องก่อน:

bytes_out = t.copy().tobytes()

6.7.3. Flatten และ flat

flatten() คืนค่า copy 1 มิติของ array:

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 มันให้ผลแต่ละ element ของ ndarray rank ใดก็ได้เป็น scalar โดยไม่จัดสรร flat copy:

for x in m.flat:
    print(x)

เมื่อแอปพลิเคชันต้องการ เดินผ่าน ทุก element ให้ใช้ flat; เมื่อต้องการ buffer 1 มิติแบบ dense เพื่อส่งไปยังฟังก์ชันอื่น ให้ใช้ flatten()

6.7.4. การวนซ้ำ

การวนซ้ำ array 1 มิติให้ผล scalar; การวนซ้ำ array rank สูงกว่าให้ผล view แบบ (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])

แถวที่ได้จากการวนซ้ำ matrix คือ view ดังนั้นการแก้ไขแถวเหล่านั้นจะแก้ไขแหล่งที่มาด้วย

6.7.5. Copy

copy() คือวิธีชัดเจนในการรับ ndarray อิสระที่การแก้ไขไม่กระทบต้นฉบับ จะจัดสรร buffer ใหม่และเดินผ่านแหล่งที่มาลงใน buffer นั้น:

c = a.copy()

tobytes() คืนค่า bytearray ที่แบ่งปันหน่วยความจำกับบล็อกข้อมูลของ array การเขียนผ่าน bytearray จะแก้ไข array in place ยก ValueError หาก array ไม่ dense (เช่น sliced view, transpose, ...)

tolist() คืนค่าเนื้อหาเป็น Python list ที่อาจซ้อนกัน มีประโยชน์สำหรับการ serialize ผลลัพธ์ขนาดเล็ก; มีต้นทุนสูงสำหรับผลลัพธ์ขนาดใหญ่ เพราะทุก element กลายเป็น Python object แยกต่างหาก

6.7.6. การดำเนินการไหนให้ผลอะไร

กฎฉบับสมบูรณ์:

การดำเนินการต่อไปนี้คืนค่า view:

  • slicing -- a[1:5], a[::2], m[:, 0];

  • การ index แกนเดียวของ array rank สูงกว่า -- m[0];

  • การวนซ้ำ array แบบ n มิติ;

  • reshape(), เมื่อ layout ที่ขอรองรับได้;

  • transpose() / .T;

  • frombuffer();

  • asarray(), เมื่อ dtype ตรงกัน

การดำเนินการต่อไปนี้คืนค่า copy:

  • copy();

  • flatten();

  • boolean indexing -- a[mask];

  • การคำนวณทางคณิตศาสตร์ -- a + b, a * 2, np.sin(a);

  • array() -- คัดลอกเสมอ แม้จาก array อื่น;

  • concatenate().

ใช้ copy อย่างชัดเจนเฉพาะเมื่อต้องการ buffer อิสระอย่างแท้จริง บน camera ที่มี RAM จำกัด ความแตกต่างระหว่าง view และ copy มักเป็นความแตกต่างระหว่าง code ที่พอดีและ code ที่ไม่พอดี