โมดูล `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 ด้วย