המודול `copy` של פייתון
מאמר זה מסביר את המודול copy של פייתון.
בהתמקדות בהבדלים בין העתקה רדודה לעמוקה, אנו מספקים הסבר ברור — החל מהמנגנונים הבסיסיים של שכפול אובייקטים ועד יישומים במחלקות מותאמות — בצירוף דוגמאות מעשיות.
YouTube Video
המודול copy של פייתון
המודול copy של Python הוא המודול הסטנדרטי לטיפול בשכפול (העתקה) של אובייקטים. המטרה היא להבין את ההבדל בין העתקות רדודות לעמוקות ולהצליח לשלוט בהתנהגות ההעתקה עבור אובייקטים מותאמים אישית.
יסודות: מהי העתקה רדודה?
כאן נדגים את ההתנהגות של העתקות רדודות עבור אובייקטים ברי-שינוי כמו רשימות ומילונים. העתקה רדודה משכפלת רק את האובייקט ברמה העליונה ומשתפת את ההפניות שבתוכו.
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(העתקה רדודה).
התמודדות עם אובייקטים מובנים ובלתי־ניתנים לשינוי
אובייקטים בלתי־ניתנים לשינוי (למשל 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 והעתקה מותאמת בסגנון סריאליזציה (מתקדם)
אם תרצו לרשום התנהגות העתקה מיוחדת בתיאום עם ספריות, תוכלו להשתמש במודול הסטנדרטי 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__. - מצב פנימי בלתי־ניתן לשינוי אם אתם מחזיקים נתונים בלתי־ניתנים לשינוי פנימית, ייתכן שאין צורך בהעתקה.
- תהליכונים ומשאבים חיצוניים משאבים שאינם ניתנים להעתקה, כגון שקעים (sockets) וידיות קבצים, או שאין טעם להעתיקם או שהם יגרמו לשגיאות—לכן יש להימנע מהעתקתם בתכנון המערכת.
דוגמה מעשית: תבנית לעדכון בטוח של מילונים
בעת עדכון אובייקט תצורה מורכב, דוגמה זו משתמשת ב־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__במחלקות מותאמות אישית כדי להגיע להתנהגות המצופה. - מכיוון שהעתקות עמוקות יקרות, הפחיתו את הצורך בהעתקה באמצעות תכנון נכון כאשר הדבר אפשרי. שקלו אי־שינוי (immutability) ושיטות שכפול מפורשות, לצד טכניקות נוספות.
- בעת התמודדות עם הפניות מחזוריות, השתמשו ב־
deepcopyאו ספקו מנגנון ידני דמויmemo. - תכננו כך שמשאבים חיצוניים כגון ידיות קבצים ושרשורים לא יועתקו.
סיום
המודול copy הוא הכלי הסטנדרטי לשכפול אובייקטים ב־Python. על־ידי הבנה נכונה של ההבדלים בין העתקות רדודות לעמוקות ומימוש התנהגות העתקה מותאמת בעת הצורך, תוכלו לבצע שכפול בצורה בטוחה וצפויה. באמצעות הבהרה בשלב התכנון אם אכן נדרשת העתקה ומה אמור להיות משותף, ניתן להימנע מבאגים מיותרים ומבעיות ביצועים.
תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.