مقدمة في علوم الحاسوب والبرمجة باستخدام Python › المحاضرة 12: فهم القوائم، والدوال ككائنات، والاختبار، وتصحيح الأخطاء 14 / 30 السابق التالي

النص الكامل للمحاضرة 12: استيعاب القوائم والدوال بوصفها كائنات والاختبار وتنقيح الأخطاء

النص الكامل للمحاضرة 12: استيعاب القوائم والدوال بوصفها كائنات والاختبار وتنقيح الأخطاء

المصدر الأصلي: النص الرسمي الإنجليزي، PDF، المحاضرة والفيديو الرسمي.

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

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

صفحة المصدر 1

MITOCW | المحاضرة 12 من 6.100L، النسخة المتعددة، الإصدار 4.mp4

[صرير]

[حفيف]

[نقرات]

آنا بيل (ANA BELL):

حسنًا. لنبدأ محاضرة اليوم. ستكون محاضرة أكثر استرخاءً مما فعلناه سابقًا، مع أن لدينا عددًا لا بأس به من الأشياء التي سنتناولها، كما ترون في شريحة العنوان. لن أتحدث بسرعة كبيرة جدًا، لذلك لا تترددوا في طرح الكثير من الأسئلة. وسيكون النصف الثاني من المحاضرة هادئًا جدًا، لأننا سنتحدث عن استراتيجيات الاختبار (Testing) وتنقيح الأخطاء (Debugging). فهو موضوع على مستوى عام جدًا.

لكن أولًا سنُحكم بعض الأمور المعلّقة المتعلقة بالقوائم (Lists) والدوال (Functions). لن نقدّم الكثير من الصيغ الجديدة. هذه الأفكار اختيارية أكثر في برمجتكم اليومية، لكن من الجميل جدًا جدًا معرفتها. لنبدأ أولًا بالحديث عن فكرة استيعاب القوائم (List Comprehension).

كنتم تكتبون دوال تتعامل مع القوائم. وهناك نمط شائع جدًا آمل أنكم رأيتموه حتى الآن، وهو التالي. هذا الكود هنا يعرض شيئًا برمجناه بالتأكيد معًا، وبرمجتموه بالتأكيد في التمارين القصيرة والاختبارات. لذلك فهو نمط شائع جدًا.

الفكرة هنا أن لديك دالة تنشئ قائمة جديدة، وتكون عناصر هذه القائمة الجديدة دالةً في قائمة المدخلات. حسنًا؟ النمط هنا هو أننا ننشئ قائمة جديدة فارغة داخل الدالة. ولدينا حلقة تمرّ على كل عنصر في المدخلات، ونطبق الدالة نفسها على كل واحد من هذه العناصر. في هذه الحالة بالذات نأخذ ذلك العنصر ونربّعه.

ونُلحق كل واحد من هذه العناصر بهذه القائمة الجديدة، التي كانت فارغة في الأصل، إلى أن نصل — إلى أن نكون قد طبّقنا هذه الدالة على كل عنصر في L. ثم نُرجع هذه القائمة المنشأة حديثًا. ولأن هذا أمر شائع جدًا يفعله المبرمجون، تسمح لك Python بأداء الوظيفة نفسها تمامًا بسطر واحد من الكود. والطريقة التي نفعل بها ذلك هي استخدام ما يسمى استيعاب القوائم.

طريقة تنفيذ استيعاب قائمة، أي أخذ هذه الأسطر الأربعة من الكود في هذه الدالة، هي أننا سنكتبها في هذا السطر الواحد الذي يبدو شيئًا كهذا. الفكرة هنا أننا باستخدام هذا السطر الواحد سننشئ قائمة جديدة. وسيكون لدينا مكرّر (Iterator) يمرّ على تسلسل ما من القيم. وسنطبق الدالة نفسها على كل واحد من تلك العناصر. والجزء الاختياري الآخر الذي نستطيع إضافته داخل استيعاب القائمة هو تطبيق تلك الدالة فقط إذا تحقق شرط ما.

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

التعبير — آسفة، الدالة التي سنطبقها على كل عنصر في L هي أخذ ذلك العنصر وتربيعه. على الجانب الأيمن هنا في استيعاب القائمة لدينا e تربيع. وما e؟ ستكون كل عنصر e في L.

صفحة المصدر 2

إذا قرأنا هذا بالإنجليزية نقول أساسًا إن Lnew ستحتوي على العناصر e تربيع لكل e في L. يبدو ذلك غريبًا، لكنه منطقي نوعًا ما حتى إن قرأناه بالإنجليزية. ووراء الكواليس ستأخذ Python كل عنصر واحدًا تلو الآخر وتربّعه. وهذا هو تسلسل العناصر الذي ستملأ به Lnew.

حسنًا. ماذا لو أضفنا شرطًا إلى ذلك؟ لنفترض أننا نريد إنشاء هذه القائمة الجديدة من العناصر للعناصر الزوجية فقط. نريد تربيع العناصر الزوجية داخل قائمتي الأصلية L فقط. إذا أردنا كتابة دالة تفعل ذلك، فعلينا إضافة هذا الشرط الإضافي هنا. كل شيء آخر هو نفسه باستثناء if e%2==0. يخبر هذا Python بأن تأخذ فقط العناصر الزوجية، القابلة للقسمة على 2.

كيف نكتب هذا بصيغة استيعاب القوائم؟ هذه قائمة جديدة. وهذه الدالة التي نطبقها فقط إذا كان الاختبار صحيحًا. في استيعاب القائمة، هذه قائمتي الجديدة. حلقة for عندي موجودة هنا. والاختبار الذي نطبقه موجود في النهاية هنا، if e%2==0. وما الدالة التي نطبقها؟ إنها e تربيع فقط، كما من قبل. إذًا يُضاف الاختبار فقط إلى نهاية تعبير استيعاب القائمة هنا. نعم.

أحد الحضور (AUDIENCE):

هل يعمل أسرع؟ هل هناك سبب لفعل ذلك؟

آنا بيل (ANA BELL):

هل يعمل أسرع؟ لست متأكدة في الحقيقة. ربما يعمل أسرع بدرجة طفيفة، لكن على الأرجح ليس بفارق كبير. السبب هو أنك مع اكتساب مزيد من التدريب عليه ستجد هذا أسهل قراءةً في الكود. وكثيرًا ما إذا رأيت كتلة كبيرة كهذه ستفقد التركيز. لن ترغب في قراءة كتلة كهذه.

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

حسنًا. استيعابات القوائم مفيدة إلى حد كبير. إذا تدربت عليها قليلًا فستجد نفسك تستخدمها في كل مكان. وهي تستبدل أساسًا كودًا يبدو هكذا. هذه الأسطر من الكود طريقة عامة جدًا لكتابة استيعاب القائمة هذا في سطر واحد.

هنا لدي دالة f أريد تطبيقها. تعبير expr هذا هو الدالة التي أريد تطبيقها على كل عنصر، وهذه هي القائمة التي أريد تطبيق تلك الدالة عليها، والاختبار سيكون الشرط. في هذه الحالة بالتحديد يعني هذا الاختبار أنني أطبقها على كل عنصر، لكن يمكنكم تخيّل وجود دالة نقول فيها، في الحالة السابقة، lambda x: x%2==0 بوصفها شرطنا.

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

لننظر إلى أمثلة ملموسة. هنا لا أطبق دالة e تربيع على مجموعة محددة من عناصر قائمة. أطبقها على تسلسل القيم الذي تعطيه range. تذكروا، عندما تحدثنا عن مرور حلقات for على الأشياء، يمكنها المرور على أعداد صحيحة تتبع نمطًا ما، مثل range(6) أو range(1, 9, 2) أو شيء كهذا.

صفحة المصدر 3

ما دام لديك تسلسل من القيم تستطيع المرور عليه، يمكنك وضعه في استيعاب القائمة هذا. تستطيع المرور على قوائم، أو على صفوف (Tuples)، أو على نطاقات range المباشرة هذه، أو على نطاق بطول قائمة. أي شيء ينشئ لك كائنًا قابلًا للتكرار (Iterable) تستطيع وضعه في استيعاب القائمة.

في هذه الحالة بالتحديد أقرأه على النحو التالي: لدي شيء أربّعه. وما الشيء الذي أربّعه؟ سيكون كل قيمة في range(6). أفكر فيه هكذا: ما تسلسل القيم الذي سأعمل عليه؟ سيكون الأعداد 0, 1, 2, 3, 4, 5. وما سأفعله بها هو تربيع كل قيمة من تلك القيم. إذًا القائمة النهائية التي أحصل عليها من هذا السطر الواحد هنا قائمة تحتوي على 0 تربيع، و1 تربيع، و2 تربيع، و3 تربيع، و4 تربيع، و5 تربيع.

يمكننا إضافة شرط إلى ذلك. هنا لدي كل عنصر تربيع لكل e في range(8)، فقط إذا كانت e زوجية. وطريقة تفكيري في هذا هي: لنبدأ بكل عنصر في النطاق. إنها 0, 1, 2, 3, 4, 5, 6, 7.

الشرط الذي أطبقه هو أن يكون زوجيًا. الأعداد التي سأنتهي بها، بعدما أصفّي كل تلك الأعداد، ستكون فقط 0, 2, 4, 6، لأننا نصعد إلى 8 دون تضمينها. ثم سأربّع كل واحد منها. إذًا النتيجة النهائية من استيعاب القائمة هذا هي قائمة تحتوي على العناصر 0 تربيع، و2 تربيع، و4 تربيع، و6 تربيع.

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

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

ولدي شرط هنا. لدي العناصر 0, 1, 2, 3. هذا هو النطاق. لكنني آخذ العناصر الفردية فقط في هذه الحالة بالتحديد. إذًا مجموعة الأعداد الناتجة التي سأطبق عليها هذا ستكون العدد — ستكون العددين 1 و3، لأنهما العددان الفرديان في range(4).

لذلك ستحتوي القائمة الناتجة على عنصرين. هذا القوس المربع الخارجي هو القائمة التي أنشأتها، وستكون عناصرها العنصر الذي مررت عليه فعلًا ومربعه في صورة قائمة. إذًا 1 و1 تربيع مقابل e وe تربيع عندما تكون e هي 1، ثم 3 و9، أي 3 تربيع، عندما تكون e هي 3.

هل هناك أسئلة عن ذلك؟ حسنًا. رائع إلى حد كبير. إنها طريقة جميلة جدًا لإنشاء قوائم بسرعة شديدة. مثلًا، إذا أردت إنشاء قائمة مليئة بالأصفار، مليئة بمئة صفر، فلا حاجة إلى حلقة. تنفّذ أساسًا استيعاب قائمة يقول: قوسان مربعان، 0 for e in range(101) — أو 100. ثم تصبح لديك قائمة صغيرة لطيفة مليئة بمئة صفر.

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

صفحة المصدر 4

إذًا الخطوة الأولى: ما القيم التي أمرّ عليها؟ القيم كاملة، دون تضمين الشرط. ليقلها أحد بصوت عالٍ.

أحد الحضور (AUDIENCE):

تلك القائمة في الوسط.

آنا بيل (ANA BELL):

نعم. تلك القائمة في الوسط. رائع. حسنًا. إذًا xy وabcd، ثم 7، ثم ما الشيء الأخير؟ هل هو العدد 4.0 أم سلسلة نصية؟

أحد الحضور (AUDIENCE):

سلسلة نصية.

آنا بيل (ANA BELL):

نعم، بالضبط. 4.0. حسنًا. سلسلة نصية. سلسلة نصية. الخطوة الثانية. من هذه القائمة، ما القيم التي أحتفظ بها فعلًا بناءً على الشرط؟

أحد الحضور (AUDIENCE):

إذا كانت سلسلة نصية.

آنا بيل (ANA BELL):

إذا كانت سلسلة نصية. حسنًا. أيها سلسلة نصية؟ هل xy؟

أحد الحضور (AUDIENCE):

نعم.

آنا بيل (ANA BELL):

نعم. هل abcd؟

أحد الحضور (AUDIENCE):

نعم.

آنا بيل (ANA BELL):

نعم. هل 7؟

أحد الحضور (AUDIENCE):

لا.

آنا بيل (ANA BELL):

لا. هل 4.0؟

أحد الحضور (AUDIENCE):

نعم.

آنا بيل (ANA BELL):

نعم، ممتاز. حسنًا، جيد. حسنًا. هذه إذًا هي العناصر التي أحتفظ بها. والآن ما الدالة التي أطبقها، وما النتيجة التي ستكون؟ ستكون قائمة تحتوي على؟

أحد الحضور (AUDIENCE):

2 و4.

آنا بيل (ANA BELL):

نعم. 3 —

أحد الحضور (AUDIENCE):

2, 4.

آنا بيل (ANA BELL):

2, 4, 3. 2 لأن طول تلك 2، و4 لأن طول تلك 4، و3 لأن طول تلك 3. وصارت لدينا قائمة صغيرة لطيفة بناءً على ذلك الشرط، وتسلسل القيم ذاك، وتلك الدالة المطبقة. نعم؟

أحد الحضور (AUDIENCE):

لماذا تُرجع قائمة؟

آنا بيل (ANA BELL):

لماذا تُرجع قائمة؟

أحد الحضور (AUDIENCE):

نعم.

صفحة المصدر 5

آنا بيل (ANA BELL):

الشيء كله؟

أحد الحضور (AUDIENCE):

أو أظن أنني اعتقدت أنها ستُرجع فقط 2, 4, 3 في أسطر منفصلة.

آنا بيل (ANA BELL):

آه، نعم. نحن لا نطبع الأشياء هنا. عندما نكتب هذا بوصفه استيعاب قائمة، فنحن نطلب أساسًا من Python إنشاء هذه القائمة الناتجة من القيم. هذا ما يفعله استيعاب القوائم. وهذا التعبير هنا، مع هذين القوسين المربعين الخارجيين حول تعبيرنا كله، يخبر Python بأن الشيء الناتج قائمة. نعم. هذا سؤال جيد. أسئلة أخرى؟ حسنًا.

حسنًا. إذًا ذلك — آه، نعم. سؤال.

أحد الحضور (AUDIENCE):

هل يدعم شروطًا متعددة؟

آنا بيل (ANA BELL):

هل يدعم شروطًا متعددة؟ نعم. هنا في النهاية تقول if، ثم يمكنك إحاطتها بأقواس. لا أعرف إن كان ذلك لازمًا، لكن للاحتياط فقط سأحيط شروطي بأقواس. وتستخدم and أو or أو أي شيء تريد الجمع به بين التعبيرات أو الشروط. هل هناك سؤال؟ نعم؟

أحد الحضور (AUDIENCE):

أليست lambda [غير مسموع]؟

آنا بيل (ANA BELL):

هذه، lambda؟

أحد الحضور (AUDIENCE):

نعم.

آنا بيل (ANA BELL):

هنا، هذه دالة lambda التي تحدثنا عنها، نسيت متى. قبل بضع محاضرات. هي أساسًا دالة مجهولة الاسم (Anonymous Function)، وكل ما تفعله هو إرجاع True طوال الوقت. لذلك سيكون الاختبار صحيحًا دائمًا، وهذا يعني أننا عندما ننفّذ if test(e)، سيكون هذا صحيحًا دائمًا في هذه الحالة بالتحديد. لكن عندما تُعطى دالة lambda مختلفة فقد لا يكون الأمر كذلك.

حسنًا. لننتقل إلى الموضوع التالي. الموضوعان التاليان، أظن، سيتعلقان بالدوال. وأريد إنهاء أمرين هنا فقط لأعطيكم فكرتين إضافيتين عن الدوال.

الأول متعلق فعلًا بهذا السؤال الأخير، وهو فكرة المعامل الافتراضي (Default Parameter). ستكون هذه طريقة لإضافة معاملات إلى دوالنا تحصل على قيمة افتراضية ما، وهذا ما كان عليه شيء lambda فعلًا في ذلك المثال. لكن آمل أن يجعل هذا الجزء من المحاضرة ذلك أوضح قليلًا.

والجزء الثاني المتعلق بالدوال الذي سنتناوله هو فكرة الدوال بوصفها كائنات (Functions as Objects)، نبني عليها قليلًا. وسنرى ما يحدث عندما نُرجع كائن دالة من دالة أخرى. رأينا الدوال بوصفها معاملات لدوال أخرى، لكننا سنرى ما يحدث عندما تجعل دالةً القيمة المُرجعة من دالة أخرى.

لكن ذلك بعد قليل. الآن لننظر إلى المعاملات الافتراضية. حسنًا. رأينا هذا الكود من قبل. يستدعي ذكريات قديمة. هذه bisection_root. سأمرّ عليها فقط لنتذكر ما تفعله. لدينا هذا الكود داخل هذه الدالة التي كتبناها منذ وقت طويل جدًا. ثم قررنا تغليفه في دالة ليكون قطعة كود مفيدة جميلة جدًا نستطيع تشغيلها مرات كثيرة جدًا.

صفحة المصدر 6

معامل هذه الدالة كان x، وهي قيمة نريد تقريب جذرها التربيعي. والكود الذي نستخدمه للتقريب يستخدم خوارزمية البحث بالتنصيف (Bisection Search)، التي تهيئ بعض المتغيرات، وبالتحديد epsilon، أي مدى القرب الذي نريده من الإجابة النهائية. وطرفي المجال low وhigh. نتذكر ذلك. ثم تخمين أولي في منتصف المسافة بين low وhigh.

ثم نواصل وضع تخمينات بين low وhigh، تكون نقطة المنتصف بينهما، ما دمنا لسنا قريبين بما يكفي من النهائي — لسنا قريبين بما يكفي من الإجابة النهائية. سنعيد تهيئة إما الطرف الأدنى أو الطرف الأعلى بحسب ما إذا كان ذلك التخمين منخفضًا جدًا أو مرتفعًا جدًا، ثم داخل الحلقة نضع تخمينًا آخر باستخدام القيم التي تغيّرت، إما low أو high، بناءً على if أو else. ثم نواصل عملية وضع المزيد من التخمينات عند نقطة المنتصف ما دمنا أبعد من epsilon.

حسنًا. كانت هذه مراجعة لما فعلناه حتى الآن. الأمر المثير للاهتمام الذي فعلناه بهذه الدالة، أو عندما حوّلناها إلى دالة، كان إرجاع تقريبنا. هذا التخمين، بدلًا من طباعته فقط للمستخدم، أرجعناه ليكون مفيدًا في أجزاء أخرى من الكود. لذلك عندما استدعينا الدالة قلنا فقط اسم الدالة، ثم قيمة ما لـx.

هناك مواقف يريد فيها المستخدم تغيير قيمة epsilon. الآن، بالطريقة التي كتبنا بها هذا الكود، ضُبطت epsilon على 0.01. وكلما شغّلت الدالة فإنها تجد دائمًا تقريب الجذر التربيعي لـx بتلك الدقة، 0.01.

أحيانًا، بحسب التطبيق، قد يريد المستخدم تقريبًا أفضل، مثل 0.000001، أو قد لا يهتم بكل هذه الدقة ويريد تقريبًا إلى 1 أو إلى 0.5 أو شيء أكبر بكثير من 0.01. فما الخيارات في هذه الحالة بالتحديد لهذه السيناريوهات؟

أحد الخيارات، بالطبع، الدخول إلى دالتنا والقول: سأغيّر epsilon لتكون شيئًا بالغ الدقة، 0.000001. وهكذا يحصل الأشخاص الذين يستدعون هذه الدالة دائمًا على تقريب بتلك الدقة. لكن ماذا عن الأشخاص الذين لا يريدونه بتلك الدقة؟ ستتأثر جميع استدعاءات الدالة بهذا التغيير. وهذا ليس مرغوبًا حقًا. نريد أن نجعل الشخص الذي يستدعي الدالة مسؤولًا عن الدقة التي يريدها.

خيار آخر هو وضع epsilon خارج الدالة. فنقول: حسنًا، المعامل الوحيد سيكون x. ولنضبط الأمر بحيث لا نعيّن epsilon داخل الدالة. لندع المستخدم ربما يعيّن epsilon خارج الدالة. ثم يستطيع استخدام — ثم سيصعد كودنا أساسًا مستوى واحدًا إلى النطاق العام (Global Scope) ويستخدم epsilon التي اختارها المستخدم.

هذه ليست فكرة جيدة؛ لأننا بمجرد السماح لشخص يستخدم كودنا بإنشاء متغيراته الخاصة داخل كودنا، نضع ثقتنا في يد شخص آخر، وقد ينسى إعادة تعيين epsilon، أو ينسى تعيينها أصلًا. لذلك فإن استخدام متغيرات عامة (Global Variables) ليس فكرة جيدة من البداية. نريد الاحتفاظ بالتحكم في epsilon المستخدمة داخل دالتنا.

لذلك، كما تتوقعون، الخيار الأخير سيكون أفضل خياراتنا. لنُضف epsilon فقط بوصفها معاملًا آخر للدالة. ها هي. لدينا bisection_root مجددًا بوصفها دالة. ولدينا معامل x. ولدينا epsilon بوصفها معاملًا ثانيًا يستطيع المستخدم استدعاء الدالة به.

صفحة المصدر 7

حسنًا. بخلاف ذلك، جسم الدالة (Function Body) هو نفسه تمامًا، إلا أننا الآن عندما نستدعي الدالة يجب أن نمرّر epsilon بوصفها المعامل الثاني. من ناحية الكود، هذه bisection_root مع epsilon بوصفها معاملًا. والآن يستطيع المستخدم إيجاد تقريب لـ123 بدقة 0.1. وهو 11.088، إن كنتم تتساءلون، ثم تقريب 223 بدقة 0.000001، وهو 11.0905.

هذا أفضل بكثير. يستطيع المستخدم الآن أن يكون مسؤولًا عن تحديد مدى قرب التقريب الذي يريده لكل قيمة من قيمه. لكن لاحظوا أن هذا الكود مطوّل قليلًا. وفي الحقيقة ربما لا يريد المستخدمون معظم الوقت أن يكونوا مسؤولين عن تعيين epsilon. ربما لا يعرفون ما قيمة epsilon الجيدة.

فكيف يعرفون أن عليهم اختيار 0.01 افتراضيًا؟ ربما يكون ذلك شيئًا تستطيع وضعه في مواصفات الدالة (Function Specification) لأي شخص يستخدم دالتك. لكنك ستعتمد على قراءة المستخدمين لمواصفاتك، وهذا مخيف قليلًا. لذلك، الوظيفة التي نريدها حقًا بدلًا من ذلك هي أن نقول: حسنًا، أريد كتابة دالة تأخذ بالفعل معاملين. لكن افتراضيًا يكون أحد هذين المعاملين شيئًا أحدده أنا، بوصفـي الشخص الذي يكتب هذه الدالة.

ما أريده حقًا هو أن تكون لـepsilon قيمة افتراضية ما، بحيث إن لم يعرف المستخدمون ما يستدعون الدالة به، يستخدم الكود تلك القيمة الافتراضية فقط. أما إن كان المستخدم أكثر خبرة ويعرف أنه يريد epsilon تساوي 1 مضروبًا في 10 أس سالب 10، أو أيًّا تكن، فيستطيع أن يكون مسؤولًا عن تعيينها.

معظم الوقت إذًا نريد استدعاء دالة bisection_root دون معامل epsilon، كي تستخدم قيمة افتراضية. لكننا أحيانًا نريد السماح للمستخدم بتعيين epsilon فعلًا. ولهذا نقدّم فكرة المعاملات بالكلمات المفتاحية (Keyword Parameters)، المعروفة أيضًا بالمعاملات الافتراضية. وتُعيّن هكذا.

تعريف دالة bisection_root ما يزال يأخذ الشيء الذي نريد تقريب جذره التربيعي، x، لكن المعامل الثاني هنا، epsilon، سيكون مساويًا لشيء داخل تعريف الدالة. نحن، بوصفنا الأشخاص الذين يكتبون هذه الدالة، سنقول إن القيمة الافتراضية لـepsilon هي 0.01.

يعني هذا أننا عندما نستدعي الدالة هنا في الأسفل، إذا استدعى المستخدم الدالة دون تمرير معامل ثانٍ صراحةً، فستستخدم Python القيمة الافتراضية التي عيّنها الشخص الذي كتب الدالة. ستشغّل Python إذًا bisection_root لـ123 مع كون epsilon هي 0.01. وإلا، إذا أراد المستخدم تجاوز قيمة epsilon تلك، يستطيع تمريرها بنفسه، وتُستبدل تلك القيمة الافتراضية 0.01 لتصبح 0.5.

وفي كودنا هنا، هذه bisection_root مع القيم الافتراضية. تستطيعون رؤية أنني إذا شغّلتها مع 123، فعلى الرغم من وجود معاملين هنا لدالة الجذر التربيعي بالتنصيف، لا تشتكي Python لأنها تستخدم epsilon بقيمة 0.01. أشغّلها إذًا، وتعمل تمامًا. لكن في السطر الثاني هنا، إذا أردت فعلًا استخدام 0.5 بوصفها قيمة epsilon، فإنها تتجاوز معاملي الافتراضي وتحسب الجذر التربيعي لـ123 مع كون epsilon هي 0.5.

هل هناك أسئلة حتى الآن؟ بعدما قدمنا المعاملات الافتراضية، هناك بعض القواعد المتعلقة باستدعاء الدوال. عندما تنشئ تعريف الدالة — هنا، عندما تكون أنت من يعرّف دالة وتقرر السماح ببعض المعاملات الافتراضية في قائمة معاملاتك، يجب أن يوضع كل معامل افتراضي في النهاية. لا تستطيع تبديل ترتيب هذه. لا تستطيع القول epsilon = 0.01, x. لن تسمح Python بذلك.

ملاحظة المترجم: يذكر النص الرسمي في المثال الثاني العدد 223 ثم النتيجة 11.0905؛ حُفظا كما وردا. هذه النتيجة تقارب جذر 123 لا 223، فلا ينبغي قراءة الزلة الشفهية بوصفها مساواة صحيحة.

صفحة المصدر 8

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

الأولى هنا تعرض ما يحدث عندما تعطي قيمًا لكل شيء ليس معاملًا افتراضيًا، وهو في هذه الحالة x فقط. إذا أعطيت قيمة للمعاملات غير الافتراضية فقط، تضبط Python المعاملات الافتراضية لكل شيء آخر. فلا مشكلة كبيرة.

وبدلًا من ذلك تستطيع تمرير — تمامًا كما فعلنا في الماضي عندما نكتب دوالنا الخاصة بمعاملات متعددة، تستطيع تمرير قيم لكل واحد من تلك المعاملات، واحدًا تلو الآخر، بالترتيب نفسه، سواء أكان افتراضيًا أم لا. وإذا مرّرت قيمًا لها جميعًا، فلن ترتبك Python، وستطابقها فقط واحدة تلو الأخرى.

وهناك تنويعات على ذلك. يمكنك دائمًا تمرير قيمة لاسم معامل. بالنظر إلى تعريف الدالة نستطيع رؤية أسماء المعاملات؛ المعاملان الشكليان اسماهما x وepsilon. لذلك عندما تستدعي دوالك، تستطيع إخبار Python صراحةً بشيء مثل x = 123, epsilon = 0.1. وإذا كانت لديك معاملات أكثر تقول ذلك المعامل يساوي أي قيمة تريد التشغيل بها.

وهذا لن يربك Python. وإذا فعلت ذلك بهذه الطريقة، تستطيع فعليًا فعله بأي ترتيب تريد، لأن Python ستعيّن كل واحد من هذه المتغيرات إلى أي شيء أخبرتها به. في أسوأ الأحوال، تفعل شيئًا كهذا: واحدًا تلو الآخر، تقول فقط ما المعامل الشكلي وقيمته، وعندئذ لن ترتبك Python.

لكن الطرق الموجودة في الأسفل هي حيث نواجه المشكلات. مثلًا إذا وضعت المعامل الافتراضي أولًا، ثم وضعت معاملًا فعليًا — آسفة، وضعت المعامل الافتراضي أولًا، ثم أي معامل غير افتراضي بعده، تعطي Python خطأ لأن المعاملات الافتراضية يجب أن تأتي بعد غير الافتراضية.

والأخيرة لا تعطي خطأ في الحقيقة. لكن تذكروا أن Python تطابق المعاملات واحدًا تلو الآخر. لذلك ستجد فعليًا تقريبًا للجذر التربيعي لـ0.001 بدقة epsilon تساوي 123، لأنها تربط المعاملات واحدًا تلو الآخر فقط. وهذا ليس خطأ، لكنه ليس تمامًا ما نريدها أن تفعله. هل هناك أسئلة عن هذا؟

حسنًا. لننتقل الآن إلى شيء آخر، إلى تفصيل آخر عن الدوال. سنعود إلى فكرة كون الدوال كائنات في Python. رسمت هذه الصورة عندما تعلمنا أول مرة عن الدوال بوصفها كائنات. لذلك سأرسمها مرة أخرى فقط لتذكيركم.

تذكروا أنه عندما ننشئ تعريف دالة، تنشئ Python كائنًا داخل الذاكرة. بمجرد أن نرى تعريف الدالة هذا فقط، لا تهتم Python بالكود الموجود هنا. لا يُشغّل هذا الكود. لا يُشغّل إلا عندما يُستدعى. وهنا لم أستدعِ دالة إطلاقًا.

كل ما تعرفه Python في هذه اللحظة هو أن هناك كائن دالة داخل الذاكرة واسمه even. وهذا هو نفسه تمامًا مثل إنشاء كائن عدد صحيح داخل الذاكرة وإعطائه الاسم r من خلال سطر كهذا، أو إنشاء كائن عدد عشري (Float) في الذاكرة وإعطائه الاسم pi. إنه مجرد كائن ما له اسم ما.

صفحة المصدر 9

وهذا يعني أننا نستطيع أن نكتب كودًا يبدو هكذا، ينشئ أساسًا اسمًا بديلًا (Alias) لكائن الدالة ذاك في الذاكرة. هنا يشير الاسم is_even إلى كائن الدالة ذاك. وأخبر Python بأنني أريد الإشارة إلى كائن الدالة ذاك باستخدام الاسم my_func أيضًا.

إذًا my_func وis_even اسمان يشيران إلى هذا الكائن في الذاكرة. ليس هذا استدعاء دالة. لا أحاول معرفة ما إذا كان عدد ما زوجيًا. أنا أعطي حرفيًا اسمًا آخر لهذه الدالة، لهذا الكود الذي يفعل هذا الشيء هنا.

حسنًا. وهذا يعني أنه إذا كان لدي اسمان يشيران إلى الكائن نفسه، وإذا استدعيت هذين الاسمين كما أفعل هنا مع بعض المعاملات، فستقول Python: سأشغّل الكود الذي يشير إليه هذان الاسمان بهذه المعاملات. سيشغّلان إذًا الكود الذي يشيران إليه، هذه is_even. لذلك ستُرجع فقط True أو False. رأينا هذا من قبل. تذكروا إذًا: مجرد اسم آخر لذلك الكائن في الذاكرة.

رأينا بالفعل كيف نستطيع تمرير دوال بوصفها معاملات لدوال أخرى. والآن سنرى ما يحدث عندما نُرجع دالة من دالة أخرى. نحن لا نُرجع استدعاء دالة هنا، بل نُرجع كائن دالة. في هذا الكود بالتحديد لدينا دالة واحدة فقط اسمها make_prod. ويحدث أن هناك بعض الأشياء التي تجري داخلها.

ما الأشياء التي ستفعلها هذه الدالة؟ هذه الدالة نفسها ستنشئ دالة أخرى. g هذه لا توجد إلا حين توجد make_prod. البرنامج الرئيسي — تستطيعون التفكير فيه على أنه هذا المستوى من الكود من حيث المسافة البادئة (Indentation). البرنامج الرئيسي لا يعرف g. لا تُعرّف g إلا داخل make_prod.

عندما نشغّل هذا البرنامج أول مرة كما هو، لا يُنفّذ أي استدعاء دالة. لذلك لا يعرف البرنامج الرئيسي شيئًا عن داخل make_prod. تنشئ make_prod دالتها الخاصة هنا. ثم كل ما تفعله هو إرجاع كائن الدالة هذا.

لاحظوا أنه ليس استدعاء دالة. لا توجد أقواس افتتاحية وختامية وبداخلها معامل. إنه الاسم g فقط. إنه كائن الدالة هذا. هذه هي النقطة الأساسية هنا.

لنشغّل الكودين، هذا وهذا. سيفعلان الشيء نفسه تمامًا. سيستدعيان make_prod مع بعض المعاملات. ثم سنرى ما يحدث عندما نُرجع g هذه. ولاحظوا أنه يبدو مختلفًا قليلًا بالفعل عما فعلناه من قبل.

نعم، لدينا استدعاء لـmake_prod هنا، لكننا وصلنا به نوعًا ما استدعاء دالة آخر بعد make_prod مباشرة. لدينا make_prod ثم قوسان و2، ثم قوسان و3، أي make_prod(2)(3). أفكر فيه نوعًا ما على أنه تسلسل (Chaining) لمجموعة من استدعاءات الدوال معًا. وهذا ممكن، كما سنرى عندما نتتبّع بيئات الدوال (Function Environments) التي تُنشأ؛ يصبح هذا ممكنًا لأن استدعاء الدالة make_prod يُرجع دالة في حد ذاته.

لنتتبّع الكود على اليسار بعناية شديدة، ثم سأتتبّع الكود على اليمين الذي سيفعل الشيء نفسه تمامًا. وآمل أن يوضح ذلك الالتباس إذا فعلناه مرتين. هذا هو الكود من اليسار. لنفترض أن لدينا هذا البرنامج نفسه هنا. لدي تعريف دالة واحد، ثم لدي استدعاء دالة واحد هنا. ثم سأطبع القيمة المُرجعة.

صفحة المصدر 10

بمجرد أن أشغّل الكود، تنشئ Python بيئتي العامة (Global Environment). وفي البيئة العامة، هذا هو نطاق (Scope) البرنامج الرئيسي. ما الذي لدينا؟ لدينا تعريف دالة واحد فيه بعض الكود. لا يهمني ما هو في هذه اللحظة، لأنه لا يوجد لدي استدعاء دالة.

الشيء التالي الذي عليّ فعله هو النزول إلى هنا والقول val تساوي. سأنشئ متغيرًا val في بيئتي العامة. وسأستدعي دالة. تُنفّذ استدعاءات الدوال من اليسار إلى اليمين، تمامًا مثل التعبيرات. وأول ما تراه Python هو استدعاء الدالة هذا، make_prod(2).

إنه استدعاء دالة، لذلك علينا إنشاء صندوق برتقالي آخر، لأن بيئة جديدة تُنشأ كلما استدعينا دالة. هنا لدي نطاقي، بيئتي الخاصة بـmake_prod. وأنا حاليًا عالقة هنا فقط، أحاول معرفة ما الذي سيُرجعه هذا، هذا الصندوق الأحمر هنا فقط.

كلما كان لدي استدعاء دالة، عليّ النظر إلى تعريف الدالة. ويقول تعريف الدالة إن هناك معاملًا شكليًا (Formal Parameter) واحدًا، a، يجب أن أربطه بالمعامل الفعلي (Actual Parameter). والشيء الذي أستدعي به make_prod هو 2. ينبغي أن يكون هذا واضحًا إلى حد كبير، صحيح؟

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

حتى الآن لا مشكلة. أريد منكم ملاحظة أن g هذه لا توجد إلا داخل هذا الاستدعاء لـmake_prod. البيئة العامة لا تعرف g في هذه اللحظة، لأننا لم نعرّف g إلا داخل make_prod. إنها هنا. لم أعرّفها خارج make_prod، لذلك لا يعرفها النطاق العام، لكن make_prod تعرفها.

والطريقة الوحيدة التي تستطيع بها البيئة العامة معرفة g هي أن تُرجع دالة make_prod هذه g بطريقة ما. فإذا مررنا g مرة أخرى بوصفها معاملًا — بوصفها قيمة، آسفة، إلى نطاق البرنامج الرئيسي، يستطيع البرنامج الرئيسي معرفة g. وإلا فـg عالقة نوعًا ما في هذه المهمة الفرعية الصغيرة، هذه البيئة الصغيرة لـmake_prod. ولا يعرفها البرنامج الرئيسي.

هذا ما يفعله هذا الكود. يقول أساسًا: أنشأت تعريفي، والآن أُرجع g. هنا ستُرجع g والكود المرتبط بها — أي هذا الكائن الذي تشير إليه g — إلى البرنامج الرئيسي. الآن يعرف البرنامج الرئيسي هذا الكائن g، الذي يرتبط به بعض الكود، هذا السطر هنا الذي يُرجع a*b.

الشيء الذي وضعت حوله صندوقًا أحمر هنا في الأسفل هو القيمة المُرجعة من make_prod(2). وقد أرجعت make_prod(2) الدالة g. تستطيع إذًا أساسًا القول إن هذا هو g. هل هذا واضح؟ هل هذا منطقي؟ نحن نمرّر الدوال، لا استدعاءات الدوال. هذه إذًا مجرد دالة اسمها g.

والآن، هذا السطر من الكود، val تساوي، إذا استبدلنا الصندوق الأحمر بـg، يصبح val = g(3). وg(3) استدعاء دالة آخر. واضح جدًا. ننظر إليه: إنه استدعاء دالة. فيه اسم دالة وأقواس ومعامل.

ولأنه استدعاء دالة، ننشئ نطاقًا آخر لهذا الاستدعاء. كما من قبل ننظر إلى ما تأخذه g بوصفه معاملًا. إنه متغير اسمه b، معامل شكلي b. ونربطه بـ3 لأن هذا هو استدعاء دالتنا، g(3).

صفحة المصدر 11

ثم علينا تنفيذ جسم g. يقول جسم g: أرجع a مضروبة في b. أعرف ما b. إنها 3، لأنك استدعيتني للتو بتلك القيمة. لكن ما a؟ لا توجد a داخل نطاق g.

بالعودة إلى محاضرتنا عن الدوال، إذا لم يعرف استدعاء دالة اسم متغير داخل بيئته، داخل نطاقه، فإنه يصعد تسلسل استدعاءات الدوال. فيقول: من استدعاني؟ أين عُرّفت g؟

عُرّفت g داخل make_prod، ولذلك استُدعيت من make_prod. هل لدى make_prod متغير اسمه a؟ نعم. وكانت قيمته 2. فلا نحتاج إلى الصعود أكثر في التسلسل. وجدنا بالفعل متغيرًا اسمه a. لذلك ستستخدم Python كون b هي 3 وa هي 2.

تضربهما فتكون النتيجة 6. ثم يستطيع استدعاء الدالة g إرجاع 6. يُرجعها إلى البرنامج الرئيسي، لأن هذا هو المكان الذي جرى فيه استدعاء الدالة. تذكروا أننا استبدلنا هذا بـg(3) هنا في هذا النطاق العام. فتُرجع 6 تلك إلى البرنامج الرئيسي، ثم تصبح val هي 6. ونطبع 6.

حسنًا. كان هذا يريكم كيفية وصل استدعاءات الدوال معًا. ولم يصبح هذا ممكنًا إلا لأن make_prod، بوصفها دالة، أرجعت كائن دالة آخر. لو أرجعت make_prod، لا أعرف، صفًا أو عددًا صحيحًا أو شيئًا ليس دالة، لفشل هذا الكود، لأن القيمة المُرجعة من make_prod ستكون — لنقل إنها أرجعت العدد 10.

تُستبدل القيمة المُرجعة من make_prod بـ10، ثم ترى Python هذا السطر على أنه 10(3). وما هذا أصلًا؟ وسيفشل تمامًا. إذًا لا يصبح هذا ممكنًا إلا بفضل حقيقة أن دالة make_prod هذه تُرجع كائن دالة. وهكذا نستطيع وصل استدعاءات الدوال هذه معًا.

لننظر إلى الكود نفسه تمامًا، لكن هذه المرة بدلًا من وصلها بالتتابع لنحفظ الخطوات الوسيطة (Intermediate Steps) صراحةً. ما سأفعله هو القول إنني سأحفظ make_prod(2) في متغير، ثم أجعل ذلك المتغير يستدعي 3، الجزء الثاني من سلسلتي في الشريحة السابقة. وسيفعل الشيء نفسه تمامًا.

هنا لدي النطاق العام. وكما من قبل لدي تعريف دالة لـmake_prod. هذا هو الاسم make_prod. يشير إلى بعض الكود. ثم لدي هذا المتغير doubler الذي سيساوي شيئًا. فهذا استدعاء دالة.

يقول استدعاء الدالة: هذه بيئتي لـmake_prod بنطاقها. في هذا النطاق بالتحديد لدي معاملي الشكلي a الذي يرتبط بـ2، ثم ينشئ جسم الدالة نفسه هذا المتغير g. هذا مجرد كود، كما من قبل تمامًا.

هل هناك أسئلة حتى الآن بناءً على ما حدث في المثال السابق وهنا، أم أن هذا واضح حتى الآن؟ حسنًا. جهزت الكود الآن، وهنا يأتي الجزء المثير للاهتمام. ستُنهي make_prod استدعاءها بالقول: سأُرجع شيئًا. والشيء الذي تُرجعه هو g.

تُرجع إذًا هذا الاسم، g. ويحدث أنه كائن دالة. لكن فكروا فيه كأي شيء آخر. نقول أساسًا doubler تساوي 10، أو doubler تساوي قائمة ما أو صفًا ما. ستكون doubler قيمة ما. هذه القيمة مجرد كود مرتبط بدالة.

ملاحظة المترجم: يخلط الشرح الشفهي هنا بين موضع الاستدعاء وموضع التعريف. البحث عن a في هذا المثال يعتمد على النطاق المحيط الذي عُرّفت فيه g، لا على هوية مستدعيها؛ وقد نُقل تسلسل كلام المتحدثة دون استبداله بشرح جديد.

صفحة المصدر 12

في نطاق برنامجي الرئيسي إذًا لدي doubler = g، وهو، بناءً على مخطط الذاكرة الذي رسمناه قبل خمس أو عشر شرائح، يشبه ما كان لدينا عندما قلنا my_func = is_even. لدي أساسًا اسمان لكائن الدالة نفسه. doubler اسم، وg الاسم الآخر. وكلاهما يشير إلى كائن الدالة هذا.

هل هذا منطقي؟ هل هذا واضح؟ حسنًا. بعدما صار لدي اسمان يشيران إلى كائن الدالة نفسه، نستطيع استخدام doubler هذه في السطر التالي. وdoubler هذه تشبه القول g(3)، إلا أنني أستخدم الاسم doubler الذي حفظتها به في السطر السابق.

إذًا g(3) استدعاء دالة آخر. أنشئ بيئة أخرى لـg أو doubler أو أي اسم. وهنا لدي معامل شكلي واحد b. قيمته 3. ثم نفعل الحيلة نفسها، حيث نسأل: ما قيمة a؟ سأبحث صعودًا في تسلسل الأشياء التي استُدعيت لأرى ما أول قيمة لـa أحصل عليها.

وأول قيمة لـa نحصل عليها هي 2. لذلك سنضرب 2 في 3. وتُرجع 6 تلك إلى من استدعاها، وهو هنا في نطاق البرنامج الرئيسي. فتكون val هذه مساوية لـ6. وهذا كل شيء.

أسئلة؟ أيهما كان أسهل فهمًا، هذا أم الذي وصلنا فيه الاستدعاءات؟ برفع الأيدي فقط. من أعجبه هذا أكثر؟ من أعجبه تسلسل الاستدعاءات أكثر؟ آه، مثير للاهتمام. حسنًا. هل كان تسلسل الاستدعاءات أسهل فهمًا فقط لأن الأسماء كانت أقل؟ حسنًا، رائع. أنا سعيدة لأنني عرضته أولًا إذًا. لكن هل هناك أي أسئلة؟ نعم.

أحد الحضور (AUDIENCE):

هل هناك سبب معين لفعل ذلك بهذه الطريقة مقارنة بتسلسل الاستدعاءات؟

آنا بيل (ANA BELL):

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

ربما كان ذلك مربكًا. لماذا نتكلف فعل ذلك؟ لأن كل ما فعلناه في ذلك المثال بالتحديد هو ضرب اثنين — أو أظن مضاعفة عدد. كان يمكننا بسهولة كتابة ذلك الكود لمضاعفة عدد دون إرجاع دالة فعلًا. بدا ذلك مبالغًا فيه جدًا بالنسبة لما كان الكود يحاول فعله.

كان يريكم ما تستطيعون فعله بمثال سهل، ولن تكتبوا بالتأكيد أبدًا دوال تُرجع دوال أخرى من أجل أمثلة بهذه البساطة. لكنه في الحقيقة أسلوب للحالات التي تكون فيها لديكم أجزاء أكبر من الكود تريدون كتابتها، لأنكم إذا كنتم تحاولون — إذا كنتم تكتبون جزءًا أكبر من الكود، مشروعًا برمجيًا ما، وكانت كل دالة قد تريدون استخدامها معرّفة نوعًا ما في المستوى الأعلى في البرنامج الرئيسي، سيصبح الأمر فوضويًا جدًا.

هناك حالات تريد فيها أن تكون بعض الدوال مرئية أو متاحة لدوال أخرى فقط. ولذلك لا تعرّف تلك الدوال إلا داخل نطاق دوال أخرى. هذا أمر. والأمر الآخر أن استخدام أسلوب تسلسل الاستدعاءات هذا يسمح لك ببعض التحكم في تدفق التحكم (Flow of Control) في برنامج. تستطيعون تخيّل المثال هنا، حيث تنشئون أساسًا هذا — لديكم هذا السطر هنا. وفي مرحلة ما تُرجعون g.

ولا تريدون إجراء المضاعفة فورًا. لذلك لا تريدون تنفيذ val تساوي doubler فورًا. تستطيعون تخيّل وجود مجموعة أخرى من أسطر الكود هنا تفعل أشياء أخرى قبل إجراء المضاعفة فعليًا. في تلك الحالة، في هذا البرنامج الأكبر والأكثر تعقيدًا، تقاطعون أساسًا تدفق التحكم هنا. لا تُجرون المضاعفة فورًا، لكنكم حصلتم على هذه الدالة. ثم تستطيعون ربما فعل أشياء أخرى بتلك الدالة قبل إجراء المضاعفة أخيرًا.

صفحة المصدر 13

في تلك الحالة تستطيعون أساسًا تنفيذ بعض الكود جزئيًا، وإجراء بعض العمليات الأخرى، ثم إنهاء التنفيذ في النهاية بعدما تُجرون هذه العمليات. مرة أخرى، لا يبدو ذلك منطقيًا كثيرًا لهذا المثال، لكن في جزء أكبر من الكود تكون فكرة الدوال التي تُرجع دوال مجرد أداة أخرى لتحقيق أفكار التفكيك (Decomposition) والتجريد (Abstraction)، التي تقودكم إلى كتابة كود أكثر تنظيمًا، وأكثر متانة، وأسهل قراءة، وهكذا وهكذا. لذلك ليس عليكم فعل ذلك، لكن عليكم فهم معنى أن تُرجع دالةٌ دالةً أخرى.

أي أسئلة أخرى؟ حسنًا. سننفّذ الآن الجزء الأخير من محاضرة اليوم، أفكار الاختبار وتنقيح الأخطاء. هذه المحاضرة عادة جافة إلى حد كبير، لذلك سأحاول جعلها أكثر متعة، بقدر استطاعتي. السبب في تقديم هذه المحاضرة الآن هو أنني آمل أنه في هذه المرحلة من المقرر أُتيحت لكم فرصة تجربة بعض استراتيجيات الاختبار والتنقيح بأنفسكم، بنوع من المحاولة والخطأ في الاختبارات ومجموعات المسائل.

أُتيحت لكم فرصة استخدام Python Tutor ربما، وفرصة استخدام تعليمات print، وأشياء متنوعة كهذه، ومعرفة ما الذي يعمل أفضل لكم وما الذي لا يعمل إطلاقًا، وأشياء من هذا النوع. ربما تأذيتم قليلًا من بعض هذه الاستراتيجيات. لكنني آمل أنكم بعدما تأذيتم من بعض الأشياء التي جربتموها، والتي نجحت والتي لم تنجح، ستقدّرون هذه المحاضرة أكثر قليلًا مما لو عرضتها لكم في اليوم الأول أو الثاني أو شيء كهذا، لأنها تحتوي على الكثير من البديهيات، لكن هناك أيضًا قدرًا قليلًا من الاستراتيجية الفعلية في هذه المجموعة من الشرائح بالتحديد.

تجربتكم البرمجية حتى الآن — أعرف أن هذه تجربتي بالتأكيد — هي أنني آمل عندما أشغّل الكود أن يعمل فورًا بشكل مثالي. لكن بدلًا من ذلك، ما يحدث لي في النهاية هو أنني أشغّل الكود فيتعطل فورًا. تظهر لدي الأخطاء الحمراء على الجانب، وأرتبك قليلًا.

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

يساعد أيضًا تقسيم البرامج إلى وحدات (Modularizing). إذا رأيت كتلة من الكود تنسخها وتلصقها في كل مكان، سترغب في إخراجها إلى دالة صغيرة تستدعيها في مواضع متعددة. أفكار كهذه تستخدم نوعًا ما آلية البرمجة الدفاعية (Defensive Programming)، وتسمح لك بأداء الاختبار والتحقق بسهولة كبيرة عندما يحين وقتهما، ثم ربما تنقيح الأخطاء عندما يحين وقته.

ستُقسم المحاضرة إلى جزأين. أولًا سنتحدث عن الاختبار والتحقق (Validation)، وبعض استراتيجيات الاختبار اللطيفة. ثم سنتحدث عن بعض استراتيجيات تنقيح الأخطاء أيضًا. جزء الاختبار والتحقق هو حيث تتوصل إلى مجموعة من حالات الاختبار المدخلة (Input Test Cases) والمخرجات المتوقعة (Expected Outputs). وكل ما تفعله هو تشغيل الاختبار، وتشغيل الكود للتأكد من أن المخرج المتوقع يطابق المخرج الذي تحصل عليه فعلًا من تشغيل الكود.

جزء التنقيح هو حيث لا يطابق أحد اختباراتك المخرج المتوقع. لا يطابق أحد المخرجات التي تحصل عليها المخرج المتوقع. وعند تلك النقطة عليك معرفة سبب عدم عمل الكود، بالطبع. لذلك قبل أن تختبر الكود أصلًا، كما ذكرت من قبل، عليك تجهيز نفسك للاختبار والتنقيح.

صفحة المصدر 14

لتسهيل هذا الجزء، من المهم كتابة التوثيق (Documentation) جيدًا جدًا. عندما تكتب دالتك الخاصة، لا الدوال التي أعطيناكم إياها، وثّقها في سلسلة التوثيق (Docstring). ما المدخلات التي تتوقعها؟ ما الذي ينبغي أن تفعله الدالة؟ ما الذي ينبغي أن تُرجعه؟ أشياء كهذه.

إذا كتبت الكود بطريقة غريبة نوعًا ما، أو استخدمت جزءًا من Stack Overflow أو شيء كهذا، فوثّقه لتتأكد من أنك إذا نظرت إليه بعد أسبوع ستظل تتذكر ما فعله ذلك الجزء من الكود. أشياء بسيطة جدًا جدًا كهذه يمكن أن تُحدث فرقًا كبيرًا جدًا عندما يحين وقت الاختبار والتنقيح.

تقسيم الكود، بالطبع، إلى أجزاء أصغر مهم جدًا؛ لأنك إذا كنت تنسخ قطعة الكود نفسها وتلصقها مرارًا، فقد تتذكر إجراء تغيير في موضع واحد، وتنسى إجراء التغييرات نفسها في كل هذه المواضع المختلفة. وسيكون ذلك محبطًا جدًا عندما يحين وقت تشغيل الكود وتنقيحه فعليًا.

بمجرد أن يكون لديك كود مكتوب تبدأ عملية الاختبار. تزيل جميع الأخطاء. الأخطاء الدلالية الساكنة (Static Semantic Errors) وأخطاء الصياغة (Syntax Errors) سهلة الإزالة جدًا. تخبرك Python فورًا: خطأ فهرسة في هذا السطر، أو خطأ صياغة في هذا السطر. هذه سهلة الفهم جدًا.

باستخدام ورقة وقلم، أو بالكتابة في ملف Python، تتوصل إلى مجموعة من حالات الاختبار. ولكل واحدة من تلك الحالات، بالطريقة التي نكتب بها حالات الاختبار في اختباراتكم المصغّرة، تقول ما الذي تتوقع أن يكون عليه المخرج. عندما تشغّله فعليًا، لا تحتاج إلى تذكر ما ينبغي أن يكون هذا المخرج. يكون مكتوبًا فقط في مكان ما على الورق أو على الشاشة.

عندما تنشئ مجموعة من حالات الاختبار، تستطيع إنشاء بعض فئات الاختبارات المختلفة. نأمل أن نكون قد قسّمنا برامجنا إلى وحدات، مما يعني أننا ننشئ دوال. أبسط فئات الاختبارات تسمى اختبارات الوحدة (Unit Tests)، وهذه الاختبارات تختبر أساسًا دالة بمدخلات مختلفة. ما ستفعله هو التوصل إلى مجموعة من حالات الاختبار المختلفة لدالة معينة وتشغيل هذه الحالات على الدالة.

إذا عملت جميعها فممتاز. لكن إن لم تعمل، أو إذا وجدت عيبًا وأنت تكتب حالات الاختبار في الكود، فسترغب في إجراء اختبار عدم التراجع (Regression Testing). ويعني اختبار عدم التراجع أنك عندما تجد عيبًا تضيف حالة اختبار جديدة له، أو عندما تصلح عيبًا تشغّل الكود — تشغّل الكود نفسه بجميع حالات الاختبار السابقة لتتأكد من أن العيب الذي أصلحته لم يُدخل أخطاء في حالة اختبار سابقة.

هناك إذًا مجموعة من التكرارات لاختبار الوحدة واختبار عدم التراجع لاختبار كل هذه الوحدات المختلفة، جميع الدوال في برنامجك. وفي مرحلة ما تكون مستعدًا لإجراء اختبار التكامل (Integration Testing). في اختبار التكامل لديك كل هذه الوحدات، مثلما فعلتم في Hangman. لديكم كل هذه الدوال الصغيرة التي تفعل أشياء منفردة.

تجمعها كلها في برنامج أكبر. في Hangman كانت حلقة while كبيرة، تتحقق فيها من كل هذه الأشياء المختلفة التي قد يُدخلها المستخدم، ثم تستدعي الدوال المختلفة التي كتبتها. وعندما تجد أخطاء في التكامل، بعدما كتبت كودًا جمع كل هذه الأجزاء المختلفة معًا، قد تضطر إلى العودة وإجراء مزيد من اختبارات الوحدة لبعض الدوال التي كتبتها.

صفحة المصدر 15

حسنًا. أجريت اختبار الوحدة واختبار عدم التراجع واختبار التكامل. فما بعض الأساليب الفعلية للاختبار؟ كيف تنشئ حالات الاختبار هذه فعليًا لتشغيل الكود؟ أظن أن الطريقة الأكثر طبيعية لكتابة حالة اختبار هي مجرد الحدس بشأن المسألة. بناءً على سلسلة التوثيق، ما بعض الحدود الطبيعية، وبعض القيم الطبيعية لـx وy التي تختبر بها هذا الكود؟

أخبروني أنتم. ما بعض القيم التي نستطيع اختبار هذا الكود بها؟ فكّروا في حدود السؤال.

أحد الحضور (AUDIENCE):

هذا يعني أن الفرق بين x وy هو [غير مسموع].

آنا بيل (ANA BELL):

نعم. 3 و4 جيدان. إذًا كون x أصغر من y حالة جيدة. والعكس. 4 و3 حالة أخرى تكون فيها y أصغر من x. نستطيع اختبار كونهما متساويين. ماذا عن 0 و0؟ ماذا عن 1,000 و1,000؟ نستطيع إذًا اختبار الحالات القصوى، واختبار الأكبر من والأصغر من، واختبار التساوي، وأشياء كهذه.

الدوال الرياضية يسهل نوعًا ما تطبيق هذه الفكرة عليها، لأن لها حدودًا طبيعية. لكن كثيرًا ما توجد دوال لا تملك هذه الحدود الطبيعية، وعندئذ قد نكون مضطرين إلى إجراء اختبار عشوائي (Random Testing). وفي الاختبار العشوائي، بالطبع، كلما كانت لديك حالات اختبار أكثر كان احتمال العثور على عيب أكبر.

لكن هناك تقنيات فعلية للتوصل إلى حالات اختبار. الأولى تسمى اختبار الصندوق الأسود (Black Box Testing). والثانية تسمى اختبار الصندوق الزجاجي (Glass Box Testing). في اختبار الصندوق الأسود ستعامل كود الدالة بوصفه صندوقًا أسود. لذلك لا ننظر أصلًا إلى ما يفعله الكود. وكل ما ننظر إليه لإرشاد كتابة حالات اختبارنا هو المواصفات، سلسلة التوثيق.

نأمل إذًا أن يكون الشخص الذي كتب هذه الدالة قد كتب سلسلة توثيق جميلة جدًا، لأن هذا ما سنستخدمه لكتابة حالات اختبارنا. والطريقة التي سنكتب بها حالة اختبار لدالة الجذر التربيعي هذه هي أن نقول: ما قيمة x وepsilon وفقًا لهذه القيود هنا؟ بالطبع لن نختبر الكود بقيم لا تطابق تلك القيود، لأن الشخص الذي كتب هذه الدالة لا يضمن عملها مع تلك القيم الغريبة.

الأمر الجيد في اختبار الصندوق الأسود هو أننا، إذا كنا نحن من يختبر هذه الدالة، نستخدم المواصفات فقط لكتابة حالات الاختبار. مثلًا، إذا نفّذ هذا الشخص الجذر التربيعي باستخدام طريقة التقريب، فلا يهمني ذلك. ستعمل حالات اختباري إذا غيّر هذا الشخص تنفيذه إلى طريقة التنصيف. وستظل مجموعة حالات اختباري تعمل حتى لو غيّر الشخص الذي كتب هذه الدالة الصندوق الأسود، أي التنفيذ (Implementation). إذًا اختبار الصندوق الأسود جميل جدًا من هذا المنظور.

لهذه الدالة بالتحديد، ها هي مجموعة من حالات الاختبار التي سأشغّلها بها. بالطبع كون x هي 0، ومربعًا كاملًا أصغر من 1، حالات لطيفة للاختبار، وقيمًا غير نسبية (Irrational)، ثم مجموعة من الحالات القصوى، من الجيد اختبارها أيضًا. وكذلك epsilon. لدينا بعض القيم المعقولة لـepsilon، ثم بعض الحالات القصوى. ويمكننا حتى المزج والمطابقة. يمكن أن يكون لدينا 0 وقيم epsilon قصوى، ومربعات كاملة وقيم epsilon قصوى، وأشياء كهذه. هناك إذًا حالات اختبار أكثر بكثير من هذه، لكن هذه بداية جيدة جدًا.

صفحة المصدر 16

في اختبار الصندوق الزجاجي سنستخدم الكود نفسه لإرشاد حالات الاختبار التي نكتبها. فإذا كتبنا شيئًا، مجموعة اختبارات (Test Suite) كاملة المسارات (Path Complete)، فهذا يعني أننا سنصل إلى كل مسار داخل البرنامج. وهذا يعني أنه علينا النظر إلى الكود لإرشاد حالات الاختبار التي نكتبها؛ أي علينا كتابة حالة اختبار لوصول الكود إلى جزء if من فرع، وكتابة حالة اختبار لوصول الكود إلى جزء else من فرع، أو جزء elif من الفرع.

إذا كانت لدينا حلقة for، نحتاج إلى كتابة حالة اختبار لا يمرّ فيها الكود عبر الحلقة إطلاقًا، أو يمرّ مرة واحدة، أو يمرّ مرات كثيرة عبر الحلقة. والأمر نفسه مع حلقات while. نكتب حالة اختبار بحيث لا يمرّ الكود عبر حلقة while أصلًا، أو يطابق الشرط مرة واحدة، أو يطابق الشرط مرات كثيرة. يمكنكم إذًا تخيّل أن اختبار الصندوق الزجاجي هذا يؤدي إلى حالات اختبار أكثر بكثير، خصوصًا عندما تكون لدينا مجموعة كاملة من التركيبات المختلفة لكل هذه الشروط والحلقات والأشياء التي نريد الوصول إليها.

المشكلة في اختبار الصندوق الزجاجي وفي امتلاك مجموعة اختبارات كاملة المسارات هي أننا قد نفوّت عيبًا عن طريق الخطأ. ها هو مثال لكود غير صحيح. يجد القيمة المطلقة (Absolute Value) لـx. إذا كانت x أصغر من سالب 1، نُرجع سالب x، وإلا نُرجع x. إذًا يمكن أن تكون مجموعة اختبارات كاملة المسارات هي اختبار 2 وسالب 2.

تقودنا 2 عبر else. فنُرجع 2. وتقودنا سالب 2 عبر if. فنُرجع 2. ربما نقول إن هذا الكود يعمل، لكنه لا يعمل. نستطيع بالفعل معرفة أن سالب 1 تُرجع بصورة غير صحيحة بوصفها سالب 1. لذلك، بالإضافة إلى اختبار جميع المسارات عبر الكود، سنريد أيضًا النظر إلى الحالة الحدّية (Boundary Condition)، خصوصًا للشروط، عندما نُجري اختبار الصندوق الزجاجي.

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

لا توجد وصفة، كما كانت هناك في اختبار الصندوق الزجاجي واختبار الصندوق الأسود لكتابة حالات الاختبار. لا توجد وصفة مشابهة لتنقيح برنامج. هناك حاجة إلى الكثير من الخبرة، وإلى مرات كثيرة رأيت فيها عيبًا يظهر، حتى تعرف ما المشكلة المحتملة. لذلك فإن الكثير من الخبرة في كتابة الكود مفيد جدًا في عملية التنقيح.

توجد أدوات تساعدك في عملية التنقيح، لكن ليست هناك أدوات كثيرة تقوم بالتنقيح فعليًا. عليك نوعًا ما أن تفعله بنفسك. هناك أدوات مدمجة في Anaconda. ليست جيدة جدًا. استخدمتها. وPython Tutor، بالطبع، أداة جيدة جدًا، خصوصًا للبرامج الصغيرة، لأنك تستطيع المرور خطوة بخطوة ورؤية قيم كل متغير أثناء تشغيل الكود. لذلك أحبها كثيرًا.

تعليمات print جيدة جدًا أيضًا، لكن عليك معرفة أين تضعها. وعليك استخدامها بفاعلية. لذلك، من هذا المنظور، إذا لم تكن معتادًا كثيرًا على تعليمات print، فقد تكون Python Tutor أنسب للتنقيح. لكن مهما كان الأمر فمن المهم أن تكون منهجيًا. لا تبدأ فقط بتغيير متغيرات عشوائية أو شروط عشوائية ثم تشغيل الكود عبر أداة الاختبار مرة أخرى. لن يعمل ذلك جيدًا جدًا لنا.

عندما نرى رسائل أخطاء في عملية التنقيح، تكون هذه سهلة الفهم جدًا. IndexError. آه، تبًا. عليّ التحقق من فهارسي. ربما تجاوزت الحدود. إذا رأيت IndexError فعليك على الأرجح طباعة المتغير الذي تفهرس داخله أو الذي تستخدمه في الفهرسة.

صفحة المصدر 17

أخطاء TypeError. يا إلهي، انظروا. أحوّل قائمة إلى عدد صحيح. ماذا سيفعل ذلك؟ لا شيء. سيعطينا خطأ. أو هنا أقسم سلسلة نصية على عدد صحيح. مرة أخرى، شيء بسيط جدًا يمكن إصلاحه. وأخطاء NameError بالطبع. لدي هنا متغير لم أهيئه أبدًا. وأخطاء SyntaxError تعني أساسًا أشياء مثل أن المسافة البادئة غير صحيحة، أو أن قوسًا مفقود، أو شيء كهذا.

الأخطاء المنطقية (Logic Errors) أصعب بكثير. في هذه لا تستطيع فقط النظر إلى السطر والقول: هنا المشكلة. تحدث هذه عندما لا يطابق مخرجك المخرج المتوقع. وهنا يكون إشراك جزء آخر من دماغك مهمًا جدًا.

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

شرح الكود لشيء آخر، لشخص آخر، أمر جميل جدًا أيضًا. هذه أنا أشرح لابني الكود لشيء سنفعله بعد بضع دقائق. عمره الآن سبع سنوات، ويستخدم Scratch. وهذا رائع إلى حد كبير. كان يساعدني في التنقيح، والآن أساعده في التنقيح. نعم.

أو يمكنك شرح الكود لشيء جامد، مثل بطة مطاطية. والآن، بعدما قلت ذلك، جئتم في يوم جيد، لأنكم جميعًا ستتمكنون من الحصول على بطتكم المطاطية الخاصة. أنواع مختلفة. خذوا بعد المحاضرة البطة التي توافق شخصيتكم.

لدي بطات Minecraft، وبطات زرافة، وبطات أميرة، وبطات شرطة، وبطات فيل. أي بطات تريدون، تعالوا وخذوا واحدة. استخدموها لاختباراتكم. استخدموها لمجموعات المسائل. أي شيء تريدون استخدامها له، افعلوه. حسنًا. آمل أن تكون مفيدة. استخدموها جيدًا.

حسنًا. لكننا لم ننتهِ بعد تمامًا. حسنًا. سأعطيكم بعض نصائح التنقيح. أعرف أنني قلت إنها عملية إبداعية، وقلت إنه من الصعب جدًا التوصل إلى وصفة لأداء التنقيح الفعلي. لكن ربما توجد طريقة جميلة جدًا لذلك.

الفكرة وراء التنقيح هي أساسًا استخدام المنهج العلمي (Scientific Method). كما قلت، لا تغيّر الأشياء عشوائيًا متوقعًا أن ينجح الأمر. ما تريد فعله هو النظر إلى مجموعة من حالات الاختبار التي فشلت. قد يكون بينها جميعًا شيء مشترك. وقد يقودك ذلك إلى جزء صغير من الكود في برنامجك، هو الذي ينبغي أن تركز على تغييره. تريد إذًا النظر إلى البيانات، وصياغة فرضية (Hypothesis)، ومحاولة معرفة ما إذا كانت حالة اختبار أخرى تفشل أيضًا في ذلك بالتحديد.

وأنت تنفّذ طريقة التنقيح، إن لم تكن لديك أي فكرة أصلًا عن موضع البداية، فحاول وضع تعليمات print في مواضع معقولة في الكود. عندما تدخل الدوال أول مرة، وعندما تدخل حلقة أول مرة، اكتب جميع قيم متغير الحلقة وجميع المتغيرات التي تنشئها داخل الحلقة أو تعدّلها فيها، وأشياء من هذا النوع. وإذا فشل كل شيء آخر، فإن استخدام طريقة التنصيف طريقة جميلة جدًا لمحاولة حل المشكلة.

طريقة التنصيف في التنقيح تقول أساسًا: ضع تعليمة print في منتصف الكود تقريبًا. إذا بدا كل شيء صحيحًا لجميع المتغيرات عند تلك النقطة، تعرف أن المشكلة بعد هذا. وإذا كان شيء خاطئًا، تعرف أن المشكلة في النصف الأول من الكود.

صفحة المصدر 18

ثم ضع تعليمة print عند ربع الكود. وعند تلك النقطة انظر إلى ما إذا كانت جميع القيم تطابق ما تتوقعه. إذا طابقت فممتاز. تعرف أن المشكلة في الربع الثاني، أظن. نعم، الربع الثاني. وإن لم تطابق، فالمشكلة في الربع الأول. إذًا طريقة التنصيف طريقة جميلة لمحاولة تنقيح الكود.

ما سنفعله في الجزء الأخير من المحاضرة هو أننا سننقّح بعض الكود معًا. وهو موجود في ملف Python. وما أدرجته في ملف ZIP الخاص باليوم هو في الحقيقة لعبة Wordle كتبتها. اسمها شيء مثل 12_wordle.py أو نحو ذلك. وهي معيبة. أدخلت بعض العيوب فيها. وإذا أردتم التدريب على التنقيح، تستطيعون محاولة إصلاح لعبة Wordle لتعمل. ثم تستطيعون لعبها بأنفسكم، أو إبهار أصدقائكم وجعلهم يلعبون لعبتكم إذا أردتم فعل شيء كهذا.

قبل أن ننتهي، أريد فعلًا إجراء بعض التنقيح معكم، فقط لأريكم طريقة التنصيف للتنقيح. الكود الذي سننقّحه هو هذا هنا. وقد أدرجت بالفعل الكود المصحح خطوة بخطوة، لكننا سنتحدث عنه معًا. هذه الدالة معيبة.

إنها دالة اسمها is_pal تأخذ قائمة x. ويُفترض أن تُرجع True إذا كانت عناصر القائمة متناظرة (Palindrome)، وFalse خلاف ذلك. باستخدام المدخل abcba محوّلًا إلى قائمة — إذًا المدخل سيكون السلسلة a، والسلسلة b، والسلسلة c، والسلسلة b، والسلسلة a — هذه القائمة متناظرة لأنها هي نفسها من الأمام ومن الخلف.

إذا شغّلتها، ينبغي أن تطبع True. حسنًا. عملت حالة الاختبار تلك جيدًا. لكن الآن ماذا عن حالة الاختبار الثانية؟ مفاجأة، لن تعمل. إذا مرّرت القائمة ab، فسيكون مدخلي السلسلة a والسلسلة b. هذا ليس تناظرًا. لذلك أتوقع أن تطبع False، لكنها تطبع True.

لدي حالة اختبار لطيفة هنا أستطيع إجراء إصلاحات بها ومعرفة ما إذا كانت تُصلح فعلًا. والآن، بالطبع، abcdefghijklm، هذه أيضًا لا تعمل. هذه حالة اختبار أخرى لن تعمل. لكنني لا أريد استخدام هذه الطويلة بوصفها حالة اختباري. أريد استخدام أبسط حالة اختبار أستطيع العثور عليها ولا تعمل. لذلك تبدو ab حالة جميلة جدًا للاختبار بها.

حسنًا. الآن، أول ما نريد فعله بعدما حددنا المدخل الذي أريد الاختبار به هو وضع تعليمة print في منتصف الكود تقريبًا. نعم، لا يوجد هنا إلا نحو خمسة أسطر من الكود. لذلك ربما لا يوجد إلا موضع واحد منطقي لوضع تعليمة print. لكن سايروني هنا فقط.

يمكن وضع تعليمة print هنا مباشرة، قبل تعليمة if تمامًا. لدي سطران من الكود يفعلان شيئًا، ثم if. فلنضعها قبل if تمامًا. أنزل إلى الأسفل. الخطوة الثانية. ها أنا. وضعت تعليمة print قبل if مباشرة.

نستطيع الآن تشغيل الكود مرة أخرى. لن أشغّل الحالة التي عملت. دعوني أحاول تشغيل التي لم تعمل لمعرفة المشكلة. أشغّل هذا. تطبع تعليمة print المتغير temp، أي معكوس x، وx. ما أتوقعه — وكان ينبغي على الأرجح أن أكتب هنا ما أتوقع الحصول عليه. ما أتوقعه هو رؤية معكوس ab، أي ba، ثم x الأصلية، ab.

لكنني لا أرى ذلك. أرى ab وab. ينبغي أن تكون الأولى ba. لدي إذًا بالفعل شيء غير متوقع، ولذلك أعرف أن المشكلة ستكون في هذين السطرين الأولين من الكود، في مكان ما هنا. حسنًا.

بعد ذلك أريد معرفة أي واحد من هذين السطرين هو المشكلة. لذلك سأضع تعليمة print أخرى عند ربع الطريق عبر الكود. حسنًا. لا يوجد إلا موضع آخر واحد لوضعها. فلنضعها هنا.

صفحة المصدر 19

لدي تعليمة print أخرى قبل reverse مباشرة. ما سأتحقق منه قبل العكس هو قيمة متغيري temp ومتغيري الأصلي، وبعد العكس قيمة متغيري المعكوس والمتغير الأصلي. ما أتوقع رؤيته هنا هو أن هذين ينبغي أن يكونا متساويين: ab, ab. لكن هنا أتوقع رؤية ba, ab.

أشغّلها إذًا بهذا المثال المعيب. قبل العكس أتوقع ab وab. وأحصل عليهما بالفعل، وهذا جيد. أنا سعيدة برؤية ذلك. ثم بعد العكس، هذه مشكلتي. أتوقع أن تكون هذه معكوسة، لكنها ليست كذلك. أعرف الآن أن المشكلة هنا، temp.reverse، لأن temp وx هنا في تعليمة print هذه كانتا كما هو متوقع. فما الإصلاح الذي تظنون أنه ينبغي أن يُجرى لـreverse؟ نعم.

أحد الحضور (AUDIENCE):

علينا إضافة أقواس.

آنا بيل (ANA BELL):

نعم، بالضبط. علينا إضافة أقواس. هذه دالة. علينا استدعاؤها كدالة. لنُجرِ ذلك الإصلاح. فعلناه هنا. أضفت هنا الأقواس إلى reverse. وأشغّلها مرة أخرى.

ما أتوقعه الآن قبل العكس هو أن أرى ab, ab. ينبغي أن تبقى هذه كما هي. لا ينبغي أن تتغيّر، لأنني لم أفعل شيئًا لـtemp = x تلك. وبعد العكس أتوقع أن تكون temp هي ba وأن تكون x هي ab دون تغيير.

حسنًا. لنشغّلها. قبل العكس يبدو كل شيء سليمًا. temp وx متساويتان. بعد العكس، انظروا إلى ذلك. لدي ba بوصفها متغيري المعكوس. أنا سعيدة. لكن x عندي تغيّرت أيضًا. أنا حزينة. نعم.

أحد الحضور (AUDIENCE):

عليك إنشاء نسخة من x.

آنا بيل (ANA BELL):

بالضبط. هناك دليل. نرى دليلًا. غيّرنا temp وتغيّرت x أيضًا. لذلك، كما اقتُرح من الخلف، نحتاج إلى إنشاء نسخة من x. ما فعلناه هنا يسمى، عندما نفّذت temp = x على متغير قابل للتغيير، ماذا أنشأت؟

أحد الحضور (AUDIENCE):

اسمًا بديلًا.

آنا بيل (ANA BELL):

اسمًا بديلًا. بالضبط. لننشئ نسخة من x تلك هنا. آمل أن يصلح هذا الأمور، لأنني لم أعد أملك أسطرًا أصلحها. إذا شغّلنا هذا الكود مرة أخرى مع ab ورأينا المخرج قبل العكس، ينبغي أن تكون temp وx متساويتين. وهما كذلك. كلتاهما ab, ab. وبعد العكس ينبغي أن تكون temp المعكوسة ba، وهي كذلك. وينبغي أن تبقى x كما هي، ab. والنتيجة False، فهي ليست متناظرة.

آخر شيء عليّ فعله هو إعادة التحقق من حالة اختباري الأصلية، التي عملت فعلًا قبل أن أُجري جميع تغييراتي، لأرى ما إذا كانت ما تزال تعمل. وهي تعمل. تلك القائمة بالتحديد متناظرة. ولذلك ما تزال تُرجع True.

هذا كل شيء إذًا. لدي اثنان — أو واحد فقط، أظن، من استيعابات القوائم لتجربوا كتابته بأنفسكم. ثم، بالطبع، لعبة Wordle المعيبة لتجربوها أيضًا.