مقدمة في علوم الحاسوب والبرمجة باستخدام Python › المحاضرة 15: الاستدعاء الذاتي (Recursion) 17 / 30 السابق التالي

شرائح المحاضرة 15: الاستدعاء الذاتي

شرائح المحاضرة 15: الاستدعاء الذاتي (Recursion)

المصدر: الشرائح الأصلية، PDF، وكود المحاضرة.

إعداد الأصل: Ana Bell / MIT OpenCourseWare، معهد ماساتشوستس للتكنولوجيا، مقرر 6.100L، خريف 2022. هذه ترجمة وتكييف عربيان غير رسميين للاستخدام غير التجاري، ولا يعنيان اعتماد MIT. الترخيص: CC BY-NC-SA 4.0، باستثناء مواد الأطراف الثالثة المحددة في الأصل. أرقام الأقسام هي أرقام الشرائح، بما فيها الشرائح المتكررة التي توضّح خطوات العرض.

الشريحة 1 — الاستدعاء الذاتي

نزّل الشرائح وملفات .py للمتابعة. المحاضرة 15 من 6.100L — Ana Bell.

الشريحة 2 — الخوارزميات التكرارية حتى الآن

  • تؤدي بُنى الحلقات، أي حلقات while وfor، إلى خوارزميات تكرارية (Iterative algorithms).
  • يمكن تمثيل الحساب بمجموعة من متغيرات الحالة (State variables)، تتحدث وفق قواعد في كل دورة من الحلقة:
    • ما الذي يتغير كل مرة، وكيف؟
    • كيف أتتبع عدد مرات المرور بالحلقة؟
    • متى يمكنني التوقف؟
    • أين تكون النتيجة عندما أتوقف؟

الشريحة 3 — الضرب

  • ينفّذ المعامل * ذلك لنا.
  • أنشئ دالة:
def mult(a, b):
    return a*b

الشريحة 4 — الضرب: فكّر باستخدام التكرار

  • هل تستطيع جعل هذه العملية تكرارية؟
  • عرّف a*b بوصفه a+a+a+a...، عدد b من المرات.
  • اكتب دالة:
def mult(a, b):
    total = 0
    for n in range(b):
        total += a
    return total

الشريحة 5 — الضرب: حل تكراري آخر

  • «اضرب a * b» يكافئ «اجمع b نسخ من a».
  • مثّل الحالة باستخدام:
    • رقم دورة i يبدأ من b: حدّثه وفق i ← i-1 وتوقف عند الصفر.
    • القيمة الحالية للحساب result التي تبدأ من الصفر: حدّثها وفق result ← result + a.

وصف المترجم للمخطط: على امتداد a + a + a + a + … + a، تشير الأسهم إلى تقدم الحساب. تكون قيم result بالتتابع 0 ثم a ثم 2a ثم 3a ثم 4a، مع الانتقال إلى الحد التالي في كل دورة.

def mult_iter(a, b):
    result = 0
    while b > 0:
        result += a
        b -= 1
    return result

الشريحة 6 — الضرب: لاحظ الأنماط العودية

  • لاحظ أن لدينا مسألة نحلها مرات كثيرة.
  • إذا كان a = 5 وb = 4، فإن 5*4 هي 5+5+5+5.
  • فكّك المسألة الأصلية إلى شيء تعرفه وإلى المسألة نفسها مجددًا.
  • المسألة الأصلية تستخدم * بين عددين:
    • 5*4 هي 5+5*3.
    • لكن هذا يساوي 5+5+5*2.
    • وهذا يساوي 5+5+5+5*1.

الشريحة 7 — الضرب: ابحث عن نسخ أصغر من المسألة

  • لاحظ أن لدينا مسألة نحلها مرات كثيرة.
  • إذا كان a = 5 وb = 4، فإن 5*4 هي 5+5+5+5.
  • فكّك المسألة الأصلية إلى شيء تعرفه وإلى المسألة نفسها مجددًا.
  • المسألة الأصلية تستخدم * بين عددين:
5*4
= 5+(    5*3    )
= 5+(5+( 5*2 ))
= 5+(5+(5+(5*1)))

الشريحة 8 — الضرب: ابحث عن نسخ أصغر من المسألة

  • لاحظ أن لدينا مسألة نحلها مرات كثيرة.
  • إذا كان a = 5 وb = 4، فإن 5*4 هي 5+5+5+5.
  • فكّك المسألة الأصلية إلى شيء تعرفه وإلى المسألة نفسها مجددًا.
  • المسألة الأصلية تستخدم * بين عددين:
5*4
= 5+(    5*3    )
= 5+(5+( 5*2 ))
= 5+(5+(5+(5*1)))

الشريحة 9 — الضرب: ابحث عن نسخ أصغر من المسألة

  • لاحظ أن لدينا مسألة نحلها مرات كثيرة.
  • إذا كان a = 5 وb = 4، فإن 5*4 هي 5+5+5+5.
  • فكّك المسألة الأصلية إلى شيء تعرفه وإلى المسألة نفسها مجددًا.
  • المسألة الأصلية تستخدم * بين عددين:
5*4
= 5+(    5*3    )
= 5+(5+( 5*2 ))
= 5+(5+(5+(5*1)))

الشريحة 10 — الضرب: وصلنا إلى النهاية

  • لاحظ أن لدينا مسألة نحلها مرات كثيرة.
  • إذا كان a = 5 وb = 4، فإن 5*4 هي 5+5+5+5.
  • فكّك المسألة الأصلية إلى شيء تعرفه وإلى المسألة نفسها مجددًا.
  • المسألة الأصلية تستخدم * بين عددين:
5*4
= 5+(    5*3    )
= 5+(5+( 5*2 ))
= 5+(5+(5+(5*1)))

الشريحة 11 — الضرب: أعد بناء النتيجة صعودًا

  • لاحظ أن لدينا مسألة نحلها مرات كثيرة.
  • إذا كان a = 5 وb = 4، فإن 5*4 هي 5+5+5+5.
  • فكّك المسألة الأصلية إلى شيء تعرفه وإلى المسألة نفسها مجددًا.
  • المسألة الأصلية تستخدم * بين عددين:
5*4
= 5+(    5*3    )
= 5+(5+( 5*2 ))
= 5+(5+(5+( 5 )))

الشريحة 12 — الضرب: أعد بناء النتيجة صعودًا

  • لاحظ أن لدينا مسألة نحلها مرات كثيرة.
  • إذا كان a = 5 وb = 4، فإن 5*4 هي 5+5+5+5.
  • فكّك المسألة الأصلية إلى شيء تعرفه وإلى المسألة نفسها مجددًا.
  • المسألة الأصلية تستخدم * بين عددين:
5*4
= 5+(    5*3    )
= 5+(5+( 10    ))
= 5+(5+(5+( 5 )))

الشريحة 13 — الضرب: أعد بناء النتيجة صعودًا

  • لاحظ أن لدينا مسألة نحلها مرات كثيرة.
  • إذا كان a = 5 وb = 4، فإن 5*4 هي 5+5+5+5.
  • فكّك المسألة الأصلية إلى شيء تعرفه وإلى المسألة نفسها مجددًا.
  • المسألة الأصلية تستخدم * بين عددين:
5*4
= 5+(    15     )
= 5+(5+( 10    ))
= 5+(5+(5+( 5 )))

الشريحة 14 — الضرب: الخطوة العودية والحالة الأساسية

  • الخطوة العودية (Recursive step): قرر كيف تختزل المسألة إلى نسخة أبسط أو أصغر من المسألة نفسها، مع عمليات بسيطة إضافية.
a*b = a + a + a + a + … + a
    = a + (a + a + a + … + a)
    = a + a * (b-1)

وصف المترجم للمخطط: القوس الأول يحدد b مرات من a، ثم يُفصل أول a، ليصبح تحت القوس الآخر b-1 مرات. السهم الموسوم «الاختزال العودي» يصل هذا الجزء بالتعبير a * (b-1).

الشريحة 15 — الضرب: الخطوة العودية والحالة الأساسية

  • الخطوة العودية: قرر كيف تختزل المسألة إلى نسخة أبسط أو أصغر من المسألة نفسها، مع عمليات بسيطة إضافية.
a*b = a + a + a + a + … + a
    = a + (a + a + a + … + a)
    = a + a * (b-1)
  • الحالة الأساسية (Base case): استمر في اختزال المسألة حتى تصل إلى حالة بسيطة يمكن حلها مباشرة.
  • عندما b=1، يكون a*b=a.

وصف المترجم للمخطط: يتكرر اختزال عدد الحدود من b إلى b-1 الموضح في الشريحة السابقة، وتُضاف هنا قاعدة التوقف عند b=1.

الشريحة 16 — الضرب: الكود العودي

إحالة الشريحة: Python Tutor.

  • الخطوة العودية: إذا كان b != 1، فإن a*b = a + a*(b-1).
  • الحالة الأساسية: إذا كان b = 1، فإن a*b = a.
def mult_recur(a, b):
    if b == 1:
        return a
    else:
        return a + mult_recur(a, b-1)

الشريحة 17 — مثال واقعي: استدعاء دالة واحد فقط

طالب يطلب إعادة التصحيح.

الطريقة التكرارية:

  • يسأل الطالب الأستاذ، ثم مساعد التدريس (TA)، ثم مساعد التعلّم (LA)، ثم المصحّح، واحدًا تلو الآخر، حتى يعيد واحد أو أكثر تصحيح الامتحان أو أجزاء منه.
  • يمر الطالب على الجميع ويتتبع الدرجة الجديدة.

وصف المترجم بدل الصور المستثناة: يظهر الطالب متصلًا بكل شخص على حدة؛ يرسل إلى كل منهم «إعادة التصحيح، من فضلك؟» ويستقبل «تفضل النتيجة». الطالب نفسه يدير التتابع ويجمع النتيجة، ولا يطلب هؤلاء الأشخاص من بعضهم تنفيذ المهمة.

إشعار حقوق الصور في الأصل: صورة فتاة الميم © مصدر غير معروف؛ صورة المرأة © 2007 NBC Universal؛ Willy Wonka © 1971 Warner Bros. Entertainment Inc.؛ لقطة Bridesmaids © 2011 Universal Studios؛ لقطة Cocoon © 2011 Universal Studios. جميع الحقوق محفوظة. هذا المحتوى مستثنى من ترخيص المشاع الإبداعي؛ معلومات الاستخدام العادل لدى OCW. لم تُنقل أي من هذه الصور إلى الترجمة.

الشريحة 18 — مثال واقعي: استدعاءات دوال كثيرة

طالب يطلب إعادة التصحيح.

الطريقة العودية:

  1. طلب الطالب، أي استدعاء دالة لإعادة التصحيح:
    • يطلب من الأستاذ إعادة التصحيح.
    • يطلب الأستاذ من مساعد التدريس إعادة التصحيح.
    • يطلب مساعد التدريس من مساعد التعلّم إعادة التصحيح.
    • يطلب مساعد التعلّم من المصحّح إعادة التصحيح.
  2. تمرير النتائج، أي إن الدوال تُرجع النتائج إلى من استدعاها:
    • يخبر المصحّح مساعد التعلّم بالدرجة.
    • يخبر مساعد التعلّم مساعد التدريس بالدرجة.
    • يخبر مساعد التدريس الأستاذ بالدرجة.
    • يخبر الأستاذ الطالب بالدرجة.

وصف المترجم بدل الصور المستثناة: سلسلة الطلبات هي الطالب ← الأستاذ ← مساعد التدريس ← مساعد التعلّم ← المصحّح، ومعنى كل وصلة إلى المستوى التالي «إعادة التصحيح، من فضلك؟». تعود الإجابة «تفضل النتيجة» على الوصلات بالترتيب العكسي، حتى الطالب. الاتجاه المقصود للطلب هو من الطالب إلى المصحّح، وللنتيجة من المصحّح إلى الطالب.

إشعار حقوق الصور في الأصل هو نفسه في الشريحة 17: فتاة الميم، وصورة المرأة © 2007 NBC Universal، وWilly Wonka © 1971 Warner Bros. Entertainment Inc.، ولقطتا Bridesmaids وCocoon © 2011 Universal Studios. جميع الحقوق محفوظة؛ الصور مستثناة من ترخيص المشاع الإبداعي ولم تُعَد هنا. معلومات OCW.

الشريحة 19 — الفكرة الكبرى

تنتظر استدعاءات الدوال «الأسبق» النتائج قبل أن تكتمل.

الشريحة 20 — ما الاستدعاء الذاتي؟

  • خوارزميًا: طريقة لتصميم حلول للمسائل بأسلوب «قسّم وتغلّب» (Divide-and-conquer) أو «قلّص وتغلّب» (Decrease-and-conquer).
    • اختزل المسألة إلى نسخ أبسط من نفسها، أو إلى مسألة يمكن حلها مباشرة.
  • من ناحية الدلالة البرمجية: تقنية تستدعي فيها الدالة نفسها.
    • في البرمجة، الهدف ألّا يكون الاستدعاء الذاتي لا نهائيًا.
    • لا بد من حالة أساسية أو أكثر يسهل حلها مباشرة.
    • لا بد من حل المسألة نفسها على مدخل آخر، بغرض تبسيط المسألة ذات المدخل الأكبر، وانتهاءً بالحالة الأساسية.

الشريحة 21 — جرّب بنفسك!

أكمل الدالة التي تحسب n مرفوعًا إلى القوة p، أي nᵖ، للمتغيرين n وp:

def power_recur(n, p):
    if _______:
        return ______
    elif _______:
        return ______
    else:
        return _________________

الشريحة 22 — المضروب (Factorial)

n! = n*(n-1)*(n-2)*(n-3)* … * 1
  • لأي n نعرف المضروب؟ عند n=1:
if n == 1:
    return 1
  • كيف نختزل المسألة؟ أعد كتابتها باستخدام شيء أبسط للوصول إلى الحالة الأساسية: n*(n-1)!.
else:
    return n*fact(n-1)

الشريحة 23 — مثال نطاق الدالة العودية (Scope)

def fact(n):
    if n == 1:
        return 1
    else:
        return n*fact(n-1)

print(fact(4))

وصف المترجم للمخطط: في النطاق العام (Global scope)، يرتبط الاسم fact بالكود. تُنشأ نطاقات مستقلة: نطاق fact باستدعاء n=4، ثم n=3، ثم n=2، ثم n=1. لكل نطاق اسم n الخاص به وقيمته. يُرجع الأخير 1، وهي الحالة الأساسية؛ ينتظر السابق 2*fact(1) ثم يُرجع 2*1، ويليه 3*fact(2) ثم 3*2، ويليه 4*fact(3) ثم 4*6. يعود التنفيذ إلى print(fact(4)) فيصبح print(24).

الشريحة 24 — الفكرة الكبرى

في الاستدعاء الذاتي، يكون كل استدعاء دالة منفصلًا تمامًا.

نطاقات وبيئات منفصلة، وأسماء متغيرات منفصلة. استقلال تام.

الشريحة 25 — بعض الملاحظات

إحالة الشريحة: Python Tutor للمضروب.

  • يُنشئ كل استدعاء عودي للدالة نطاقه وبيئته الخاصة.
  • لا تتغير ارتباطات المتغيرات (Bindings) في نطاق بسبب الاستدعاء العودي للدالة نفسها.
  • تحجب قيم ارتباطات المتغيرات ارتباطاتها في الإطارات الأخرى (Frames).
  • يعود تدفق التحكم (Control flow) إلى النطاق السابق بمجرد أن يُرجع استدعاء الدالة قيمة.

الشريحة 26 — التكرار مقابل الاستدعاء الذاتي

التكرار:

def factorial_iter(n):
    prod = 1
    for i in range(1,n+1):
        prod *= i
    return prod

الاستدعاء الذاتي:

def fact_recur(n):
    if n == 1:
        return 1
    else:
        return n*fact_recur(n-1)

تعليق الشريحة على النسخة العودية: «هذه النسخة أقرب كثيرًا إلى أسلوب Python (Pythonic)!»

  • قد يكون الاستدعاء الذاتي كفؤًا من منظور المبرمج.
  • قد لا يكون كفؤًا من منظور الحاسوب.

الشريحة 27 — متى تستخدم الاستدعاء الذاتي؟

رأينا حتى الآن كودًا بسيطًا جدًا.

  • لم يحتج ضرب عددين إلى دالة عودية، ولم يحتج حتى إلى دالة تكرارية!
  • كان تنفيذ المضروب بالاستدعاء الذاتي أكثر بداهة بعض الشيء.
    • ترجمنا معادلة رياضية أخبرتنا بالبنية المطلوبة.
  • معظم المسائل لا تحتاج إلى الاستدعاء الذاتي لحلها.
    • إذا كان التكرار أكثر بداهة لك، فحلّها باستخدام الحلقات!
  • تؤدي بعض المسائل إلى كود أبسط كثيرًا باستخدام الاستدعاء الذاتي:
    • البحث في نظام ملفات عن ملف محدد.
    • تقييم تعبيرات رياضية تستخدم الأقواس لتحديد ترتيب العمليات.

وصف المترجم للمخطط: مجلد Tom يتفرع إلى Data وThesis وNotes.txt وTools. يحتوي Data على One.txt وTwo.txt، ويحتوي Tools على Format وStats وOld. يمكن تطبيق البحث نفسه على كل مجلد فرعي. ويوضح مخطط ترتيب العمليات الاختصار PEMDAS: الأقواس (Parentheses)، الأسس (Exponents)، الضرب والقسمة (Multiplication/Division)، ثم الجمع والطرح (Addition/Subtraction).

الشريحة 28 — خلاصة الشريحة الأصلية

  • الاستدعاء الذاتي:
    • أسلوب برمجة.
    • طريقة للتقسيم والتغلّب.
  • تستدعي الدالة نفسها.
  • تُفكك المسألة إلى حالة أساسية وخطوة عودية.
  • الحالة الأساسية:
    • شيء تعرفه.
    • ستصل إلى هذه الحالة في النهاية؛ وإلا فلديك استدعاء ذاتي لا نهائي.
  • الخطوة العودية:
    • المسألة نفسها.
    • مختلفة قليلًا على نحو يؤدي في النهاية إلى الحالة الأساسية.

الشريحة 29 — إشعار المصدر

MIT OpenCourseWare — https://ocw.mit.edu. مقرر 6.100L: مقدمة في علوم الحاسوب والبرمجة باستخدام Python، خريف 2022. لمعلومات الاستشهاد بهذه المواد وشروط الاستخدام: https://ocw.mit.edu/terms.