المستوى: مبتدئ. لو لسه في أول طريقك في البرمجة، وكلمة Recursion بتخضّك، المقال ده هيخلّيك تفهمها في أقل من 6 دقايق، وتعرف بالظبط ليه دالة بسيطة بتنادي نفسها ممكن تكسر البرنامج بخطأ اسمه RecursionError، وإزاي تتجنّبه.
الـ Recursion: لما الدالة تنادي نفسها
الفكرة كلها في جملة واحدة: الـ Recursion (الاستدعاء الذاتي) هو إن دالة تنادي نفسها عشان تحل نسخة أصغر من نفس المشكلة. المكسب إن الكود بيبقى قصير وأنيق في مسائل معيّنة. المشكلة: لو نسيت تقولها تقف امتى، البرنامج بيقع.
المشكلة باختصار
أغلب المبتدئين بيكتبوا دالة recursive صح، وتشتغل مع أرقام صغيرة، وبعدين تنفجر فجأة مع مدخل كبير. الرسالة اللي بتظهر: RecursionError: maximum recursion depth exceeded. ده مش عطل عشوائي، وله سبب دقيق هنفهمه دلوقتي بمثال بسيط ثم علميًا.
خلّيك تفهمها الأول بعرائس الماتريوشكا
عندك عروسة خشب روسية (ماتريوشكا). بتفتحها، تلاقي جواها عروسة أصغر. بتفتح الصغيرة، تلاقي أصغر منها. وهكذا. لحد ما توصل لآخر عروسة صغيرة مصمتة، مش بتتفتح خالص. دي نقطة التوقف.
خلّينا نسمّي فعل افتح العروسة دالة. الدالة دي بتعمل حاجة واحدة: تفتح العروسة اللي قدامها، وبعدين تنادي نفسها على العروسة الأصغر اللي طلعت. العروسة المصمتة اللي مبتتفتحش هي اللي بتوقف السلسلة.
دلوقتي خُد عروسة معمولة غلط: كل ما تفتحها تلاقي جواها عروسة بنفس الحجم بالظبط، من غير ما توصل أبدًا لعروسة مصمتة. هتفضل تفتح للأبد. ده بالظبط اللي بيحصل للبرنامج لما تنسى نقطة التوقف.
التعريف العلمي بدقة
أي دالة recursive سليمة لازم يكون فيها جزئين: حالة الأساس (Base Case) وهي الشرط اللي بيوقف الاستدعاء ويرجع نتيجة مباشرة، والخطوة الاستدعائية (Recursive Step) اللي بتنادي الدالة نفسها على مدخل أصغر بحيث كل نداء يقرّب من حالة الأساس.
لو الخطوة الاستدعائية مش بتصغّر المدخل، أو حالة الأساس مش موجودة، الاستدعاء ميوقفش، وده مصدر المشكلة.
مثال كود شغّال
أشهر مثال: حساب المضروب (factorial). لاحظ حالة الأساس والخطوة الاستدعائية:
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
print(factorial(5)) # 120
ودلوقتي شوف اللي بيحصل لما ننسى حالة الأساس:
import sys
print(sys.getrecursionlimit()) # 1000
def count_down(n):
return count_down(n - 1)
count_down(100000)
# RecursionError: maximum recursion depth exceeded
الفرق سطر واحد: غياب الشرط اللي يوقف السلسلة.
ليه بيحصل الخطأ ده؟ قصة المكدس
كل مرة الدالة تنادي نفسها، النظام بيحجز شوية ذاكرة اسمها إطار (Stack Frame) عشان يفتكر المتغيرات المحلية ومكان الرجوع. الإطارات دي بتتركم فوق بعضها في مكدس الاستدعاء. لما الدالة توصل لحالة الأساس وترجع، الإطارات بتتشال واحد واحد.
المشكلة إن المكدس محدود. بايثون بيحط سقف افتراضي حوالي 1000 نداء متداخل. لو عدّيته من غير رجوع، بايثون بيوقفك بـ RecursionError قبل ما الذاكرة كلها تتآكل.
سيناريو واقعي: دالة recursive بتمشي على شجرة مجلدات فيها تفريعات أعمق من ألف مستوى (بيحصل مع JSON عميق أو روابط رمزية دايرة) هتضرب الخطأ، بينما نسخة بحلقة while ومكدس يدوي هتكمّل عادي.
الـ trade-off: Recursion ولا حلقة؟
الـ Recursion بتكسب كود أوضح وأقصر في المسائل المتداخلة (الأشجار، البحث في المجلدات)، بتخسر ذاكرة على المكدس بمقدار O(n) إطارات زائد بطء بسيط. الحلقة بتكسب ذاكرة ثابتة وسرعة أعلى، بتخسر شوية وضوح. القاعدة: لو العمق بالعشرات فالـ recursion آمنة، لو ممكن يوصل للمئات أو الآلاف فكّر مرتين.
متى لا تستخدم الـ Recursion
ابعد عنها لما العمق ممكن يعدّي المئات، أو لما تقدر تحلها بحلقة بسيطة، أو في كود حرج للأداء داخل حلقات ساخنة. الافتراض هنا مبني على بايثون بحدّه 1000؛ لغات تانية ليها حدود مختلفة، وبعضها بيعمل Tail Call Optimization وبايثون مش منهم.
الخطوة التالية
افتح أي دالة recursive عندك ودوّر على سطر حالة الأساس. لو مش لاقيه، ده باج مستني يحصل. وبعدين انسخ دالة factorial اللي فوق، غيّر شرط التوقف من n == 0 لـ n == -1 ونادِها بـ factorial(5)، وهتشوف RecursionError بعينك.
المصادر
- توثيق بايثون: docs.python.org/3/library/sys.html#sys.setrecursionlimit
- مسرد بايثون: docs.python.org/3/glossary.html
- Real Python: realpython.com/python-recursion
- Wikipedia Call stack: en.wikipedia.org/wiki/Call_stack
- Wikipedia Stack overflow: en.wikipedia.org/wiki/Stack_overflow