Lists and Tuples
Lists — ลำดับที่เปลี่ยนแปลงได้
หัวข้อที่มีชื่อว่า “Lists — ลำดับที่เปลี่ยนแปลงได้”list คือ container หลักของ Python ที่เก็บลำดับของ object ใด ๆ และรองรับการเปลี่ยนแปลงค่าภายใน
fruits = ['apple', 'banana', 'cherry']fruits.append('date') # add to endfruits.insert(1, 'avocado') # insert before index 1fruits.remove('banana') # remove first occurrenceprint(fruits) # ['apple', 'avocado', 'cherry', 'date']Method ที่ใช้บ่อย
หัวข้อที่มีชื่อว่า “Method ที่ใช้บ่อย”| Method | คำอธิบาย |
|---|---|
append(x) | เพิ่ม x ที่ท้าย |
insert(i, x) | แทรก x ก่อน index i |
remove(x) | ลบรายการแรกที่ตรงกับ x |
pop(i=-1) | ลบและคืนค่า item ที่ index i |
sort(key=None, reverse=False) | เรียงลำดับใน place |
reverse() | กลับลำดับใน place |
index(x) | คืน index ของรายการแรกที่ตรงกับ x |
count(x) | นับจำนวนรายการที่ตรงกับ x |
การเรียงลำดับ: ใน place เทียบกับ list ใหม่ และความ stable
หัวข้อที่มีชื่อว่า “การเรียงลำดับ: ใน place เทียบกับ list ใหม่ และความ stable”list.sort() เรียงลำดับ ใน place และคืนค่า None
ส่ง key function เพื่อเรียงตามเกณฑ์ที่กำหนดเอง
numbers = [5, 2, 8, 1, 9, 3]numbers.sort()print(numbers) # [1, 2, 3, 5, 8, 9]
words = ['banana', 'apple', 'cherry']words.sort(key=len)print(words) # ['apple', 'banana', 'cherry']ใช้ sorted(iterable) แทน .sort() เมื่อต้องการ list ใหม่โดยไม่แก้ไขต้นฉบับ — sorted() คืนค่า list เสมอ ในขณะที่ .sort() ไม่เคยคืนค่าอะไรเลย การเอาผลลัพธ์ต่อจาก .sort() ไปใช้ (x = lst.sort() หรือ lst.sort()[:3]) คือ bug จริง เพราะ x หรือเป้าหมายของการ slice จะเป็น None
ทั้ง .sort() และ sorted() เป็น stable: element ที่เท่ากันตาม key จะคงลำดับเดิมไว้ (relative order) นี่สำคัญมากสำหรับการเรียงหลาย key — เรียงตาม key ที่สำคัญน้อยกว่าก่อน แล้วค่อยเรียงตาม key ที่สำคัญที่สุดทับอีกที:
people = [('carol', 30), ('alice', 30), ('dave', 25), ('bob', 25)]people.sort(key=lambda p: p[1])print(people) # [('dave', 25), ('bob', 25), ('carol', 30), ('alice', 30)]ความ stable รับประกันแค่ว่าลำดับ เดิม จะอยู่รอดภายในกลุ่มอายุเดียวกัน — มันไม่ได้เรียงชื่อตามตัวอักษรให้ สำหรับการเรียงหลาย key อย่างแท้จริง ให้รวมทุก key ไว้ใน tuple เดียว:
people.sort(key=lambda p: (p[1], p[0]))print(people) # [('bob', 25), ('dave', 25), ('alice', 30), ('carol', 30)]Aliasing: assignment ไม่เคย copy
หัวข้อที่มีชื่อว่า “Aliasing: assignment ไม่เคย copy”b = a ไม่ได้ สร้าง list ตัวที่สอง — มันแค่ผูกชื่อที่สองเข้ากับ object ตัวเดิม การแก้ไขผ่านชื่อใดชื่อหนึ่งจะเห็นผลผ่านทั้งสองชื่อ:
a = [1, 2, 3]b = ab.append(4)print(a) # [1, 2, 3, 4] — a changed too, even though only b was toucheda[:] (หรือ list(a) หรือ a.copy()) สร้าง shallow copy: list ชั้นนอกใหม่ แต่ทุก element ยังเป็น object ตัวเดิม เหมือนก่อนหน้า ถ้า element เป็นสิ่งที่เปลี่ยนแปลงได้ มันก็ยังถูกแชร์อยู่ดี:
a = [1, [2, 3]]c = a[:]c[1].append(9)print(a) # [1, [2, 3, 9]] — the inner list is shared, so a saw the mutation tooมีแค่ copy.deepcopy() เท่านั้นที่ copy โครงสร้างที่เปลี่ยนแปลงได้ซ้อนกันแบบ recursive ทำให้ไม่มีอะไรถูกแชร์เลย:
import copya = [1, [2, 3]]d = copy.deepcopy(a)d[1].append(100)print(a) # [1, [2, 3]] — untouched; d's inner list is an independent objectกับดัก [[0] * n] * m
หัวข้อที่มีชื่อว่า “กับดัก [[0] * n] * m”[[0] * 3] * 3 คือการคูณ list object ไม่ใช่คูณค่าข้างใน — ทุกแถวของ grid ที่ได้คือ inner list ตัวเดียวกัน ที่ถูก alias ไว้สามครั้ง:
grid = [[0] * 3] * 3grid[0][0] = 1print(grid) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] — every row "changed"วิธีแก้คือใช้ comprehension ซึ่งจะ evaluate [0] * 3 ใหม่ทุกรอบ:
grid = [[0] * 3 for _ in range(3)]grid[0][0] = 1print(grid) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]การเปลี่ยนแปลงใน place เทียบกับการ rebind: += บน list เทียบ tuple
หัวข้อที่มีชื่อว่า “การเปลี่ยนแปลงใน place เทียบกับการ rebind: += บน list เทียบ tuple”+= ดูเหมือน operator เดียว แต่ทำงานต่างกันไปตาม type ทางซ้ายมือ บน list += เรียก __iadd__ ซึ่งขยาย list ใน place — ทุก alias เห็นการเปลี่ยนแปลง บน tuple ไม่มี __iadd__ ใน place ให้ใช้ Python จึงถอยไปใช้ t = t + (...) ซึ่งสร้าง tuple ใหม่ทั้งหมดและ rebind ชื่อ — alias ยังชี้ไปที่ tuple เดิม:
la = [1, 2]lb = lala += [3]print(la, lb) # [1, 2, 3] [1, 2, 3] — same object, both names see it
ta = (1, 2)tb = tata += (3,)print(ta, tb) # (1, 2, 3) (1, 2) — ta now points at a new tuple; tb is unaffectedการทดสอบ membership: in บน list เทียบ set
หัวข้อที่มีชื่อว่า “การทดสอบ membership: in บน list เทียบ set”x in some_list ตรวจ element ทีละตัว — O(n) ตามขนาดของ list ส่วน x in some_set แฮช x แล้วมองหา bucket — O(1) โดยเฉลี่ย ไม่ว่า set จะใหญ่แค่ไหน ทั้งสองแบบได้คำตอบ boolean เหมือนกัน ต่างกันแค่ต้นทุน และต้นทุนนี้จะเห็นชัดก็ต่อเมื่อ collection ใหญ่ขึ้น:
names_list = ['alice', 'bob', 'carol']names_set = {'alice', 'bob', 'carol'}print('bob' in names_list, 'bob' in names_set) # True True — same answer, different costถ้า function ทำ in ซ้ำ ๆ กับ list ที่อาจโตขึ้นมาก การแปลงมันเป็น set (หรือ frozenset ถ้าต้องไม่เปลี่ยนแปลง) คือการแก้ performance ด้วยโค้ดบรรทัดเดียว
Tuples — ลำดับที่เปลี่ยนแปลงไม่ได้
หัวข้อที่มีชื่อว่า “Tuples — ลำดับที่เปลี่ยนแปลงไม่ได้”tuple คล้าย list แต่ไม่สามารถแก้ไขหลังสร้างแล้ว ใช้ tuple เพื่อแสดง record ที่คงที่ซึ่งตำแหน่งมีความหมาย
rgb = (255, 128, 0) # an orange colourpoint = (3.0, 4.0) # a 2-D coordinaterow = (1, 'Alice', 42) # a database row“เปลี่ยนแปลงไม่ได้” หมายความแค่ว่า slot ของ tuple เองจะถูก reassign ไม่ได้ — มันไม่ได้บอกอะไรเลยว่า element ข้างในจะเปลี่ยนแปลงได้หรือไม่:
t = (1, [2, 3])t[0] = 99 # TypeError: 'tuple' object does not support item assignmentt[1].append(4) # allowed — the list inside the tuple can still changeprint(t) # (1, [2, 3, 4])element ที่เปลี่ยนแปลงได้นี้ยังส่งผลต่อการ hash ด้วย: tuple จะ hashable — และใช้เป็น dict key หรือ set member ได้ — ก็ต่อเมื่อ ทุก element ข้างในเป็น hashable:
d = {t: 'value'} # TypeError: cannot use 'tuple' as a dict key (unhashable type: 'list')tuple ที่มีแต่ element hashable ใช้เป็น key ได้ปกติ:
point = (1, 2)by_point = {point: 'ok'}Tuple unpacking
หัวข้อที่มีชื่อว่า “Tuple unpacking”Python อนุญาตให้ unpack tuple (หรือ iterable ใด ๆ) ลงในตัวแปรโดยตรง
จำนวนตัวแปรต้องตรงกับความยาว ยกเว้นใช้ * เพื่อรับส่วนที่เหลือ
coords = (10.5, 20.3, 30.1)x, y, z = coordsprint(x, y, z) # 10.5 20.3 30.1
first, *rest = [1, 2, 3, 4, 5]print(first) # 1print(rest) # [2, 3, 4, 5]การเข้าถึง field ของ tuple ด้วย index — หรือด้วยชื่อ
หัวข้อที่มีชื่อว่า “การเข้าถึง field ของ tuple ด้วย index — หรือด้วยชื่อ”โดยไม่มีชื่อ field ของ tuple จะถูกเข้าถึงด้วย integer index กำหนด constant เพื่อให้อ่านง่าย:
RGB = (255, 128, 0)RED = 0GREEN = 1BLUE = 2
print(RGB[RED]) # 255print(RGB[GREEN]) # 128print(RGB[BLUE]) # 0typing.NamedTuple ให้ tuple แบบเดียวกันทุกอย่าง (index access, unpacking, equality, hashability) บวกกับการเข้าถึง attribute แบบมีชื่อ ในโค้ดประมาณสามบรรทัด ส่วน @dataclass หน้าตาคล้ายกันแต่เป็น object คนละแบบ — เปลี่ยนแปลงได้โดย default และใช้เป็น dict key ไม่ได้เว้นแต่จะตั้งค่าเอง:
from typing import NamedTuplefrom dataclasses import dataclass
class PointNT(NamedTuple): x: int y: int
@dataclassclass PointDC: x: int y: intpnt = PointNT(1, 2)pnt.x = 99 # AttributeError: can't set attribute — still a tuple, still immutablepdc = PointDC(1, 2)pdc.x = 99 # allowed — a dataclass instance is a plain, mutable objectใช้ NamedTuple เมื่อค่าคือ record เล็ก ๆ คงที่ และ hashable (ปลอดภัยพอจะเป็น dict key ปลอดภัยพอจะส่งต่อโดยไม่ต้องกังวลว่าใครจะมาแก้) ใช้ @dataclass เมื่อต้องการ object ที่เปลี่ยนแปลงได้พร้อมพฤติกรรมแบบ class (method, inheritance, field(default_factory=...))
นอกเหนือจาก list: deque สำหรับทั้งสองปลาย
หัวข้อที่มีชื่อว่า “นอกเหนือจาก list: deque สำหรับทั้งสองปลาย”insert(0, x) และ pop(0) ของ list คือ O(n) — ทุก element ที่เหลือต้องขยับ collections.deque คือ double-ended queue ที่ append/pop จากทั้งสองปลายเป็น O(1) แลกมากับการเข้าถึงแบบสุ่มด้วย index เป็น O(n) (slice ไม่ได้):
from collections import dequedq = deque([1, 2, 3])dq.appendleft(0)dq.append(4)print(dq) # deque([0, 1, 2, 3, 4])ใช้ deque สำหรับ queue, sliding window, และ undo/history buffer เก็บ list ไว้เมื่องานหลักคือ index, slice, หรือ append ที่ท้ายเท่านั้น
ตัวอย่างรันได้
หัวข้อที่มีชื่อว่า “ตัวอย่างรันได้”# --- list mutation and sorting ---fruits = ['apple', 'banana', 'cherry']fruits.append('date')fruits.insert(1, 'avocado')print('after insert:', fruits)
fruits.remove('banana')print('after remove:', fruits)
numbers = [5, 2, 8, 1, 9, 3]numbers.sort()print('sorted:', numbers)
words = ['banana', 'apple', 'cherry']words.sort(key=len)print('by length:', words)
# --- tuple unpacking ---coords = (10.5, 20.3, 30.1)x, y, z = coordsprint('unpacked:', x, y, z)
first, *rest = [1, 2, 3, 4, 5]print('first:', first)print('rest:', rest)
# --- named fields via index constants ---RGB = (255, 128, 0)print('red:', RGB[0], 'green:', RGB[1], 'blue:', RGB[2])Loading Python runtime (first run only)…
playground ด้านล่างคือที่ที่หัวข้อความลึกด้านบนรันจริง — aliasing เทียบ shallow copy เทียบ deep copy, กับดัก [[0]*3]*3, .sort() เทียบ sorted() พร้อมความ stable, += บน list เทียบ tuple, element ที่เปลี่ยนแปลงได้ของ tuple ที่ทำให้ hash ไม่ได้, NamedTuple เทียบ dataclass, และ deque:
import copy
# --- aliasing vs shallow copy vs deep copy ---a = [1, [2, 3]]b = a # alias: same objectc = a[:] # shallow copy: new outer list, same inner listd = copy.deepcopy(a) # deep copy: fully independent
b.append(4)c[1].append(99)d[1].append(100)
print('a:', a)print('b:', b)print('c:', c)print('d:', d)
# --- [[0]*3]*3 shares rows ---grid = [[0] * 3] * 3grid[0][0] = 1print('broken grid:', grid)
fixed = [[0] * 3 for _ in range(3)]fixed[0][0] = 1print('fixed grid:', fixed)
# --- list.sort() returns None; sorted() returns a new list ---nums = [3, 1, 2]result = nums.sort()print('sort() return value:', result)print('nums after sort():', nums)
nums2 = [3, 1, 2]nums2_sorted = sorted(nums2)print('sorted() return value:', nums2_sorted)print('nums2 unchanged:', nums2)
# --- stability + multi-key sort ---people = [('carol', 30), ('alice', 30), ('dave', 25), ('bob', 25)]people.sort(key=lambda p: p[1])print('stable by age:', people)
people.sort(key=lambda p: (p[1], p[0]))print('multi-key age,name:', people)
# --- membership: list O(n) vs set O(1), same result different cost ---names_list = ['alice', 'bob', 'carol']names_set = {'alice', 'bob', 'carol'}print('in list:', 'bob' in names_list, ' in set:', 'bob' in names_set)
# --- += mutates a list in place (shared); += on a tuple rebinds (not shared) ---la = [1, 2]lb = lala += [3]print('list +=:', la, lb)
ta = (1, 2)tb = tata += (3,)print('tuple +=:', ta, tb)
# --- tuple is immutable but its mutable elements are not ---t = (1, [2, 3])try: t[0] = 99except TypeError as e: print(type(e).__name__ + ':', e)t[1].append(4)print('tuple with mutated inner list:', t)
# --- that same tuple can't be a dict key because it contains a list ---try: d2 = {t: 'value'}except TypeError as e: print(type(e).__name__ + ':', e)
frozen_point = (1, 2)d3 = {frozen_point: 'ok'}print('hashable tuple as key:', d3)
# --- NamedTuple vs dataclass, three lines each ---from typing import NamedTuplefrom dataclasses import dataclass
class PointNT(NamedTuple): x: int y: int
@dataclassclass PointDC: x: int y: int
pnt = PointNT(1, 2)pdc = PointDC(1, 2)print('namedtuple:', pnt, pnt.x, pnt[0], pnt == PointNT(1, 2))print('dataclass:', pdc, pdc.x, pdc == PointDC(1, 2))try: pnt.x = 99except AttributeError as e: print(type(e).__name__ + ':', e)pdc.x = 99print('dataclass is mutable:', pdc)
# --- deque: O(1) at both ends vs list's O(n) insert(0, ...) ---from collections import dequedq = deque([1, 2, 3])dq.appendleft(0)dq.append(4)print('deque:', dq)lst = [1, 2, 3]lst.insert(0, 0)print('list insert(0, ...):', lst)Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart LR
subgraph Names
a["a"]
b["b = a"]
c["c = a[:]"]
d["d = deepcopy(a)"]
end
subgraph Objects["heap objects"]
OUTER1["list #1 [1, ref, 4]"]
OUTER2["list #2 (shallow copy) [1, ref]"]
INNER["inner list #3 [2, 3, 99]"]
OUTER3["list #4 (deep copy) [1, ref]"]
INNER2["inner list #5 [2, 3, 100]"]
end
a --> OUTER1
b --> OUTER1
c --> OUTER2
OUTER1 --> INNER
OUTER2 --> INNER
d --> OUTER3
OUTER3 --> INNER2 Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
list (เปลี่ยนแปลงได้) | append/sort/แก้ไขใน place โดยไม่ต้อง copy | ทุก alias แชร์การเปลี่ยนแปลง — แก้สิ่งที่ไม่ได้ตั้งใจแก้ได้ง่าย |
tuple (เปลี่ยนแปลงไม่ได้) | hash ได้ แชร์ได้ปลอดภัย และใช้เป็น dict key หรือ set member ได้ | ขนาดและลำดับคงที่; “เปลี่ยนแปลงไม่ได้” ปกป้องแค่ slot ของ tuple เอง ไม่ปกป้อง element ข้างในที่เปลี่ยนแปลงได้ |
list.sort() (ใน place) | ไม่ต้อง allocate list ใหม่ | คืนค่า None — เอาไปต่อ (.sort()[:3]) จะได้ TypeError |
sorted() (list ใหม่) | ต้นฉบับไม่ถูกแตะต้อง เอาไปต่อได้อย่างปลอดภัย | allocate list ใหม่เสมอ แม้จะแค่ดูผลชั่วคราว |
deque | append/pop ที่ทั้งสองปลายเป็น O(1) | เข้าถึงแบบสุ่มด้วย index เป็น O(n), slice ไม่ได้ |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
b = acopy list” — ความจริง: มันแค่ผูกชื่อที่สองเข้ากับ object เดิม การแก้ไขผ่านbเปลี่ยนสิ่งที่aเห็นด้วย - “
a[:]ทำ copy ที่เป็นอิสระเต็มที่” — ความจริง:a[:]คือ shallow copy — element ที่เปลี่ยนแปลงได้ที่ซ้อนอยู่ (เช่น inner list) ยังถูกแชร์อยู่ - “
list.sort()คืนค่า list ที่เรียงแล้ว” — ความจริง:.sort()คืนค่าNoneเสมอ มีแค่sorted()เท่านั้นที่คืนค่า list ใหม่ - “tuple ปลอดภัยเสมอที่จะใช้เป็น dict key” — ความจริง: ปลอดภัยก็ต่อเมื่อทุก element ข้างในเป็น hashable — tuple ที่มี list อยู่ข้างในจะ raise
TypeError: unhashable type: 'list' - “
+=ทำงานเหมือนกันบน list และ tuple” — ความจริง: บน list มันแก้ไขใน place (alias เห็นด้วย); บน tuple มันสร้าง tuple ใหม่และ rebind ชื่อ (alias ไม่เห็นด้วย)
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”- function ที่รับ list มาแล้วแก้ไขใน place และ
returnมันด้วยหรือเปล่า? ตรวจทุกจุดที่เรียกใช้ว่าผู้เรียกคาดหวังให้ list ต้นฉบับไม่ถูกแตะต้องหรือไม่ - โค้ดเอาผลลัพธ์ต่อจาก
.sort()โดยตรงหรือเปล่า (.sort()[:3],x = lst.sort())?.sort()คืนค่าNoneเสมอ — บรรทัดนั้นคือ bug - grid แบบ 2 มิติถูกสร้างด้วย
[[default] * cols] * rowsหรือเปล่า? ทุกแถวคือ list object ตัวเดียวกัน — มองหารูปแบบ comprehension ([[default] * cols for _ in range(rows)]) แทน +=ปรากฏบนสิ่งที่อาจเป็น tuple ในโค้ดที่สันนิษฐานว่า object ต้นฉบับถูกแก้ไขใน place หรือเปล่า? tuple จะ rebind แทนที่จะแก้ไขใน place- tuple ถูกใช้เป็น dict key หรือ set element โดยไม่ตรวจว่าทุก element ข้างในเป็น hashable หรือเปล่า?
🐛 หา bug ในโค้ดนี้
def top_three(scores): return scores.sort(reverse=True)[:3]
print(top_three([42, 7, 99, 3, 56]))เฉลย
list.sort() เรียงใน place และคืนค่า None เสมอ — มันไม่ใช่ sorted() ดังนั้น scores.sort(reverse=True) คืนค่า None และ None[:3] จะ raise TypeError: 'NoneType' object is not subscriptable ตรวจสอบแล้วบน Python 3.14: traceback จบด้วยข้อความนี้เป๊ะ ๆ วิธีแก้คือ return sorted(scores, reverse=True)[:3] (สร้าง list ใหม่แล้วค่อย slice) หรือแยกเป็นสอง statement: scores.sort(reverse=True); return scores[:3]