مستوى المقال: مبتدئ. مناسب لأي حد كتب كام سطر بايثون أو جافاسكريبت وبيتعامل مع القوائم والكائنات، ولسه ماخدش باله من فخ المرجع المشترك.
النسخ السطحي والعميق: ليه تعديل النسخة بيغيّر الأصل
لو عملت نسخة من قائمة، عدّلت النسخة، ولقيت الأصل اتغيّر معاها، إنت مش بتتعامل مع نسختين. إنت بتتعامل مع نفس الكائن بمرجعين. المقال ده هيوريك السبب في دقيقتين، وإزاي تعمل نسخة حقيقية مستقلة بسطر واحد.
المشكلة باختصار
في بايثون، لما تكتب b = a إنت مش بتعمل نسخة. إنت بتدّي نفس القائمة اسم تاني. حتى a.copy() بيعمل نسخة، لكن نسخة سطحية بتشارك الكائنات المتداخلة جواها. النتيجة: تعديل عنصر متداخل في النسخة بيظهر في الأصل. الباج ده بيعدّي بسهولة في الكود وبيطلع في وقت متأخر لما البيانات تبقى متشابكة.
المفهوم بمثال بسيط الأول
تخيل عندك درج فيه ورق مهم. فيه تلات طرق تدّي حد حق الوصول له:
- الإسناد (
b = a): تدّيه مفتاح لنفس الدرج. أي ورقة يعدّلها بتتعدّل عندك، لأنه نفس الدرج. - النسخ السطحي (
a.copy()): تجيب درج جديد، بس تحط فيه نفس الملفات الأصلية اللي جوه الدرج القديم. الدرج مختلف، لكن الملفات مشتركة. يعدّل ورقة جوه ملف، تتعدّل عندك. - النسخ العميق (
copy.deepcopy(a)): تصوّر كل ورقة وكل ملف من الأول، وتحطهم في درج جديد. دلوقتي بقى عندك نسختين مستقلتين تمامًا. يعدّل زي ما هو عايز، ورقك ميتلمسش.
الخلاصة من المثال: النسخ السطحي بينسخ الطبقة الأولى بس. أي حاجة متداخلة (قائمة جوه قائمة، قاموس جوه قائمة) بتفضل مشتركة بين الأصل والنسخة.
نفس الكلام علميًا
في بايثون، المتغير مش بيخزّن القيمة نفسها، بيخزّن مرجع (عنوان) لكائن في الذاكرة. b = a بينسخ المرجع، فالاتنين بيأشروا على نفس الكائن. copy() وcopy.copy() بيعملوا كائن خارجي جديد، لكن بيملوه بنفس مراجع العناصر الداخلية. عشان كده الكائنات المتداخلة بتفضل مشتركة. copy.deepcopy() بيمشي على الشجرة كلها ويعيد بناء كل كائن متداخل بمرجع جديد، فمفيش أي مشاركة.
الكود اللي بيوريك الباج
import copy
a = [[1, 2], [3, 4]]
b = a.copy() # نسخة سطحية
b[0].append(99) # بنعدّل عنصر متداخل في النسخة
print(a) # [[1, 2, 99], [3, 4]] <-- الأصل اتغيّر!
c = copy.deepcopy(a) # نسخة عميقة
c[0].append(777) # نعدّل النسخة العميقة
print(a) # [[1, 2, 99], [3, 4]] <-- الأصل زي ما هو
السطر b[0].append(99) عدّل الأصل، لأن b[0] وa[0] نفس القائمة الداخلية. أما c فمستقلة تمامًا. لاحظ إن b = a.copy() بيحمي بس لو التعديل على الطبقة الأولى، زي b.append([5,6])، لكن مش بيحمي التعديل على المتداخل.
سيناريو واقعي بالأرقام
افترض عندك دالة بتاخد إعدادات المستخدم الافتراضية كـ dict متداخل، وبتعمل منها نسخة سطحية لكل مستخدم عشان تعدّلها. أول ما مستخدم يغيّر عنصر متداخل، بيغيّر الإعدادات الافتراضية لكل المستخدمين بعده. في نظام فيه 5000 مستخدم، ده معناه إعدادات ملوّثة تتسرّب بين الحسابات. الباج ده مبيرميش خطأ، بيدّي بيانات غلط بصمت، وده أصعب نوع في التتبّع.
الفرق في السرعة: قياس
import copy, timeit
data = [{"id": i, "tags": [1, 2, 3]} for i in range(10000)]
shallow = timeit.timeit(lambda: data.copy(), number=100) / 100
deep = timeit.timeit(lambda: copy.deepcopy(data), number=100) / 100
print(f"سطحي: {shallow*1000:.3f} ms")
print(f"عميق: {deep*1000:.3f} ms")
على قائمة فيها 10 آلاف قاموس متداخل، النسخة السطحية بتاخد أقل من 0.1 مللي ثانية، والنسخة العميقة بتاخد حوالي 30 إلى 60 مللي ثانية على جهاز عادي. يعني فرق مئات المرات. الأرقام دي تقديرية وبتختلف حسب الجهاز وإصدار بايثون، بس النسبة ثابتة: العميق أبطأ بكتير لأنه بيعيد بناء كل كائن.
الـ trade-off
النسخة العميقة بتكسب أمان كامل: النسختين مستقلتين، مفيش أي تلوّث. بتخسر السرعة والذاكرة، لأنها بتكرّر كل كائن متداخل، فبتستهلك ضعف الذاكرة تقريبًا للبيانات المتداخلة. النسخة السطحية بتكسب سرعة وخفّة، بتخسر الأمان مع أي بنية متداخلة. الاختيار مش "الأحسن دايمًا"، الاختيار حسب هل بياناتك متداخلة وهل هتعدّل المتداخل.
متى لا تستخدم كل واحد
ما تستخدمش النسخة العميقة لو كائنك مسطّح، يعني قائمة أرقام أو نصوص بس. هنا list.copy() أو list(a) كافية وأسرع بكتير، والعميق هيبقى هدر. وكمان ما تستخدمش deepcopy على كائنات فيها موارد زي اتصال قاعدة بيانات، أو ملف مفتوح، أو socket، لأنها مش قابلة للنسخ بشكل منطقي وممكن تكسر. في المقابل، ما تعتمدش على النسخة السطحية لو هتعدّل عناصر متداخلة وعايز الأصل يفضل نضيف.
ملاحظة للي بيشتغلوا جافاسكريبت: نفس الفكرة موجودة. {...obj} وObject.assign نسخ سطحي، وstructuredClone(obj) بيعمل نسخة عميقة مدمجة في المتصفحات الحديثة وNode.
الخطوة التالية
روح لأقرب مكان في كودك بتعمل فيه .copy() على dict أو list فيها عناصر متداخلة. اسأل نفسك سؤال واحد: هل هعدّل المتداخل؟ لو آه، حوّلها لـ copy.deepcopy() واختبر. لو الأصل بقى ثابت بعد تعديل النسخة، يبقى الباج كان موجود وإنت أصلحته دلوقتي.
المصادر
- توثيق بايثون الرسمي، وحدة copy: docs.python.org/3/library/copy.html — الفرق بين copy() وdeepcopy() وسلوك الكائنات المتداخلة.
- توثيق بايثون، نموذج البيانات والأسماء والمراجع: docs.python.org/3/reference/datamodel.html — إن المتغيرات مراجع لكائنات.
- MDN Web Docs، structuredClone: developer.mozilla.org/en-US/docs/Web/API/structuredClone — النسخ العميق في جافاسكريبت.
- توثيق بايثون، وحدة timeit المستخدمة في القياس: docs.python.org/3/library/timeit.html.