โมดูล `copy` ของ Python

โมดูล `copy` ของ Python

บทความนี้อธิบายโมดูล copy ของ Python

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

YouTube Video

โมดูล copy ของ Python

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

พื้นฐาน: การคัดลอกแบบตื้นคืออะไร?

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

 1# Example: shallow copy with lists and dicts
 2import copy
 3
 4# A nested list
 5original = [1, [2, 3], 4]
 6shallow = copy.copy(original)
 7
 8# Mutate nested element
 9shallow[1].append(99)
10
11print("original:", original)  # nested change visible in original
12print("shallow: ", shallow)

ในโค้ดนี้ ลิสต์ระดับบนสุดถูกทำสำเนา แต่ลิสต์ย่อยภายในถูกแชร์โดยการอ้างอิง ดังนั้น shallow[1].append(99) จึงสะท้อนผลไปยัง original ด้วย นี่เป็นพฤติกรรมทั่วไปของการคัดลอกแบบตื้น

การคัดลอกแบบลึกคืออะไร?

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

 1# Example: deep copy with nested structures
 2import copy
 3
 4original = [1, [2, 3], {'a': [4, 5]}]
 5deep = copy.deepcopy(original)
 6
 7# Mutate nested element of the deep copy
 8deep[1].append(100)
 9deep[2]['a'].append(200)
10
11print("original:", original)  # original remains unchanged
12print("deep:    ", deep)

ในตัวอย่างนี้ การเปลี่ยนแปลงที่ deep จะไม่กระทบต่อ original deepcopy คัดลอกกราฟของออบเจ็กต์ทั้งหมด ส่งผลให้ได้สำเนาที่เป็นอิสระ

จะตัดสินใจใช้อย่างไหนดี

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

ในทางกลับกัน หากการทำสำเนาเฉพาะออบเจ็กต์ระดับบนสุดเพียงพอ และคุณให้ความสำคัญกับความเร็วและประสิทธิภาพหน่วยความจำ copy (การคัดลอกแบบตื้น) จะเหมาะสมกว่า

  • ต้องการหลีกเลี่ยงไม่ให้ต้นฉบับเปลี่ยนเมื่อคุณแก้ไของค์ประกอบภายในที่แก้ไขได้ → ใช้ deepcopy
  • การทำสำเนาเฉพาะระดับบนสุดเพียงพอ และต้องการให้ความสำคัญกับประสิทธิภาพ (ความเร็ว/หน่วยความจำ) → ใช้ copy (การคัดลอกแบบตื้น)

การจัดการออบเจ็กต์บิลต์อินและออบเจ็กต์ไม่เปลี่ยนแปลง (immutable)

ออบเจ็กต์ที่ไม่เปลี่ยนแปลง (เช่น int, str และทูเพิลที่มีเนื้อหาไม่เปลี่ยนแปลง) โดยทั่วไปไม่จำเป็นต้องคัดลอก copy.copy อาจคืนออบเจ็กต์เดียวกันเมื่อเหมาะสม

 1# Example: copying immutables
 2import copy
 3
 4a = 42
 5b = copy.copy(a)
 6print(a is b)  # True for small ints (implementation detail)
 7
 8s = "hello"
 9t = copy.deepcopy(s)
10print(s is t)  # True (strings are immutable)

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

คลาสที่กำหนดเอง: การกำหนด __copy__ และ __deepcopy__

หากการคัดลอกแบบปริยายไม่เป็นไปตามที่คุณคาดหวังสำหรับคลาสของคุณ คุณสามารถกำหนดตรรกะการคัดลอกของตนเองได้ หากคุณกำหนด __copy__ และ __deepcopy__ แล้ว copy.copy และ copy.deepcopy จะเรียกใช้เมธอดเหล่านั้น

 1# Example: customizing copy behavior
 2import copy
 3
 4class Node:
 5    def __init__(self, value, children=None):
 6        self.value = value
 7        self.children = children or []
 8
 9    def __repr__(self):
10        return f"Node({self.value!r}, children={self.children!r})"
11
12    def __copy__(self):
13        # Shallow copy: create new Node, but reuse children list reference
14        new = self.__class__(self.value, self.children)
15        return new
16
17    def __deepcopy__(self, memo):
18        # Deep copy: copy value and deepcopy each child
19        new_children = [copy.deepcopy(child, memo) for child in self.children]
20        new = self.__class__(copy.deepcopy(self.value, memo), new_children)
21        memo[id(self)] = new
22        return new
  • ด้วยการกำหนดเมธอด __copy__ และ __deepcopy__ คุณสามารถควบคุมพฤติกรรมการคัดลอกเฉพาะคลาสได้อย่างยืดหยุ่น ตัวอย่างเช่น คุณสามารถจัดการกรณีที่บางแอตทริบิวต์ควรอ้างอิง (ใช้ร่วมกัน) กับอ็อบเจ็กต์เดียวกัน ขณะที่แอตทริบิวต์อื่นควรถูกทำสำเนาเป็นอ็อบเจ็กต์ใหม่ทั้งหมด
  • อาร์กิวเมนต์ memo ที่ส่งให้ __deepcopy__ เป็นดิกชันนารีสำหรับบันทึกอ็อบเจ็กต์ที่ถูกประมวลผลแล้วระหว่างการคัดลอกแบบรีเคอร์ซีฟ และป้องกันลูปไม่สิ้นสุดจากการอ้างอิงแบบวนซ้ำ
 1# Build tree
 2root_node = Node('root', [Node('child1'), Node('child2')])
 3
 4shallow_copy = copy.copy(root_node)   # shallow copy
 5deep_copy = copy.deepcopy(root_node)  # deep copy
 6
 7# Modify children
 8# affects both root_node and shallow_copy
 9shallow_copy.children.append(Node('child_shallow'))
10# affects deep_copy only
11deep_copy.children.append(Node('child_deep'))
12
13# Print results
14print("root_node:", root_node)
15print("shallow_copy:", shallow_copy)
16print("deep_copy:", deep_copy)
  • ในโค้ดนี้ ตัวแปร shallow_copy ถูกสร้างขึ้นผ่านการ 'คัดลอกแบบตื้น' จึงมีการทำสำเนาเฉพาะระดับบนสุดของอ็อบเจ็กต์เท่านั้น ส่วนอ็อบเจ็กต์ที่มันอ้างอิงอยู่ภายใน (ในที่นี้คือลิสต์ children) จะถูกแชร์ร่วมกับ root_node ต้นฉบับ ผลก็คือ เมื่อคุณเพิ่มโหนดใหม่ให้กับ shallow_copy ลิสต์ children ที่ถูกแชร์จะถูกอัปเดต และการเปลี่ยนแปลงนั้นจะสะท้อนในเนื้อหาของ root_node ด้วย
  • ในทางกลับกัน ตัวแปร deep_copy ถูกสร้างขึ้นผ่านการ 'คัดลอกแบบลึก' ทำให้โครงสร้างภายในถูกคัดลอกแบบเวียนซ้ำ จนได้โครงสร้างต้นไม้ที่เป็นอิสระจาก root_node อย่างสมบูรณ์ ดังนั้น แม้คุณจะเพิ่มโหนดใหม่ให้กับ deep_copy ก็จะไม่ส่งผลกระทบต่อ root_node

การอ้างอิงแบบวนรอบ (ออบเจ็กต์ที่อ้างอิงตนเอง) และความสำคัญของ memo

เมื่อกราฟออบเจ็กต์ที่ซับซ้อนอ้างอิงถึงตัวเอง (การอ้างอิงแบบวนรอบ) deepcopy จะใช้ดิกชันนารี memo เพื่อติดตามออบเจ็กต์ที่คัดลอกไปแล้วและป้องกันลูปไม่รู้จบ

 1# Example: recursive list and deepcopy memo demonstration
 2import copy
 3
 4a = []
 5b = [a]
 6a.append(b)  # a -> [b], b -> [a]  (cycle)
 7
 8# deepcopy can handle cycles
 9deep = copy.deepcopy(a)
10print("deep copy succeeded, length:", len(deep))
  • ภายใน deepcopy จะใช้ memo เพื่ออ้างถึงออบเจ็กต์ที่ถูกทำสำเนาแล้ว ทำให้คัดลอกได้อย่างปลอดภัยแม้มีโครงสร้างวนรอบ คุณจำเป็นต้องมีกลไกคล้ายกันเมื่อทำการเวียนซ้ำด้วยตนเอง

copyreg และการคัดลอกแบบกำหนดเองในสไตล์ serialization (ขั้นสูง)

หากต้องการลงทะเบียนพฤติกรรมการคัดลอกพิเศษให้สอดประสานกับไลบรารี คุณสามารถใช้โมดูลมาตรฐาน copyreg เพื่อระบุวิธีการสร้าง (หรือสร้างใหม่) ออบเจ็กต์ สิ่งนี้มีประโยชน์สำหรับออบเจ็กต์ที่ซับซ้อนและชนิดที่เป็นส่วนขยายภาษา C

 1# Example: using copyreg to customize pickling/copy behavior (brief example)
 2import copy
 3import copyreg
 4
 5class Wrapper:
 6    def __init__(self, data):
 7        self.data = data
 8
 9def reduce_wrapper(obj):
10    # Return callable and args so object can be reconstructed
11    return (Wrapper, (obj.data,))
12
13copyreg.pickle(Wrapper, reduce_wrapper)
14
15w = Wrapper([1, 2, 3])
16w_copy = copy.deepcopy(w)
17print("w_copy.data:", w_copy.data)
  • ด้วย copyreg คุณสามารถกำหนดกฎการสร้างออบเจ็กต์ขึ้นมาใหม่ที่มีผลทั้งกับ pickle และ deepcopy นี่เป็น API ขั้นสูง และในหลายกรณี __deepcopy__ ก็เพียงพอแล้ว

ข้อควรระวังและหลุมพรางในการใช้งานจริง

มีหลายจุดสำคัญเพื่อให้มั่นใจว่าพฤติกรรมถูกต้องเมื่อใช้โมดูล copy ด้านล่างนี้เราอธิบายหลุมพรางทั่วไปที่คุณอาจพบระหว่างการพัฒนาและวิธีหลีกเลี่ยง

  • ประสิทธิภาพ deepcopy อาจใช้หน่วยความจำและ CPU มาก ควรพิจารณาอย่างรอบคอบว่าจำเป็นจริงหรือไม่
  • การจัดการองค์ประกอบที่ต้องการแชร์ หากคุณต้องการให้แอททริบิวต์บางอย่าง เช่น แคชขนาดใหญ่ ยังคงถูกแชร์อยู่ ให้หลีกเลี่ยงการคัดลอกการอ้างอิงของมันภายใน __deepcopy__ โดยเจตนา
  • สถานะภายในที่ไม่เปลี่ยนแปลง หากคุณเก็บข้อมูลที่ไม่เปลี่ยนแปลงไว้ภายใน การคัดลอกอาจไม่จำเป็น
  • เธรดและทรัพยากรภายนอก ทรัพยากรที่ไม่สามารถคัดลอกได้ เช่น ซ็อกเก็ตและตัวจัดการไฟล์ มักไม่มีความหมายที่จะคัดลอกหรืออาจก่อให้เกิดข้อผิดพลาด ดังนั้นคุณต้องออกแบบให้หลีกเลี่ยงการคัดลอกสิ่งเหล่านี้

ตัวอย่างเชิงปฏิบัติ: แพทเทิร์นสำหรับอัปเดต dictionary อย่างปลอดภัย

เมื่ออัปเดตออบเจ็กต์การตั้งค่าที่ซับซ้อน ตัวอย่างนี้ใช้ deepcopy เพื่อปกป้องการตั้งค่าต้นฉบับ

 1# Example: safely update a nested configuration using deepcopy
 2import copy
 3
 4default_config = {
 5    "db": {"host": "localhost", "ports": [5432]},
 6    "features": {"use_cache": True, "cache_sizes": [128, 256]},
 7}
 8
 9# Create a working copy to modify without touching defaults
10working = copy.deepcopy(default_config)
11working["db"]["ports"].append(5433)
12working["features"]["cache_sizes"][0] = 512
13
14print("default_config:", default_config)
15print("working:", working)

ด้วย deepcopy คุณสามารถสร้างการตั้งค่าที่ต่อยอดได้อย่างปลอดภัยโดยไม่เสี่ยงทำให้การตั้งค่าเริ่มต้นเสียหาย สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับโครงสร้างแบบซ้อนที่แก้ไขได้ เช่น การตั้งค่าต่างๆ

แนวทางปฏิบัติที่ดีที่สุด

เพื่อใช้งานมอดูล copy ให้ปลอดภัยและมีประสิทธิภาพ ควรจดจำแนวทางปฏิบัติที่ใช้งานได้จริงต่อไปนี้

  • เลือกใช้การคัดลอกแบบตื้นหรือแบบลึกตามความเป็นไปได้ของการเปลี่ยนแปลงและขอบเขตของมัน
  • นำ __copy__ และ __deepcopy__ ไปใช้ในคลาสที่กำหนดเองเพื่อให้ได้พฤติกรรมตามที่คาดหวัง
  • เนื่องจากการคัดลอกแบบลึกมีต้นทุนสูง ควรลดความจำเป็นในการคัดลอกผ่านการออกแบบเมื่อเป็นไปได้ พิจารณาการทำให้ออบเจ็กต์ไม่เปลี่ยนแปลง และเมธอดการโคลนแบบชัดเจน ตลอดจนเทคนิคอื่นๆ
  • เมื่อจัดการกับการอ้างอิงแบบวนรอบ ให้ใช้ deepcopy หรือจัดเตรียมกลไกคล้าย memo ด้วยตนเอง
  • ออกแบบให้ทรัพยากรภายนอก เช่น ตัวจัดการไฟล์ (file handles) และเธรด ไม่ถูกคัดลอก

สรุป

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

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video