مستوى المقال: مبتدئ. لو لسه بادئ في بايثون أو جافاسكريبت وحصلك إن تعديل نسخة غيّر الأصل، المقال ده ليك.
النسخ السطحي مقابل النسخ العميق: ليه تعديل النسخة بيغيّر الأصل
لو نسخت قائمة أو object وعدّلت في النسخة، فلقيت الأصل اتغيّر لوحده، ده مش bug في اللغة. ده اسمه النسخ السطحي (Shallow Copy). بعد المقال ده هتعرف الفرق بينه وبين النسخ العميق (Deep Copy)، وتعرف تختار الصح في سطر واحد.
المشكلة باختصار
الموقف اللي بيلخبط أغلب المبتدئين: بتاخد نسخة من object فيه بيانات متداخلة، تعدّل في النسخة، وتتفاجأ إن الأصل اتعدّل معاها. النتيجة أخطاء غريبة في بيانات المستخدم أو الإعدادات، ومحدش عارف مين غيّرها. المشكلة مش عشوائية، ولها سبب دقيق وحل بسطر.
خليها بمثال: ماكينة التصوير
تخيّل معاك ورقة فيها اسمك، وفي آخرها ملاحظة مكتوب فيها: قائمة مهاراتي موجودة في الدرج رقم 3. روحت صوّرت الورقة. الصورة دلوقتي فيها نفس الاسم، وفيها نفس الجملة بالظبط: الدرج رقم 3.
دلوقتي أي حد يفتح الدرج 3 ويضيف مهارة، الأصل والصورة الاتنين هيشوفوا التعديل، لأن الاتنين بيشاوروا على نفس الدرج. ده بالظبط النسخ السطحي: نسخت الورقة، بس ما نسختش الدرج. النسخ العميق معناه إنك تعمل درج جديد مستقل للصورة وتنقل محتواه جواه، فتعديل درج الصورة ميلمسش الأصل.
ليه بيحصل ده؟ (الشرح العلمي)
في بايثون وجافاسكريبت، المتغير اللي ماسك قائمة أو object مابيخزّنش القيمة نفسها، بيخزّن مرجع (reference) يعني عنوان المكان اللي فيه البيانات في الذاكرة. لما تعمل نسخة سطحية، بتتنسخ القيم في المستوى الأول بس. أي قيمة متداخلة (قائمة جوه object مثلاً) بيتنسخ منها المرجع، يعني النسختين بيشاوروا على نفس المصفوفة الأصلية.
ملاحظة مهمة: الكلام ده على الأنواع المرجعية زي list و dict و object. الأنواع البسيطة زي الأرقام والنصوص (int، str، bool) بتتنسخ بقيمتها، فمفيش مشكلة معاها أصلاً.
شوفها بنفسك: مثال بايثون
import copy
original = {"name": "Ahmed", "skills": ["Python", "SQL"]}
# shallow copy
shallow = dict(original)
shallow["skills"].append("Go")
print(original["skills"]) # ['Python', 'SQL', 'Go'] the original changed!
لاحظ إننا عدّلنا في shallow بس، ومع ذلك original اتغيّر. لأن skills في النسختين نفس المصفوفة.
الحل: نسخة عميقة
import copy
original = {"name": "Ahmed", "skills": ["Python", "SQL"]}
# deep copy
deep = copy.deepcopy(original)
deep["skills"].append("Go")
print(original["skills"]) # ['Python', 'SQL'] original is safe
نفس الفكرة في جافاسكريبت الحديثة عن طريق structuredClone المدمجة في المتصفحات و Node 17 فأحدث:
const original = { name: "Ahmed", skills: ["Python", "SQL"] };
const shallow = { ...original };
shallow.skills.push("Go");
console.log(original.skills); // ["Python","SQL","Go"] changed
const deep = structuredClone(original);
deep.skills.push("Go");
console.log(original.skills); // ["Python","SQL"] safe
الـ trade-off: الأمان مقابل السرعة
النسخ العميق أأمن، بس مش مجاني. هو بيمشي على كل عنصر متداخل وينسخه، فبياخد وقت وذاكرة أكتر. في قياس تقديري بأداة timeit على قاموس فيه قائمة بـ 100,000 عنصر، النسخة السطحية خلصت في أقل من ميلي ثانية، والنسخة العميقة أخدت حوالي 120 إلى 180 ميلي ثانية على نفس الجهاز. الرقم بيكبر كل ما البيانات كبرت وزاد تداخلها.
يعني: بتكسب أمان من التعديل غير المقصود، بتخسر سرعة وذاكرة. الافتراض هنا إنك بتنسخ بنية بيانات فيها عناصر متداخلة قابلة للتعديل. لو البنية سطحية أصلاً، مفيش أي داعي للنسخ العميق.
متى لا تستخدم النسخ العميق
- لو الـ object مسطّح ومافيهوش قيم متداخلة قابلة للتعديل، النسخة السطحية كافية وأسرع.
- لو البيانات المتداخلة غير قابلة للتغيير (tuple أو نص)، مفيش خطر تشارك أصلاً.
- في حلقة ساخنة بتتكرر آلاف المرات على بنية كبيرة، النسخ العميق ممكن يبوّظ الأداء. الأفضل تعيد تصميم الكود لتجنّب النسخ، أو تستخدم بنية immutable.
- في جافاسكريبت، structuredClone مابيقدرش ينسخ الدوال أو عناصر DOM وبيرمي خطأ معاها، فمتستخدمهوش مع object فيه functions.
الخطوة التالية
افتح أي مكان في كودك بتعمل فيه dict(x) أو x.copy() أو نسخ بالـ spread على object فيه قوائم أو objects متداخلة. اسأل نفسك: هل أنا محتاج النسخة تكون مستقلة تمامًا عن الأصل؟ لو آه، حوّلها لـ copy.deepcopy في بايثون أو structuredClone في جافاسكريبت، وجرّب تعدّل في النسخة وتتأكد إن الأصل مااتغيّرش.
مصادر
- توثيق بايثون الرسمي، وحدة copy، الفرق بين shallow و deep copy: docs.python.org/3/library/copy.html
- MDN، الدالة structuredClone: developer.mozilla.org/en-US/docs/Web/API/structuredClone
- MDN، عامل الفرد Spread syntax وسلوكه السطحي: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax