Notes & software courses · Free to learn
Aph's Blog
On this page

Mutability, Reference & Copy

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

เข้าใจว่าตัวแปรชี้ไปที่ object อย่างไร — รากของบั๊ก Python ที่เจอบ่อยที่สุดเมื่อเริ่มเขียนจริงจัง

นี่คือหัวข้อที่อธิบายบั๊กลึกลับจำนวนมาก: "ทำไมแก้ list หนึ่งแล้วอีก list เปลี่ยนตาม?" คำตอบอยู่ที่การเข้าใจว่าใน Python ตัวแปรไม่ได้ "เก็บค่า" แต่ "ชี้ไปที่ object" เมื่อเข้าใจเรื่องนี้ คุณจะเลิกเจอบั๊กประหลาดไปครึ่งหนึ่ง

ตัวแปรคือป้ายชื่อ ไม่ใช่กล่อง

คิดว่าตัวแปรเป็น "ป้ายชื่อ" ที่แปะไปบน object ในหน่วยความจำ การทำ b = a ไม่ได้ก็อป object แต่เอาป้าย b ไปแปะบน object เดียวกับ a

python
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
python
# 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 เดียวกันไหม (ที่อยู่ในหน่วยความจำเดียวกัน)" — สองอย่างนี้คนละเรื่อง

python
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 เดียวกัน)
ใช้ is เฉพาะกับ None

เปรียบเทียบค่าทั่วไปใช้ == เสมอ แต่เช็ค None ให้ใช้ is None / is not None เพราะ None เป็น object เดี่ยวในระบบ (singleton) เป็นสำนวนมาตรฐานของ Python — อย่าเขียน x == None

ส่ง mutable เข้าฟังก์ชัน = ฟังก์ชันแก้ของจริงได้

เพราะส่ง reference ไป ฟังก์ชันที่ append/แก้ list จึงเปลี่ยน list ของผู้เรียกจริง — เป็นทั้งฟีเจอร์และกับดัก

python
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 items

copy แบบตื้น vs ลึก (deepcopy)

การก็อปธรรมดา (.copy() หรือ list(x)) เป็น shallow copy — ก็อปชั้นนอกแต่ object ข้างในยังแชร์กัน ถ้ามี list ซ้อน list ต้องใช้ copy.deepcopy

python
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]]  ← ไม่กระทบแล้ว
เชื่อมกลับ mutable default argument

จำกับดัก 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