المستوى المطلوب لقراءة هذا المقال: مبتدئ. لو لسه بتتعلم الدوال (functions) والمتغيرات، المقال ده هيوفّر عليك ساعات تدوير في الأخطاء. هتعرف بالظبط ليه دالة بتعدّل على بياناتك من غير ما تطلب منها كده، وإزاي توقف السلوك ده وقت ما تحب.
الخلاصة الأول: لما تبعت رقم أو نص لدالة، الدالة بتشتغل على نسخة، وبياناتك الأصلية بتفضل سليمة. لكن لما تبعت list أو dictionary، الدالة بتشتغل على نفس الشيء الأصلي، فأي تعديل جوّاها بيظهر برّاها. ده الفرق اللي بيوقّع المبتدئين كل يوم.
المشكلة باختصار
تخيّل إنك كاتب دالة بتضيف عنصر على list علشان تطبع النتيجة بس. بعد ما الدالة تخلص، تلاقي الـ list الأصلية اتغيّرت كمان. انت معملتش return، ومع ذلك البيانات اتبدّلت. الموقف ده مش غلطة في اللغة، ده اسمه التعديل عبر المرجع (pass by reference)، وفهمه بيفرق بين كود بتثق فيه وكود بيفاجئك.
المفهوم بمثال: الورقة والأوضة
تخيّل عندك أوضة فيها صندوق. القيمة (value) معناها إنك بتدّي صاحبك صورة من اللي جوّا الصندوق على ورقة. لو هو شخبط على الورقة، صندوقك مش هيتأثر، لأنه معاه نسخة بس.
المرجع (reference) معناه إنك بتدّي صاحبك مفتاح الأوضة نفسها. لو دخل وغيّر اللي في الصندوق، هتلاقي التغيير لما تدخل انت، لأنكم الاتنين بتفتحوا نفس الأوضة بنفس المفتاح.
دلوقتي بالشكل العلمي: المتغيّر في الذاكرة بيشاور (points) على عنوان. مع الأنواع البسيطة زي الأرقام والنصوص، الدالة بتاخد نسخة من القيمة. مع الأنواع المركّبة زي list و dict، الدالة بتاخد نسخة من العنوان نفسه، فالاتنين بيشاوروا على نفس الكائن (object) في الذاكرة. تعديل محتوى الكائن بيبان عند الطرفين.
كود شغّال يوريك الفرق
الكود ده اتجرّب على Python 3.13. لاحظ إن الرقم رجع زي ما هو، لكن الـ list اتغيّرت:
# رقم: بيتبعت بالقيمة، الأصل ما بيتأثرش
def add_ten(n):
n = n + 10
return n
x = 5
add_ten(x)
print(x) # 5 ← الأصل سليم
# list: بتتبعت بالمرجع، الأصل بيتأثر
def append_item(items):
items.append(99) # تعديل في نفس الكائن
my_list = [1, 2, 3]
append_item(my_list)
print(my_list) # [1, 2, 3, 99] ← اتغيّرت من غير return
نفس السلوك في JavaScript: الـ primitives (number, string, boolean) بالقيمة، والـ objects والـ arrays بالمرجع:
function pushItem(arr) {
arr.push(99); // نفس المصفوفة الأصلية
}
const list = [1, 2, 3];
pushItem(list);
console.log(list); // [1, 2, 3, 99]
الحل: انسخ قبل ما تعدّل
لو عايز الدالة تشتغل من غير ما تلمس الأصل، اعمل نسخة جوّاها. في Python فيه طريقتين، وكل واحدة بتمنها:
- نسخة سطحية (shallow copy): بـ
items[:]أوlist(items). بتنسخ المستوى الأول بس. سريعة، بس لو جوّا الـ list فيه lists تانية، دي لسه بتتشارك. - نسخة عميقة (deep copy): بـ
copy.deepcopy(items). بتنسخ كل حاجة لآخر مستوى. آمنة تمامًا، بس أبطأ وبتاكل ذاكرة أكتر.
import copy
def append_safe(items):
local = items[:] # نسخة سطحية
local.append(99)
return local
my_list = [1, 2, 3]
result = append_safe(my_list)
print(my_list) # [1, 2, 3] ← الأصل اتحمى
print(result) # [1, 2, 3, 99] ← التعديل في النسخة
الـ trade-offs اللي لازم تعرفها
التعديل عبر المرجع مش عيب، ده ميزة في حالات. لكن كل اختيار له ثمن:
- التعديل المباشر (in-place): بتكسب سرعة وذاكرة أقل، لأنك مش بتكرّر البيانات. بتخسر الأمان: أي حد بيستدعي الدالة ممكن يتفاجئ. مناسب لو الـ list صغيرة وانت اللي ماسك الكود كله.
- النسخة قبل التعديل: بتكسب أمان ووضوح، الدالة بقت pure ومش بتأثر على برّا. بتخسر شوية أداء وذاكرة. لو الـ list فيها مليون عنصر، النسخة العميقة ممكن تاخد وقت محسوس.
الافتراض هنا إنك شغّال على بيانات صغيرة لمتوسطة (آلاف العناصر مش ملايين). فوق كده، اقيس الفرق بنفسك قبل ما تقرر تنسخ في كل نداء.
متى متشغلش بالك
لو الدالة بتقرأ البيانات بس وما بتعدّلش (زي sum أو len أو طباعة)، مفيش أي خطر، وما تحتاجش تنسخ حاجة. كمان لو الكائن غير قابل للتعديل أصلًا زي الـ tuple في Python أو الـ string، اللغة بتحميك تلقائيًا، فأي محاولة تعديل بتطلّع خطأ أو بتعمل كائن جديد. النسخ الزيادة في الحالات دي بياكل ذاكرة من غير فايدة.
الخطوة التالية
دلوقتي افتح أي دالة عندك بتاخد list أو dict كـ parameter. لو الدالة بتعدّل فيها وانت مش قاصد ده يطلع برّا، ضيف نسخة في أول سطر: items = items[:]. شغّل الكود وشوف هل النتيجة بقت متوقّعة. لو لسه بتتفاجئ، غالبًا فيه list جوّا list ومحتاج deepcopy.
المصادر
- توثيق Python الرسمي — قسم نموذج التنفيذ والدوال (docs.python.org).
- وحدة
copyفي Python:copy()وdeepcopy()(docs.python.org/3/library/copy.html). - MDN Web Docs — Primitive vs Object، وكيفية تمرير الوسائط في JavaScript (developer.mozilla.org).
- Real Python — Passing Mutable Objects and Side Effects.