Slicing
Slice syntax
หัวข้อที่มีชื่อว่า “Slice syntax”Slice notation ของ Python ดึงลำดับย่อยจาก sequence type ใด ๆ — list, tuple, string และ bytes
seq[start : stop : step]start— index ของ element แรกที่รวม (ค่าเริ่มต้น: 0)stop— index ถัดจาก element สุดท้ายที่รวม (ค่าเริ่มต้น:len(seq))step— ช่วงระหว่าง element (ค่าเริ่มต้น: 1)
ส่วนใดก็ตามสามารถละได้ Python จะใส่ค่าเริ่มต้นให้
nums = [10, 20, 30, 40, 50, 60, 70]nums[:3] # [10, 20, 30] — first threenums[2:5] # [30, 40, 50] — indices 2, 3, 4nums[::2] # [10, 30, 50, 70] — every other elementnums[::-1] # [70, 60, 50, 40, 30, 20, 10] — reversedNegative indices
หัวข้อที่มีชื่อว่า “Negative indices”Python รองรับ negative index: -1 คือ element สุดท้าย, -2 คือรองสุดท้าย และต่อไป
nums = [10, 20, 30, 40, 50, 60, 70]nums[-1] # 70 — last elementnums[-3:] # [50, 60, 70] — last threenums[:-3] # [10, 20, 30, 40] — everything except last threeNegative index ทำให้เข้าถึงส่วนท้ายของ sequence ได้ง่ายโดยไม่ต้องรู้ความยาว
มี negative index ตัวหนึ่งที่ไม่ negative จริง ๆ: -0 == 0 seq[-0:] หมายถึง seq[0:] — คือ ทั้ง sequence — ไม่ใช่ “ศูนย์ element สุดท้าย” เลย โค้ดใดก็ตามที่คำนวณ negative bound จากตัวแปรที่อาจเป็น 0 ได้ ต้องจัดการกรณีนี้อย่างชัดเจน ดูตัวอย่างจริงในหัวข้อ “อ่านโค้ดที่ AI เขียน” ด้านล่าง
การกลับลำดับ sequence
หัวข้อที่มีชื่อว่า “การกลับลำดับ sequence”seq[::-1] คือวิธีที่ idiomatic ใน Python เพื่อกลับลำดับ sequence ใด ๆ
คืนค่าเป็น object ใหม่ที่กลับลำดับ — ไม่แก้ไข sequence ต้นฉบับ
original = [1, 2, 3, 4, 5]reversed_copy = original[::-1] # [5, 4, 3, 2, 1]print(original) # [1, 2, 3, 4, 5] — unchangedสำหรับ string [::-1] ทำงานเหมือนกัน:
s = 'Hello, World!'print(s[::-1]) # !dlroW ,olleHnegative step ที่ไม่ใช่ -1 จะเดินถอยหลังพร้อม stride — [::-2] กลับลำดับและเอาทุก element ที่สองด้วย:
letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g']print(letters[::-2]) # ['g', 'e', 'c', 'a']Shallow copy ผ่าน slice
หัวข้อที่มีชื่อว่า “Shallow copy ผ่าน slice”seq[:] (ไม่มี start, stop หรือ step) คืน shallow copy ของ sequence
list ใหม่เป็น object ที่ต่างกันแต่แต่ละ element ยังชี้ไปยัง object เดียวกัน
original = [1, 2, 3, 4, 5]copy = original[:]copy[0] = 99print(original) # [1, 2, 3, 4, 5] — unchangedprint(copy) # [99, 2, 3, 4, 5]สำหรับโครงสร้างซ้อน (list ภายใน list) ใช้ copy.deepcopy() แทนหากต้องการ inner list ที่เป็นอิสระ
Slice assignment และการลบ
หัวข้อที่มีชื่อว่า “Slice assignment และการลบ”ต่างจากการอ่าน การ assign เข้า slice จะแก้ไข list ใน place — และตัวแทนที่ assign เข้าไปไม่จำเป็นต้องยาวเท่ากับ slice ที่ถูกแทนที่:
nums = [10, 20, 30, 40, 50]nums[1:3] = [99, 98, 97]print(nums) # [10, 99, 98, 97, 40, 50] — 2 elements replaced by 3, the list grewnums2 = [10, 20, 30, 40, 50]nums2[1:4] = [0]print(nums2) # [10, 0, 50] — 3 elements replaced by 1, the list shrankdel seq[start:stop] ลบ slice ออกทั้งหมด แล้ว element ที่เหลือขยับลงมา:
nums3 = [10, 20, 30, 40, 50]del nums3[1:3]print(nums3) # [10, 40, 50]slice ธรรมดา (ละ step หรือเป็น 1) ยอมรับความยาวที่ไม่ตรงกันได้ — นั่นคือวิธีที่มันทำให้ list โตหรือหดได้ ส่วน slice แบบ extended (step อื่นใด) ไม่ยอมรับ: ตัวแทนที่ assign เข้าไปต้องมีจำนวน element เท่ากับที่ slice เลือกไว้เป๊ะ ๆ เพราะไม่มีความคลุมเครือให้แก้ว่าตำแหน่งไหนควรได้ค่าไหน
nums4 = [1, 2, 3, 4, 5]nums4[::2] = [10, 30, 50] # exactly 3 elements for 3 selected positionsprint(nums4) # [10, 2, 30, 4, 50]
nums5 = [1, 2, 3, 4, 5]nums5[::2] = [100]# ValueError: attempt to assign sequence of size 1 to extended slice of size 3Slice บน sequence type อื่น: tuple, str, bytes, และ range
หัวข้อที่มีชื่อว่า “Slice บน sequence type อื่น: tuple, str, bytes, และ range”Slicing ทำงานเหมือนกันทุกประการข้าม built-in sequence type ทุกตัว — จะได้ object ประเภทเดียวกับที่ slice เสมอ ยกเว้นตัวเดียว: การ slice range จะได้ range อีกตัว คำนวณแบบ lazy ไม่ใช่ list:
t = (10, 20, 30, 40, 50)print(t[1:4]) # (20, 30, 40) — a tuple
s = 'abcdef'print(s[1:4]) # 'bcd' — a str
b = b'abcdef'print(b[1:4]) # b'bcd' — bytes
r = range(0, 20, 2)sub = r[2:5]print(sub) # range(4, 10, 2) — still a range, not a listprint(list(sub)) # [4, 6, 8] — only materialised when you askTuple, str, และ bytes ล้วนเปลี่ยนแปลงไม่ได้ ดังนั้น — ต่างจาก list — ทั้งสามไม่รองรับการ assign เข้า slice เลย; t[1:3] = (...) จะ raise TypeError
Named slices และ slice object
หัวข้อที่มีชื่อว่า “Named slices และ slice object”seq[a:b:c] คือ syntactic sugar: Python สร้าง slice object แล้วส่งเข้า __getitem__ คุณสร้างและเก็บ object นั้นเองได้ด้วย slice(start, stop, step) ซึ่งทำให้ slice ใช้ซ้ำได้และอธิบายตัวเองได้:
HEADER = slice(None, 3)PAYLOAD = slice(3, -2)FOOTER = slice(-2, None)
data = b'HDR' + b'payload' + b'FT'print(data[HEADER]) # b'HDR'print(data[PAYLOAD]) # b'payload'print(data[FOOTER]) # b'FT'method .indices(length) ของ slice object จะ resolve start/stop/step — รวมถึงค่า negative และค่าที่เกินขอบเขต — ให้เป็นค่าที่เป็นรูปธรรม, clamp แล้ว, และวนไปข้างหน้าได้เสมอสำหรับ sequence ที่มีความยาวตามที่กำหนด นี่คือสิ่งที่ __getitem__ ทำภายในก่อนจะเดินผ่าน sequence:
sl = slice(1, 10, 2)print(sl.indices(5)) # (1, 5, 2) — stop clamped from 10 down to 5
nums6 = [0, 1, 2, 3, 4]start, stop, step = sl.indices(len(nums6))print([nums6[i] for i in range(start, stop, step)]) # [1, 3] — matches nums6[sl] exactlyตัวอย่างรันได้
หัวข้อที่มีชื่อว่า “ตัวอย่างรันได้”nums = [10, 20, 30, 40, 50, 60, 70]print('first 3:', nums[:3])print('last 3:', nums[-3:])print('middle:', nums[2:5])print('every other:', nums[::2])print('reversed:', nums[::-1])
s = 'Hello, World!'print('first 5:', s[:5])print('last 6:', s[-6:])print('every 2nd:', s[::2])print('reversed:', s[::-1])
original = [1, 2, 3, 4, 5]copy = original[:]copy[0] = 99print('original:', original)print('copy:', copy)Loading Python runtime (first run only)…
playground ด้านล่างครอบคลุมหัวข้อความลึกด้านบน — slice assignment ที่ทำให้ list โตและหด, del บน slice, การตรวจความยาวของ extended slice, [::-2], การ slice tuple/str/bytes/range, และ .indices() ของ slice object:
# --- slice assignment: replace a range, can change length ---nums = [10, 20, 30, 40, 50]nums[1:3] = [99, 98, 97]print('after slice assignment (grows):', nums)
nums2 = [10, 20, 30, 40, 50]nums2[1:4] = [0]print('after slice assignment (shrinks):', nums2)
# --- slice deletion ---nums3 = [10, 20, 30, 40, 50]del nums3[1:3]print('after slice deletion:', nums3)
# --- extended slice assignment must match length exactly ---nums4 = [1, 2, 3, 4, 5]nums4[::2] = [10, 30, 50]print('extended slice assign (matching length):', nums4)
nums5 = [1, 2, 3, 4, 5]try: nums5[::2] = [100]except ValueError as e: print('ValueError:', e)
# --- [::-2] ---letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g']print('[::-2]:', letters[::-2])
# --- slicing works the same on tuple, str, bytes ---t = (10, 20, 30, 40, 50)print('tuple slice:', t[1:4], type(t[1:4]).__name__)
s = 'abcdef'print('str slice:', s[1:4], type(s[1:4]).__name__)
b = b'abcdef'print('bytes slice:', b[1:4], type(b[1:4]).__name__)
# --- slicing a range returns a NEW range, not a list ---r = range(0, 20, 2)sub = r[2:5]print('range slice:', sub, type(sub).__name__, list(sub))
# --- slice object + .indices() ---sl = slice(1, 10, 2)print('slice object:', sl)print('indices() resolved against len=5:', sl.indices(5))
nums6 = [0, 1, 2, 3, 4]start, stop, step = sl.indices(len(nums6))manual = [nums6[i] for i in range(start, stop, step)]print('manual walk using indices():', manual)print('direct slice result:', nums6[sl])Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart LR Seq["seq[1:4]"] -->|excluded| A0["seq[0]"] Seq -->|included| A1["seq[1]"] Seq -->|included| A2["seq[2]"] Seq -->|included| A3["seq[3]"] Seq -->|excluded, this is stop| A4["seq[4]"]
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
การ slice (seq[a:b:c]) | ไม่ raise IndexError เลย — ขอบเขตที่เกินจะถูก clamp แบบเงียบ ๆ | การ clamp เดียวกันนั้นอาจซ่อน off-by-one bug แทนที่จะโผล่ให้เห็น |
slice assignment ธรรมดา (ละ step) | ทำให้ list โตหรือหดได้ใน statement เดียว | ความยาวใหม่จะตรงกับความตั้งใจก็ต่อเมื่อคุณตั้งใจจริง ๆ ว่า “แทนที่ทั้งช่วงนี้” |
extended-slice assignment (step != 1) | แทนที่ตามตำแหน่งได้แม่นยำ | เข้มงวด — ความยาวไม่ตรงกันจะ raise ValueError แทนที่จะตัดหรือเติมให้แบบเงียบ ๆ |
การ slice range | ยังคง lazy — range ขนาดใหญ่ slice ได้ O(1) ไม่ใช่ O(n) | คืน range ไม่ใช่ list — โค้ดที่คาดหวัง method ของ list (.append, การ index ด้วย API ที่รับแค่ list) ต้อง list(...) ก่อน |
slice object + .indices() | ใช้ซ้ำได้ ตั้งชื่อได้ และ resolve ขอบเขต negative/เกินขอบเขตให้เอง | ต้องอ่านผ่านชั้นเพิ่มอีกหนึ่งชั้นเมื่อเทียบกับ [a:b:c] แบบ inline |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “index ที่เกินขอบเขตของ slice จะ raise
IndexErrorเหมือน indexing ธรรมดา” — ความจริง:a[100:]บน list 5 element คืน[]; slicing จะ clamp เสมอ ไม่ raise - “
seq[-0:]หมายถึง tail ที่ว่างเปล่า — ศูนย์ element” — ความจริง:-0 == 0ใน Python ดังนั้นseq[-0:]คือseq[0:]เป๊ะ ๆ — ทั้ง sequence - “slice assignment ต้องแทนที่ด้วยจำนวน element เท่าเดิม” — ความจริง: จริงเฉพาะ extended slice (
step != 1) เท่านั้น; slice ธรรมดาแทนที่แล้วทำให้ list โตหรือหดได้อย่างอิสระ - “slice
rangeจะได้ list” — ความจริง: ได้rangeอีกตัว คำนวณแบบ lazy; ครอบด้วยlist(...)ถ้าต้องการ element ที่เป็นรูปธรรม - “
a[:],a[::1], และlist(a)ทำงานเหมือนกันทุกบริบท” — ความจริง: ทั้งสามทำ shallow copy ของ list แต่มีแค่a[:]/a[::1]เท่านั้นที่ใช้กับ sequence type ใดก็ได้ผ่าน__getitem__; custom sequence class บางตัวมีแค่อย่างใดอย่างหนึ่ง
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”- function คำนวณ negative slice bound จากตัวแปรที่อาจเป็น
0ได้หรือเปล่า (เช่นitems[-n:]เพื่อ “ตัดnitem สุดท้ายทิ้ง”)? เมื่อn == 0,-nคือ0ไม่ใช่ “ไม่มี item” —items[-0:]คืน ทั้ง sequence และถ้าผลลัพธ์นั้นถูกลบหรือทิ้งไป bug จะเงียบและร้ายแรง - โค้ด slice assignment สมมติว่าตัวแทนที่ assign ยาวเท่ากับ slice เดิมเสมอหรือเปล่า? นั่นรับประกันเฉพาะ extended slice เท่านั้น; สำหรับ slice ธรรมดา ตัวแทนที่สั้นหรือยาวกว่าถูกต้องและเปลี่ยนความยาวของ list ได้ — ตรวจว่าการเปลี่ยนความยาวนั้นตั้งใจหรือไม่
rangeobject ถูก slice แล้วเอาไปใช้ทันทีในที่ที่คาดหวังlist(.append(), JSON serialization, API ที่รับแค่ list) หรือเปล่า? ต้องlist(...)ก่อน- โค้ด slice string หรือ tuple แล้วพยายามแก้ไขผลลัพธ์ใน place หรือเปล่า? การ slice
str,tuple, หรือbytesคืน object ประเภท immutable เดียวกัน —s[1:3][0] = 'x'ล้มเหลวแบบเดียวกับs[0] = 'x' - slice ที่กลับลำดับพร้อม stride (
[::-2],[::-3], …) ปรากฏในที่ที่น่าจะตั้งใจแค่กลับลำดับธรรมดา ([::-1]) หรือเปล่า? เลขเกินมาตัวเดียวจะข้าม element แบบเงียบ ๆ แทนที่จะ error
🐛 หา bug ในโค้ดนี้
def drop_last_n(items, n): del items[-n:] return items
print(drop_last_n([1, 2, 3, 4, 5], 0))print(drop_last_n([1, 2, 3, 4, 5], 2))เฉลย
function สมมติว่า n=0 หมายถึง “ไม่ต้องลบอะไรเลย” แต่ -n เมื่อ n == 0 คือ -0 ซึ่ง Python ถือว่าเท่ากับ 0 เป๊ะ ๆ — ไม่ใช่ “negative zero” และไม่ใช่ “ไม่มี item จากท้าย” ดังนั้น items[-0:] คือ items[0:] ทั้ง list และ del items[0:] จะลบมันทิ้งทั้งหมด ตรวจสอบแล้วบน Python 3.14: การเรียกครั้งแรกพิมพ์ [] ไม่ใช่ [1, 2, 3, 4, 5] ที่ไม่เปลี่ยนแปลงตามชื่อ drop_last_n สัญญาไว้; การเรียกครั้งที่สอง (n=2) ทำงานตามที่ตั้งใจและพิมพ์ [1, 2, 3] ไม่มี exception เกิดขึ้นเลยสำหรับกรณี n=0 ซึ่งเป็นสิ่งที่ทำให้ bug นี้หลุดออกไปได้ง่าย วิธีแก้คือใส่ guard ชัดเจน: if n: del items[-n:] หรือ slice ด้วย stop ที่ชัดเจนแทน negative start: del items[len(items) - n:]