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

copy กับ deepcopy ใน Python: ทำไม "สำเนา" ถึงไม่ใช่สำเนาจริง

ปานกลาง20 นาที5 แบบฝึกหัด70 XP

บทเรียนนี้แปลจากต้นฉบับภาษาอังกฤษRead in EnglishLeer en españolЧитать на русскомLer em portuguêsแจ้งข้อผิดพลาดในการแปล

0/5 แบบฝึกหัด

ลองนึกว่าคุณมีแฟ้มที่เต็มไปด้วยรูปถ่าย คุณทำสำเนาแฟ้มนั้นแล้วให้เพื่อน แต่มีจุดที่ต้องระวัง: มีแค่ตัวแฟ้มเท่านั้นที่เป็นของใหม่ รูปถ่ายข้างในยังเป็นรูปใบเดิมจริง ๆ ที่ทั้งสองแฟ้มใช้ร่วมกัน ถ้าเพื่อนวาดหนวดลงบนรูปใบหนึ่ง รูปของคุณก็จะมีหนวดด้วย

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

ในบทเรียนนี้ คุณจะได้เรียนรู้ความแตกต่างระหว่างการกำหนดค่า (assignment) สำเนาแบบตื้น (shallow copy) และสำเนาแบบลึก (deep copy) เมื่อจบบทเรียน คุณจะรู้ชัดว่าควรใช้แบบไหนเมื่อไร และจะไม่โดนบั๊ก "การอ้างอิงร่วมกัน" (shared reference) เล่นงานอีก

ทำไมการเปลี่ยน "สำเนา" จึงเปลี่ยนต้นฉบับไปด้วย

มาเริ่มจากเรื่องที่ทำให้แปลกใจบ่อยที่สุด เมื่อเขียน b = a คุณ ไม่ได้ สร้างสำเนา แต่กำลังสร้างชื่อที่สองให้ออบเจกต์ตัวเดิม ทั้ง a และ b ชี้ไปที่ข้อมูลเดียวกันในหน่วยความจำ

การกำหนดค่าสร้างชื่อแฝง ไม่ใช่สำเนา
a = [1, 2, 3]
b = a # NOT a copy - just a second name
b.append(4)
print(a) # [1, 2, 3, 4] - a changed too!
print(b) # [1, 2, 3, 4]
ผลลัพธ์
[1, 2, 3, 4]
[1, 2, 3, 4]

ที่เป็นแบบนี้เพราะ a และ b เป็นป้ายสองป้ายที่ติดอยู่บนกล่องใบเดียวกัน เมื่อใส่ของใหม่ลงในกล่องผ่านป้าย b คุณก็จะเห็นของนั้นเมื่อดูผ่านป้าย a ด้วย

คุณตรวจได้ว่าตัวแปรสองตัวชี้ไปที่ออบเจกต์เดียวกันหรือไม่ โดยใช้ตัวดำเนินการ is หรือดูค่า id() ของตัวแปรทั้งสอง

ตรวจตัวตนของออบเจกต์ด้วย is และ id()
a = [1, 2, 3]
b = a
print(a is b) # True - same object
print(id(a)) # Same number...
print(id(b)) # ...as this one
ผลลัพธ์
True
80036304
80036304

สำเนาแบบตื้นคืออะไร

สำเนาแบบตื้นสร้างคอนเทนเนอร์ชั้นนอกขึ้นมาใหม่ แต่ ไม่ได้ คัดลอกออบเจกต์ที่อยู่ข้างใน ลิสต์ใหม่แยกเป็นอิสระในระดับบนสุด แต่ออบเจกต์ที่ซ้อนอยู่ข้างในยังใช้ร่วมกับต้นฉบับ

การสร้างสำเนาแบบตื้นของลิสต์ทำได้หลายวิธี

สามวิธีในการสร้างสำเนาแบบตื้นของลิสต์
original = [1, 2, 3]
# Three ways to shallow copy a list
copy1 = original.copy() # .copy() method
copy2 = original[:] # slice from start to end
copy3 = list(original) # list() constructor
# They are all independent copies
copy1.append(99)
print(original) # [1, 2, 3] - unchanged!
print(copy1) # [1, 2, 3, 99]
ผลลัพธ์
[1, 2, 3]
[1, 2, 3, 99]

สำหรับดิกชันนารี ให้ใช้ .copy() หรือ dict()

สร้างสำเนาแบบตื้นของดิกชันนารี
original = {'name': 'Alice', 'age': 25}
copy1 = original.copy() # .copy() method
copy2 = dict(original) # dict() constructor
copy1['age'] = 30
print(original) # {'name': 'Alice', 'age': 25} - unchanged
print(copy1) # {'name': 'Alice', 'age': 30}
ผลลัพธ์
{'name': 'Alice', 'age': 25}
{'name': 'Alice', 'age': 30}

ดูเหมือนจะสมบูรณ์แบบ แต่นี่คือกับดัก สำเนาแบบตื้นคัดลอกลงไปแค่ชั้นเดียว ถ้าลิสต์ของคุณมีลิสต์อื่น (หรือดิกชันนารี) อยู่ข้างใน ออบเจกต์ชั้นในเหล่านั้นยังใช้ร่วมกันอยู่

กับดักของสำเนาแบบตื้นกับข้อมูลที่ซ้อนกัน
# Shallow copy with nested data - the trap!
original = [[1, 2], [3, 4], [5, 6]]
shallow = original.copy()
# Modifying the outer list is fine
shallow.append([7, 8])
print(len(original)) # 3 - unaffected
print(len(shallow)) # 4 - only shallow changed
# But modifying an INNER list affects both!
shallow[0].append(99)
print(original[0]) # [1, 2, 99] - original changed too!
print(shallow[0]) # [1, 2, 99]
ผลลัพธ์
3
4
[1, 2, 99]
[1, 2, 99]
สำเนาแบบตื้น: ลิสต์ชั้นในใช้ร่วมกัน
original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0].append(99)
print(original)  # [[1, 2, 99], [3, 4]]
สำเนาแบบลึก: ทุกอย่างแยกเป็นอิสระ
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0].append(99)
print(original)  # [[1, 2], [3, 4]]

สำเนาแบบลึกคืออะไร และต้องใช้เมื่อไร

สำเนาแบบลึกสร้างโคลนที่แยกเป็นอิสระโดยสมบูรณ์ โดยคัดลอกออบเจกต์ชั้นนอก และคัดลอกทุกออบเจกต์ที่เปลี่ยนแปลงได้ซึ่งอยู่ข้างในแบบเรียกซ้ำ (recursive) ไม่ว่าจะซ้อนกันลึกแค่ไหน สำเนาจะไม่ใช้สิ่งใดที่แก้ไขได้ร่วมกับต้นฉบับเลย มีเพียงค่าที่เปลี่ยนแปลงไม่ได้ เช่น ตัวเลขและสตริง ที่ถูกนำกลับมาใช้ซ้ำ

การสร้างสำเนาแบบลึกต้องใช้โมดูล copy จากไลบรารีมาตรฐานของ Python

สำเนาแบบลึกสร้างโคลนที่แยกเป็นอิสระทั้งหมด
import copy
original = [[1, 2], [3, 4], [5, 6]]
deep = copy.deepcopy(original)
# Modify the deep copy
deep[0].append(99)
deep.append([7, 8])
print(original) # [[1, 2], [3, 4], [5, 6]] - completely unchanged!
print(deep) # [[1, 2, 99], [3, 4], [5, 6], [7, 8]]
ผลลัพธ์
[[1, 2], [3, 4], [5, 6]]
[[1, 2, 99], [3, 4], [5, 6], [7, 8]]

สำเนาแบบลึกใช้กับดิกชันนารีได้ด้วย รวมถึงโครงสร้างที่ซ้อนกันหลายชั้น

สำเนาแบบลึกกับดิกชันนารีและลิสต์ที่ซ้อนกัน
import copy
user = {
'name': 'Alice',
'scores': [90, 85, 92],
'profile': {'level': 5, 'badges': ['star', 'gold']},
}
clone = copy.deepcopy(user)
# Modify the clone at every level
clone['name'] = 'Bob'
clone['scores'].append(100)
clone['profile']['badges'].append('diamond')
print(user['name']) # Alice
print(user['scores']) # [90, 85, 92]
print(user['profile']['badges']) # ['star', 'gold']
print(clone['name']) # Bob
print(clone['scores']) # [90, 85, 92, 100]
print(clone['profile']['badges']) # ['star', 'gold', 'diamond']
ผลลัพธ์
Alice
[90, 85, 92]
['star', 'gold']
Bob
[90, 85, 92, 100]
['star', 'gold', 'diamond']

เบื้องหลัง: การอ้างอิงใน Python ทำงานอย่างไร

การจะเข้าใจการคัดลอกอย่างแท้จริง ต้องเข้าใจก่อนว่า Python เก็บข้อมูลอย่างไร ข้อมูลทุกชิ้นอยู่ที่ไหนสักแห่งในหน่วยความจำในรูปของ ออบเจกต์ ส่วนตัวแปรเป็นเพียงชื่อที่ อ้างอิง (ชี้ไปยัง) ออบเจกต์เหล่านั้น

การกำหนดค่าคัดลอกการอ้างอิง สำเนาแบบตื้นสร้างคอนเทนเนอร์ใหม่ที่มีการอ้างอิงชุดเดิมอยู่ข้างใน ส่วนสำเนาแบบลึกสร้างคอนเทนเนอร์ใหม่ที่การอ้างอิงข้างในชี้ไปยังสำเนาใหม่ของทุกออบเจกต์ที่เปลี่ยนแปลงได้ซึ่งอยู่ข้างใน

เห็นภาพการอ้างอิงด้วย is
import copy
inner = [1, 2, 3]
original = [inner, inner] # Both slots point to the same inner list
shallow = copy.copy(original)
deep = copy.deepcopy(original)
print(original[0] is original[1]) # True - same inner list
print(shallow[0] is original[0]) # True - shallow shares inner objects
print(deep[0] is original[0]) # False - deep made a new copy
ผลลัพธ์
True
True
False

ตารางสรุปทั้งสามวิธีมีดังนี้

สรุป: การกำหนดค่า สำเนาแบบตื้น และสำเนาแบบลึก
# Summary:
# ─────────────────────────────────────────────────────
# Method | New outer? | New inner? | Safe for nested?
# ─────────────────────────────────────────────────────
# b = a | No | No | No
# b = a.copy() | Yes | No | No
# b = deepcopy(a) | Yes | Yes | Yes
# ─────────────────────────────────────────────────────
print("Assignment: same outer, same inner")
print("Shallow copy: new outer, same inner")
print("Deep copy: new outer, new inner")
ผลลัพธ์
Assignment:   same outer, same inner
Shallow copy: new outer, same inner
Deep copy:    new outer, new inner

ควรใช้การคัดลอกแต่ละแบบเมื่อไร

ใช้ การกำหนดค่า (b = a) เมื่อตั้งใจให้มีสองชื่อสำหรับข้อมูลเดียวกัน กรณีนี้พบบ่อยเมื่อส่งข้อมูลให้ฟังก์ชัน ซึ่งปกติคุณต้องการให้ฟังก์ชันทำงานกับต้นฉบับ

ใช้ สำเนาแบบตื้น เมื่อมีลิสต์หรือดิกชันนารีชั้นเดียว (ไม่มีออบเจกต์ที่เปลี่ยนแปลงได้ซ้อนอยู่) และต้องการสำเนาที่แยกเป็นอิสระ ซึ่งครอบคลุมกรณีส่วนใหญ่ในการใช้งานทั่วไป

ใช้ สำเนาแบบลึก เมื่อมีออบเจกต์ที่เปลี่ยนแปลงได้ซ้อนกัน (ลิสต์ของลิสต์ ดิกชันนารีที่มีค่าเป็นลิสต์ ฯลฯ) และต้องการโคลนที่แยกเป็นอิสระทั้งหมด ซึ่งจำเป็นมากเมื่อต้องการแก้ไขสำเนาโดยไม่มีความเสี่ยงที่จะกระทบต้นฉบับ


แบบฝึกหัด

ทายผลลัพธ์: ชื่อแฝงจากการกำหนดค่า
ทายผลลัพธ์

อ่านโค้ดอย่างละเอียด โค้ดจะแสดงผลอะไร อย่าลืมว่า = กับลิสต์จะสร้างชื่อแฝง ไม่ใช่สำเนา

พิมพ์ผลลัพธ์ให้ตรงทุกตัวอักษร

a = [10, 20, 30]
b = a
b[1] = 99
print(a)
print(a is b)
สร้างสำเนาที่แยกเป็นอิสระ
แก้บั๊ก

โค้ดด้านล่างพยายามสำรองลิสต์ต้นฉบับไว้ก่อนแก้ไข แต่ข้อมูลสำรองกลับเปลี่ยนตามไปด้วย ให้แก้โค้ดเพื่อให้ข้อมูลสำรองไม่เปลี่ยนแปลง

ผลลัพธ์ที่ต้องการ:

Original: [1, 2, 3, 4]
Backup: [1, 2, 3]
original = [1, 2, 3]
backup = original
original.append(4)
print(f"Original: {original}")
print(f"Backup: {backup}")
ทายผลลัพธ์: กับดักของสำเนาแบบตื้น
ทายผลลัพธ์

โค้ดนี้ใช้สำเนาแบบตื้นกับข้อมูลที่ซ้อนกัน อ่านอย่างละเอียดแล้วทายว่าจะแสดงผลอะไร

พิมพ์ผลลัพธ์ให้ตรงทุกตัวอักษร

original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0].append(5)
print(original)
print(shallow)
คัดลอกข้อมูลที่ซ้อนกันอย่างปลอดภัย
เขียนโค้ด

สร้างสำเนาของข้อมูล original ที่แยกเป็นอิสระทั้งหมดด้วยสำเนาแบบลึก จากนั้นเพิ่มคะแนน 100 เข้าไปในลิสต์ scores ของสำเนา แสดงคะแนนของต้นฉบับและคะแนนของสำเนาคนละบรรทัด เพื่อพิสูจน์ว่าทั้งสองแยกจากกัน

original = {
'name': 'Alice',
'scores': [85, 90, 78],
}
# Create a deep copy
# Add 100 to the copy's scores
# Print original scores, then copy's scores
แก้บั๊กสถานะของเกม
แก้บั๊ก

เกมนี้บันทึกจุดเซฟ (checkpoint) ของสถานะผู้เล่นไว้ จากนั้นผู้เล่นเก็บไอเทมชิ้นหนึ่ง แต่เมื่อโหลดจุดเซฟ ไอเทมนั้นกลับอยู่ในนั้นแล้ว ให้แก้โค้ดเพื่อให้จุดเซฟเป็นภาพสถานะที่แยกเป็นอิสระจริง ๆ

ผลลัพธ์ที่ต้องการ:

Checkpoint inventory: ['sword', 'shield']
Current inventory: ['sword', 'shield', 'potion']
player = {
'name': 'Hero',
'hp': 100,
'inventory': ['sword', 'shield'],
}
# Save checkpoint
checkpoint = player.copy()
# Player picks up a potion
player['inventory'].append('potion')
print(f"Checkpoint inventory: {checkpoint['inventory']}")
print(f"Current inventory: {player['inventory']}")

บทเรียนที่เกี่ยวข้อง

บทเรียนนี้มีประโยชน์หรือไม่