On this page
Mutability, Reference & Copy
เข้าใจว่าตัวแปรชี้ไปที่ object อย่างไร — รากของบั๊ก Python ที่เจอบ่อยที่สุดเมื่อเริ่มเขียนจริงจัง
นี่คือหัวข้อที่อธิบายบั๊กลึกลับจำนวนมาก: "ทำไมแก้ list หนึ่งแล้วอีก list เปลี่ยนตาม?" คำตอบอยู่ที่การเข้าใจว่าใน Python ตัวแปรไม่ได้ "เก็บค่า" แต่ "ชี้ไปที่ object" เมื่อเข้าใจเรื่องนี้ คุณจะเลิกเจอบั๊กประหลาดไปครึ่งหนึ่ง
ตัวแปรคือป้ายชื่อ ไม่ใช่กล่อง
คิดว่าตัวแปรเป็น "ป้ายชื่อ" ที่แปะไปบน object ในหน่วยความจำ การทำ b = a ไม่ได้ก็อป object แต่เอาป้าย b ไปแปะบน object เดียวกับ a
a = [1, 2, 3]
b = a # b ชี้ไปที่ list เดียวกับ a (ไม่ได้ก็อป)
b.append(4)
print(a) # [1, 2, 3, 4] ← a เปลี่ยนตามด้วย!
print(b) # [1, 2, 3, 4]
print(a is b) # True (เป็น object เดียวกัน)mutable vs immutable
object แบ่งเป็น 2 ประเภท: เปลี่ยนค่าข้างในได้ (mutable) กับเปลี่ยนไม่ได้ (immutable) ประเภทนี้กำหนดว่าจะเจอปัญหา aliasing ข้างบนหรือไม่
| ประเภท | เปลี่ยนข้างในได้? | ตัวอย่าง |
|---|---|---|
| mutable | ได้ | list, dict, set |
| immutable | ไม่ได้ | int, float, str, tuple, bool |
# immutable: การ 'แก้' จริง ๆ คือสร้าง object ใหม่
x = 5
y = x
y = y + 1 # สร้าง int ใหม่ ไม่กระทบ x
print(x, y) # 5 6 (x ไม่เปลี่ยน)
# mutable: แก้ object เดิม กระทบทุกป้ายที่ชี้อยู่
list_a = [1, 2]
list_b = list_a
list_b.append(3)
print(list_a) # [1, 2, 3] (กระทบ)is กับ == ต่างกัน
== ถามว่า "ค่าเท่ากันไหม" ส่วน is ถามว่า "เป็น object เดียวกันไหม (ที่อยู่ในหน่วยความจำเดียวกัน)" — สองอย่างนี้คนละเรื่อง
a = [1, 2, 3]
b = [1, 2, 3] # list คนละตัว ค่าเท่ากัน
c = a # ป้ายชี้ตัวเดียวกับ a
print(a == b) # True (ค่าเท่ากัน)
print(a is b) # False (คนละ object)
print(a is c) # True (object เดียวกัน)เปรียบเทียบค่าทั่วไปใช้ == เสมอ แต่เช็ค None ให้ใช้ is None / is not None เพราะ None เป็น object เดี่ยวในระบบ (singleton) เป็นสำนวนมาตรฐานของ Python — อย่าเขียน x == None
ส่ง mutable เข้าฟังก์ชัน = ฟังก์ชันแก้ของจริงได้
เพราะส่ง reference ไป ฟังก์ชันที่ append/แก้ list จึงเปลี่ยน list ของผู้เรียกจริง — เป็นทั้งฟีเจอร์และกับดัก
def add_zero(items):
items.append(0) # แก้ list ตัวจริงของ caller
nums = [1, 2, 3]
add_zero(nums)
print(nums) # [1, 2, 3, 0] ← ถูกแก้!
# ถ้าไม่อยากให้กระทบ ให้ก็อปก่อน
def add_zero_safe(items):
items = items.copy() # ทำงานบนสำเนา
items.append(0)
return itemscopy แบบตื้น vs ลึก (deepcopy)
การก็อปธรรมดา (.copy() หรือ list(x)) เป็น shallow copy — ก็อปชั้นนอกแต่ object ข้างในยังแชร์กัน ถ้ามี list ซ้อน list ต้องใช้ copy.deepcopy
import copy
original = [[1, 2], [3, 4]]
shallow = original.copy() # ก็อปชั้นนอกเท่านั้น
shallow[0].append(99) # แก้ list ข้างใน
print(original) # [[1, 2, 99], [3, 4]] ← กระทบ! (ข้างในแชร์กัน)
deep = copy.deepcopy(original) # ก็อปทุกชั้น
deep[0].append(0)
print(original) # [[1, 2, 99], [3, 4]] ← ไม่กระทบแล้วจำกับดัก def f(items=[]) ในหัวข้อ *args ได้ไหม — ตอนนี้คุณเข้าใจสาเหตุแล้ว: list default ถูกสร้าง object เดียวตอนนิยามฟังก์ชัน แล้วทุกการเรียกใช้ "ป้ายเดียวกัน" ค่าจึงค้าง ทางแก้คือใช้ None แล้วสร้าง list ใหม่ข้างใน (object ใหม่ทุกครั้ง)
สรุปหัวข้อนี้
- ตัวแปร = ป้ายชื่อที่ชี้ไป object; b = a ไม่ได้ก็อป
- mutable (list/dict/set) แก้ข้างในได้ → เจอ aliasing; immutable (int/str/tuple) ไม่เจอ
- == เทียบค่า, is เทียบว่าเป็น object เดียวกัน — ใช้ is กับ None เท่านั้น
- shallow copy ยังแชร์ object ข้างใน; ซ้อนหลายชั้นต้อง copy.deepcopy
1) ทำนายผล: a=[1,2]; b=a; b.append(3); print(a) แล้วรันเช็ค 2) เขียนฟังก์ชันที่เผลอแก้ list ของ caller แล้วแก้ให้ปลอดภัยด้วย copy 3) สร้าง list ซ้อน list ก็อปแบบ shallow แล้ว deepcopy เทียบผล 4) อธิบายว่าทำไม x is None ดีกว่า x == None