העתקת אובייקטים היא מושג בסיסי בפייתון, וחוסר הבנה שלו מוביל לעתים קרובות לבאגים שקשה לאתר. כאשר אתם משנים אובייקט אחד, ומגלים שאובייקט אחר השתנה באופן בלתי צפוי, הבעיה נעוצה ככל הנראה בדרך שבה הוא הועתק. בואו נבין את הנושא לעומק אחת ולתמיד.
🔎 למה העתקת אובייקטים כל כך חשובה?
בפייתון, משתנים אינם "קופסאות" המכילות מידע, אלא "תוויות" או הפניות (references) המצביעות על אובייקטים בזיכרון. כאשר אתם מבצעים השמה פשוטה, כמו בדוגמה הבאה:
list_a = [1, 2, 3]
list_b = list_a
אתם לא יוצרים רשימה חדשה. אתם פשוט יוצרים הפניה נוספת (list_b) שמצביעה על אותו אובייקט רשימה בדיוק.
👈 כל שינוי ב-list_a ישתקף מיד ב-list_b (ולהיפך), מכיוון ששניהם מצביעים על אותו אובייקט.
אנלוגיה: דמיינו שיש לכם מסמך ב-Google Docs. אם תשלחו לחבר קישור אליו, הוא יערוך את המסמך המקורי שלכם. זוהי פעולת השמה. לעומת זאת, אם תבחרו ב"קובץ" ← "צור עותק", ייווצר מסמך חדש ועצמאי לחלוטין. המודול copy בפייתון קיים בדיוק כדי ליצור עותקים עצמאיים כאלה.
⚙️ שני סוגי העתקה
המודול copy מספק שתי פונקציות מרכזיות:
copy.copy()– יוצר העתקה שטחית (shallow copy).copy.deepcopy()– יוצר העתקה עמוקה (deep copy).
ההבדל המרכזי ביניהן הוא האופן שבו הן מטפלות באובייקטים מקוננים (כמו רשימות בתוך רשימות או מילונים בתוך מילונים).
🟦 העתקה שטחית (Shallow Copy)
copy.copy() יוצר אובייקט-מכל חדש ברמה העליונה, אך במקום להעתיק את האובייקטים המקוננים, הוא מכניס לתוכו הפניות לאובייקטים המקוריים.
import copy
original_list = [1, 2, [3, 4]]
shallow_copy = copy.copy(original_list)
# נשנה את הרשימה המקוננת במקור
original_list[2][0] = 99
print(f"מקור: {original_list}") # פלט: מקור: [1, 2, [99, 4]]
print(f"העתקה שטחית: {shallow_copy}") # פלט: העתקה שטחית: [1, 2, [99, 4]]
כפי שניתן לראות, השינוי ברשימה המקוננת השפיע גם על המקור וגם על העותק, מכיוון ששניהם חולקים את אותה רשימה פנימית.
דיאגרמת ASCII:
original_list → [ 1, 2, ───► list_inner ]
shallow_copy → [ 1, 2, ───► list_inner ]
list_inner → [ 99, 4 ]
🟩 העתקה עמוקה (Deep Copy)
copy.deepcopy() פותר את בעיית ההפניות המשותפות. פונקציה זו עוברת באופן רקורסיבי על כל האובייקט ויוצרת עותקים מלאים ועצמאיים של כל מה שהיא פוגשת, כולל כל האובייקטים המקוננים.
import copy
original_list = [1, 2, [3, 4]]
deep_copy = copy.deepcopy(original_list)
# נשנה את הרשימה המקוננת במקור
original_list[2][0] = 99
print(f"מקור: {original_list}") # פלט: מקור: [1, 2, [99, 4]]
print(f"העתקה עמוקה: {deep_copy}") # פלט: העתקה עמוקה: [1, 2, [3, 4]]
כעת, deep_copy הוא אובייקט עצמאי לחלוטין.
דיאגרמת ASCII:
original_list → [ 1, 2, ───► list_a ]
deep_copy → [ 1, 2, ───► list_b ]
list_a → [ 99, 4 ]
list_b → [ 3, 4 ]
📑 טבלת השוואה: copy.copy() מול copy.deepcopy()
| סוג האובייקט | דוגמה | copy.copy() (שטחית) | copy.deepcopy() (עמוקה) |
|---|---|---|---|
| list (רשימה) | [1, 2, [3, 4]] | רשימה חדשה, אך הרשימה הפנימית [3, 4] משותפת | עותק עצמאי לחלוטין; גם הרשימה הפנימית מועתקת |
| dict (מילון) | {"a": 1, "b": {"c": 2}} | מילון חדש, אך המילון הפנימי {"c": 2} משותף | מילון עצמאי וכל המילונים המקוננים מועתקים |
| set (קבוצה) | {1, 2, (3, 4)} | קבוצה חדשה; איברים פנימיים שהם mutable יהיו משותפים | קבוצה חדשה; כל האיברים הפנימיים מועתקים רקורסיבית |
| tuple | (1, 2, [3, 4]) | מחזיר את אותו ה-tuple, אך הרשימה הפנימית משותפת | tuple חדש עם עותק של הרשימה הפנימית |
| str, int, float, bool | "hi", 42, 3.14, True | מחזיר את אותו האובייקט (הם immutable, אין טעם להעתיק) | זהה להעתקה שטחית |
| מחלקה מותאמת אישית | Node(1, Node(2)) | אובייקט חדש ברמה העליונה; אובייקטים פנימיים משותפים | עותק עצמאי לחלוטין של האובייקט וכל האובייקטים הפנימיים |
אובייקט עם __slots__ | מחלקה עם __slots__ | רק האובייקט ברמה העליונה מועתק | כל האובייקטים הפנימיים מועתקים אם הם תומכים בהעתקה |
| קובץ / סוקט / stream | open("file.txt") | שגיאה (אובייקט שאינו ניתן להעתקה) | שגיאה (אובייקט שאינו ניתן להעתקה) |
🧪 דוגמאות חיות לטבלה
📌 list
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # [1, 2, [99, 4]]
print(f"deepcopy: {c}") # [1, 2, [3, 4]]
📌 dict
import copy
a = {"a": 1, "b": {"c": 2}}
b = copy.copy(a)
c = copy.deepcopy(a)
a["b"]["c"] = 42
print(f"copy: {b}") # {'a': 1, 'b': {'c': 42}}
print(f"deepcopy: {c}") # {'a': 1, 'b': {'c': 2}}
📌 set
import copy
a = {1, 2, (3, 4)} # קבוצות יכולות להכיל רק טיפוסים שהם immutable
b = copy.copy(a)
c = copy.deepcopy(a)
print(b == c) # True (אך אלו שני אובייקטים נפרדים בזיכרון)
📌 tuple
import copy
a = (1, 2, [3, 4]) # ה-tuple הוא immutable, אך איבר הרשימה שבתוכו לא
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # (1, 2, [99, 4])
print(f"deepcopy: {c}") # (1, 2, [3, 4])
📌 מחלקה מותאמת אישית
import copy
class Node:
def __init__(self, value, child=None):
self.value = value
self.child = child
a = Node(1, Node(2))
b = copy.copy(a)
c = copy.deepcopy(a)
a.child.value = 99
print(f"copy: {b.child.value}") # 99
print(f"deepcopy: {c.child.value}") # 2
📌 התאמה אישית של התנהגות: __copy__ ו-__deepcopy__
אתם יכולים לשלוט על אופן ההעתקה של אובייקטים שיצרתם על ידי הגדרת מתודות מיוחדות במחלקה שלהם.
import copy
class Custom:
def __init__(self, x):
self.x = x
def __copy__(self):
print("נקראה המתודה __copy__!")
# החזרת מופע חדש, אך עם אותה הפניה למידע הפנימי
return Custom(self.x)
def __deepcopy__(self, memo):
# memo הוא מילון למעקב אחר אובייקטים שכבר הועתקו (למניעת רקורסיה)
print("נקראה המתודה __deepcopy__!")
# יצירת עותק חדש לחלוטין, כולל המידע המקונן
return Custom(copy.deepcopy(self.x, memo))
obj = Custom([1, 2])
c1 = copy.copy(obj) # פלט: נקראה המתודה __copy__!
c2 = copy.deepcopy(obj) # פלט: נקראה המתודה __deepcopy__!
⚡ ביצועים ומלכודות
- מהירות:
copy.copy()מהירה משמעותית מכיוון שהיא לא עוברת רקורסיבית על כל מבנה הנתונים.copy.deepcopy()יכולה להיות איטית מאוד עבור אובייקטים גדולים ומקוננים לעומק. - אובייקטים שאינם ניתנים להעתקה: משאבי מערכת כמו קבצים פתוחים, סוקטים או זרמי נתונים (streams) אינם ניתנים להעתקה. ניסיון לעשות זאת יגרום לשגיאה.
- הפניות מעגליות:
deepcopyחכמה מספיק כדי להתמודד עם הפניות מעגליות (כאשר אובייקטAמפנה ל-B, ו-Bמפנה בחזרה ל-A) מבלי להיכנס ללולאה אינסופית.
מתי להשתמש בכל שיטה: כללי האצבע
- השתמשו בהעתקה שטחית (
copy.copy()) אם האובייקט שלכם מכיל רק מידע immutable, או אם אתם מעוניינים במכוון שהעותק והמקור יחלקו אובייקטים פנימיים. זה מהיר ויעיל. - השתמשו בהעתקה עמוקה (
copy.deepcopy()) כאשר אתם זקוקים לעותק עצמאי לחלוטין, ב-100%, מהמקור, במיוחד בעבודה עם מבני נתונים מורכבים (רשימות, מילונים, מופעים של מחלקות). - עבור אובייקטים מסוג immutable (מספרים, מחרוזות, tuples ללא איברים משתנים), לרוב אין טעם בהעתקה – פייתון ממילא מבצעת אופטימיזציה לשימוש בהם.
- זכרו שניתן להתאים אישית את התהליך באמצעות
__copy__ו-__deepcopy__לשליטה מלאה.