ข้ามไปยังเนื้อหา

Slicing

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 three
nums[2:5] # [30, 40, 50] — indices 2, 3, 4
nums[::2] # [10, 30, 50, 70] — every other element
nums[::-1] # [70, 60, 50, 40, 30, 20, 10] — reversed

Python รองรับ negative index: -1 คือ element สุดท้าย, -2 คือรองสุดท้าย และต่อไป

nums = [10, 20, 30, 40, 50, 60, 70]
nums[-1] # 70 — last element
nums[-3:] # [50, 60, 70] — last three
nums[:-3] # [10, 20, 30, 40] — everything except last three

Negative index ทำให้เข้าถึงส่วนท้ายของ sequence ได้ง่ายโดยไม่ต้องรู้ความยาว

มี negative index ตัวหนึ่งที่ไม่ negative จริง ๆ: -0 == 0 seq[-0:] หมายถึง seq[0:] — คือ ทั้ง sequence — ไม่ใช่ “ศูนย์ element สุดท้าย” เลย โค้ดใดก็ตามที่คำนวณ negative bound จากตัวแปรที่อาจเป็น 0 ได้ ต้องจัดการกรณีนี้อย่างชัดเจน ดูตัวอย่างจริงในหัวข้อ “อ่านโค้ดที่ AI เขียน” ด้านล่าง

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 ,olleH

negative step ที่ไม่ใช่ -1 จะเดินถอยหลังพร้อม stride — [::-2] กลับลำดับและเอาทุก element ที่สองด้วย:

letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
print(letters[::-2]) # ['g', 'e', 'c', 'a']

seq[:] (ไม่มี start, stop หรือ step) คืน shallow copy ของ sequence list ใหม่เป็น object ที่ต่างกันแต่แต่ละ element ยังชี้ไปยัง object เดียวกัน

original = [1, 2, 3, 4, 5]
copy = original[:]
copy[0] = 99
print(original) # [1, 2, 3, 4, 5] — unchanged
print(copy) # [99, 2, 3, 4, 5]

สำหรับโครงสร้างซ้อน (list ภายใน list) ใช้ copy.deepcopy() แทนหากต้องการ inner list ที่เป็นอิสระ

ต่างจากการอ่าน การ 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 grew
nums2 = [10, 20, 30, 40, 50]
nums2[1:4] = [0]
print(nums2) # [10, 0, 50] — 3 elements replaced by 1, the list shrank

del 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 positions
print(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 3

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 list
print(list(sub)) # [4, 6, 8] — only materialised when you ask

Tuple, str, และ bytes ล้วนเปลี่ยนแปลงไม่ได้ ดังนั้น — ต่างจาก list — ทั้งสามไม่รองรับการ assign เข้า slice เลย; t[1:3] = (...) จะ raise TypeError

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] = 99
print('original:', original)
print('copy:', copy)

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])
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]"]
stop คือ index ถัดจากตัวสุดท้ายที่รวมเสมอ -- seq[1:4] รวม index 1, 2, 3 และไม่แตะ index 4 เลย
สิ่งที่ได้ประโยชน์ต้นทุน
การ 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
  • “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 บางตัวมีแค่อย่างใดอย่างหนึ่ง
  • function คำนวณ negative slice bound จากตัวแปรที่อาจเป็น 0 ได้หรือเปล่า (เช่น items[-n:] เพื่อ “ตัด n item สุดท้ายทิ้ง”)? เมื่อ n == 0, -n คือ 0 ไม่ใช่ “ไม่มี item” — items[-0:] คืน ทั้ง sequence และถ้าผลลัพธ์นั้นถูกลบหรือทิ้งไป bug จะเงียบและร้ายแรง
  • โค้ด slice assignment สมมติว่าตัวแทนที่ assign ยาวเท่ากับ slice เดิมเสมอหรือเปล่า? นั่นรับประกันเฉพาะ extended slice เท่านั้น; สำหรับ slice ธรรมดา ตัวแทนที่สั้นหรือยาวกว่าถูกต้องและเปลี่ยนความยาวของ list ได้ — ตรวจว่าการเปลี่ยนความยาวนั้นตั้งใจหรือไม่
  • range object ถูก 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:]

nums[2:5] คืนค่าอะไรสำหรับ nums = [10, 20, 30, 40, 50, 60, 70]?
โค้ดนี้พิมพ์อะไร? items = [1, 2, 3, 4, 5] n = 0 del items[-n:] print(items)
โค้ดนี้พิมพ์อะไร? nums = [1, 2, 3, 4, 5] nums[::2] = [100]
range(0, 20, 2)[2:5] มีค่าเท่ากับอะไร?
original[:] สร้าง deep copy หรือ shallow copy?
slice object คือ slice(1, 10, 2) .indices(5) คืนค่าอะไร และทำไม?