التفريغ الكامل للمحاضرة 8: الدوال بوصفها كائنات
التفريغ الكامل للمحاضرة 8: الدوال بوصفها كائنات
المصدر الأصلي: التفريغ الإنجليزي الرسمي بصيغة PDF، المرتبط بـصفحة المحاضرة وصفحة الفيديو.
النسبة والترخيص: آنا بيل (Ana Bell)، مقرر 6.100L: مقدمة في علوم الحاسوب والبرمجة باستخدام Python، خريف 2022، معهد ماساتشوستس للتكنولوجيا، MIT OpenCourseWare. هذه ترجمة عربية غير رسمية ولا تعني تأييد MIT أو اعتماده لها. المواد الأصلية المشمولة والترجمة تحت رخصة CC BY-NC-SA 4.0، مع النسبة إلى أصحابها، والاستخدام غير التجاري، ومشاركة التكييفات بالرخصة نفسها.
منهج المطابقة: تقابل عناوين صفحات المصدر فواصل الصفحات في الاستخراج الرسمي 1fkfiNALPDISPZW42aeW-nV_AJwXJ6fWN_transcript.txt، وعددها 19 صفحة غير فارغة. تُترجم الفقرات بالترتيب مع التكرار والتصحيحات الشفهية وأقوال الحضور والإشارات الصوتية، وتبقى أسماء Python ورموزها دون ترجمة. تُوسم أي إضافة بملاحظة المترجم. عنوان التسجيل في المصدر: MITOCW | 6.100L Lecture 8 Version 2.mp4.
صفحة المصدر 1
[صرير]
[حفيف]
[نقر]
آنا بيل: حسنًا يا جماعة، لنبدأ. في المحاضرة الماضية قدمنا الدوال (Functions)، ورأينا بعض الصياغة النحوية (Syntax) المتعلقة بإنشائها. لكن اهتمامنا الأساسي كان تبرير استخدام الدوال كوسيلة للبدء في كتابة شيفرة نظيفة حقًا، سهلة التنقيح (Debugging)، وسهلة القراءة مستقبلًا. اليوم سنواصل مغامرتنا الممتعة مع الدوال، وسنرى معنى التعامل معها بوصفها كائنات (Objects).
لنتذكر المثال الذي ناقشناه في المحاضرة الماضية. أنشأنا دالة is_even. صياغة إنشاء الدالة أساسًا هي أن الكلمة المفتاحية def تخبر Python أننا نعرّف دالة. نقرر الاسم الذي نعطيه لدالتنا. وتخبر الأقواس Python أننا سنسمّي هنا كل الوسائط (Arguments)، كل مدخلات الدالة. وتبدأ النقطتان الرأسيتان جسم الدالة.
الجزء الأول ليس مطلوبًا، لكن ينبغي أن يكون موجودًا دائمًا كوسيلة لتحقيق التجريد (Abstraction). يسمى سلسلة التوثيق (Docstring). هذا بالأخضر هو سلسلة التوثيق. تبدأها ثلاث علامات اقتباس وتنهيها ثلاث علامات اقتباس. وفكروا في سلسلة التوثيق، المعروفة أيضًا بالمواصفة (Specification)، كتعليق طويل جدًا. وفيها، إنها— وسلسلة التوثيق نوعًا ما— سميتها عقدًا بين من يكتب الدالة ومن يستخدمها.
في العقد يقول كاتب الدالة أساسًا: ستأخذ هذه الدالة هذه المدخلات، وأضمن أن تعمل على نحو صحيح حين تعطيني هذه المدخلات بهذه الأنواع وهذه القيود عليها، وأشياء من هذا القبيل. ثم تذكر أيضًا ما ستفعله الدالة، وتذكر أيضًا ما ستعيده.
في هذه الدالة تحديدًا، لدينا سطر واحد فقط. هذا جسم الدالة. لكن آمل أنكم رأيتم دوال أطول قليلًا أثناء تدريبات المحاضرة الماضية. وجسم الدالة نفسه— أسطر الشيفرة هي أساسًا أسطر رأيناها من قبل. لا شيء خاصًا فيها، باستثناء الأسطر التي تبدأ بـreturn. هذه الأسطر تخبر Python أساسًا أنه بمجرد الوصول إلى هذا السطر الذي فيه return.
وأثناء تنفيذ دالتي، أحتاج إلى التوقف عن تنفيذها، وأخذ القيمة المرتبطة بهذه return وتمريرها إلى من استدعاني. تعيد الدالة دائمًا شيئًا. في هذه الحالة ستعيد True أو False، قيمة منطقية (Boolean). لكن تستطيعون كتابة دوال تعيد أعدادًا صحيحة أو أعداد فاصلة عائمة أو سلاسل نصية وما شابه. في هذه الحالة هذا هو ما يُعاد.
ومن الممكن، وقد رأينا هذا بالفعل في أحد تمارين «جرّب بنفسك» أثناء كتابة شيفرتنا— أن نكتب دالة لا تعيد شيئًا صراحة بالفعل. هذه دالة is_even. والتغيير الوحيد الذي أجريته داخل الجسم هو حذف كلمة return الصغيرة. لكن العمل المنجز نفسه بخلاف ذلك. هنا أحسب فقط هل الباقي 0 أم لا. وعندما تُنفّذ الدالة يُستبدل هذا السطر من الشيفرة إما بـTrue أو بـFalse.
صفحة المصدر 2
لاحظوا أن هذه الدالة ليس فيها كلمة return. لكن كل الدوال تعيد شيئًا. أثناء تنفيذ الدالة بسبب استدعاء دالة (Function call)، إذا وصلت إلى نهاية كل هذه الأسطر المزاحة، كل ما له مسافة بادئة— إذا وصلت إلى النهاية ولم تصل إلى أي عبارة return، فإن Python تعيد None تلقائيًا. فهذا السطر بلا عبارة return.
يمكنكم التفكير في الشيفرة وكأن Python تضع خلف الكواليس هذا السطر الصغير في الأسفل الذي يقول return None. وهذا ليس شيئًا سنكتبه نحن أبدًا. تنفّذ العمليات، وربما تطبع بعض الأشياء، ثم تحذف كلمة return إذا أردت إعادة None من الدالة. وNone هي هذه NoneType، قيمة نوعها NoneType. تحدثنا عنها ربما في المحاضرة الأولى أو الثانية، ولم نستخدمها كثيرًا منذ ذلك الحين.
لكن أساسًا، فكروا فيها على أن نوعها NoneType فقط، وترتبط به قيمة واحدة هي None. وعادة نستخدم هذه القيمة لتمثيل غياب قيمة في شيفرتنا. دعوني أشغّل بعض الشيفرة أولًا لأريكم بالضبط بعض الأشياء التي قد تلاحظونها حين تكتبون شيفرة ليس فيها عبارة return.
هنا، أعدكم أن هذه آخر مرة نرى فيها is_even. لدي نسختان من دالة is_even: واحدة سميتها is_even_with_return، وأخرى اسمها is_even_without_return. تفعلان أشياء متشابهة جدًا. الاختلاف أن هذه فيها عبارة return أعيد فيها هل الباقي يساوي 0، وهذه لا تحتوي عبارة return، بل تطبع فقط هل الباقي يساوي 0.
حسنًا، لننظر إلى تشغيل is_even_with_return. وأثناء ذلك ستكون الدالة الأولى مراجعة للمحاضرة الماضية، نتتبع فيها ما يحدث حين نستدعي دالة. أزلت التعليق عن هذا السطر، والآن أشغّل السطر 13. حين ترى Python هذا الملف، ترى أساسًا تعريف هذه الدالة. وهذه ليست شيفرة تعمل بعد؛ إنها تخبر Python فقط أنني أنشأت هذه الدالة داخل الذاكرة.
حين يُشغَّل هذا السطر، عندئذ تُستدعى الدالة بالفعل وتعمل بالفعل. تُستبدل i بالمعامل 3. وعند هذه النقطة يُنفّذ جسم الدالة. أول ما نطلب من الدالة فعله طباعة السلسلة "with return". فإذا شغّلتها سترون أنها تطبع "with return".
ثم تحسب هذا المتغير remainder، الذي سيكون 1 لأن 3 % 2 يساوي 1. ثم سأعيد هل 1 تساوي 0، فيكون ذلك False. بمجرد رؤية عبارة return هذه، تخرج Python من استدعاء الدالة وتستبدل الاستدعاء كله بالقيمة المعادة. فيُستبدل هذا السطر كله بعد تنفيذ الاستدعاء بـFalse. وقد دوّنت ذلك هنا.
لا نفعل شيئًا بهذه القيمة المعادة، صحيح؟ كل ما نفعله هو استدعاء الدالة، وتبقى في السطر 13 نوعًا ما. لرؤية نتيجة الاستدعاء، رأينا في المحاضرة الماضية أننا نضعه فعلًا داخل عبارة print. واستدعاءات الدوال بهذا المعنى مجرد تعبيرات (Expressions) نوعًا ما. تنجز عملًا، وتقيّمها Python إلى قيمة ما، ثم تستبدل ذلك الاستدعاء بقيمة.
إذا وضعنا استدعاء is_even_with_return مع 3 داخل عبارة print، تفعل Python الأمر كله مجددًا. i هي 3، وتعيد False. ويصبح هذا السطر فعليًا print(False). نعرف ما يفعل ذلك: يطبع False على الشاشة فقط. وها هي. لاحظوا أننا ما زلنا نفّذنا عبارة الطباعة هذه، لأننا نطلب تنفيذها كجزء من جسم الدالة. هل الجميع بخير حتى الآن؟
صفحة المصدر 3
لنرَ الآن ما يحدث حين نشغّل دالة is_even_without_return. الأمر مشابه جدًا. أنشأت فقط معاملًا إضافيًا هنا— أو متغيرًا هنا، لأريكم أنكم تستطيعون ذلك. تُشغّل دالة is_even_without_return مع 3 في السطر 27. i هي 3. ستطبع الدالة "without return"، ثم تحسب remainder لتكون 1.
ثم ستكون has_rem هي False. فستكون قيمة المتغير has_rem هي False. وبعد ذلك، كجزء من جسم الدالة، سنطبع قيمة has_rem وهي False. سيطبع هذا السطر بالفعل لي "without return" ثم هذا الشيء، False. ثم لا توجد في الدالة عبارة return صريحة.
فكروا في الأمر وكأن Python تضيف ضمنيًا return None في نهاية استدعاء الدالة. نحن لا نضيف هذا. كتبته هناك لأريكم فقط أن Python ستضيف سطرًا كهذا حين تصل إلى نهاية الدالة، لكنكم لن تضيفوه أبدًا. فهذا يعني أن استدعاء الدالة كله يُستبدل بـNone. نعم؟
أحد الحضور: حين— ماذا يحدث حين تضعين print في التعريف مقارنة بوضع return؟
آنا بيل: ماذا يحدث حين تضع print في التعريف مقارنة بوضعها حول استدعاء الدالة؟
أحد الحضور: مقارنة بوضع return في التعريف.
آنا بيل: مقارنة بوضع return في التعريف. هذا هو السطر التالي. إذا فعلنا في التالي ما فعلناه سابقًا، أي طباعة نتيجة استدعاء الدالة، فستفعل Python كل ما فعلناه للتو. ستطبع "without return"، وتطبع False، ثم ستطبع أيضًا القيمة المعادة من الاستدعاء. إذا كانت القيمة المعادة None، يصبح هذا السطر فعليًا طباعة None.
ما نراه في النهاية، أو ما يراه المستخدم إذا شغّل هذا البرنامج فعلًا، هو "without return" ثم False ثم هذه None الزائدة في وحدة التحكم (Console). ربما سترون هذا في مجموعة المسائل الثانية، وربما تصادفون خطأ كهذا في مجموعة المسائل الثالثة. لكن لا تخافوا. كلما رأيتم None في وحدة التحكم، فذلك يعني فقط أن عليكم الانتباه إلى الدالة التي استُدعيت. ربما نسيتم إعادة شيء، وبدلًا من ذلك نطبع القيمة الصحيحة داخل الدالة فقط، لكننا لم نعدها أبدًا. هذا شيء ينبغي الحذر منه.
أحد الحضور: هل ينبغي إذن أن تستخدمي return دائمًا بدلًا من [غير مسموع]؟
آنا بيل: نعم، سؤال جيد. هل ينبغي دائمًا استخدام return؟ يعتمد الأمر على ما تريد أن تفعله الدالة. معظم الدوال مفيدة لأنها تنطلق وحدها وتؤدي مهمة وتحصل في النهاية على قيمة، وتمررها إلى من استدعاها. ويمكنك استخدام الدالة بمدخلات كثيرة مختلفة لتعطيك مخرجات كثيرة مختلفة. فعادة تريد إنشاء دوال تعيد شيئًا تستطيع فعل شيء آخر به لاحقًا في جزء آخر من البرنامج.
ينبغي عادة أن تكون print داخل الدالة ربما للتنقيح، أو ربما لبيان حالة الدالة، والجزء الذي تنفّذه أو شيء كهذا.
أحد الحضور: حسنًا. ثم حين تشغّلين الدالة تعطيك القيمة المعادة. لكن إذا شغّلتِ الطباعة في الدالة، فهل عندها تظهر None؟
صفحة المصدر 4
آنا بيل: بالضبط. إذا لم تكن الدالة تعيد شيئًا، فستطبع None. صحيح، نعم. لكن إذا كانت تعيد شيئًا، فستطبع— إذا وضعتها داخل print، فستطبع ما أُعيد. حسنًا، لنعطكم هذا للعمل عليه. في الواقع ليس هناك شيء تكتبونه، لكن فكروا فيه. لدي هنا أربعة أسطر شيفرة: add مع 1, 2، ثم print، نضع ذلك داخل عبارة الطباعة؛ mult مع 3, 4، ثم نضع ذلك داخل عبارة طباعة.
حاولوا التتبع وأخبروني بالمخرجات التي سيعطيني إياها كل استدعاء. add مع 1, 2، ماذا يحدث؟ ما مخرج هذه الدالة في رأيكم؟ ما الذي يُطبع على الشاشة؟ ما هو؟
أحد الحضور: هل هو جمع؟ [غير مسموع] 3.
آنا بيل: هل أطلب منها طباعة شيء؟ هذا هو السؤال. لا يُطبع شيء بالفعل على الشاشة. لأننا في استدعاء add، فاصلة، 2، نطابق المعاملات أساسًا واحدًا واحدًا. x هي 1، وy هي 2. كان ذلك جيدًا. نعيد 3، فيُستبدل الاستدعاء كله بـ3. لكننا لم نطلب من ذلك السطر طباعة تلك النتيجة أبدًا. فلا يُطبع شيء في هذه الحالة.
ماذا لو وضعنا هذا داخل عبارة print؟ هل يُطبع شيء في هذه الحالة؟
أحد الحضور: نعم.
آنا بيل: نعم، ما الذي يُطبع؟
أحد الحضور: 5.
آنا بيل: نعم، بالضبط. add نفسها تعطيني 5، فأنا أطلب طباعة 5. وماذا عن التالية، mult— ما هي، 3, 4. هل يُطبع شيء نتيجة تشغيل هذا السطر؟
من الحضور: لا.
نعم.
آنا بيل: سمعت بعض «نعم» وبعض «لا».
أحد الحضور: الطباعة موجودة في الدالة.
آنا بيل: نعم، الطباعة في الدالة، بالضبط. مجرد أنه استدعاء دالة لا يعني أننا لا نطبع شيئًا، صحيح؟ نحتاج إلى فحص ما تفعله الدالة بالفعل. في mult تُطابق x مع 3 وy مع 4، ويقول جسم الدالة نفسه أن نطبع النتيجة. سيطبع هذا كجزء من الجسم— يطبع 12.
هل تطبع شيئًا آخر؟ لا. وأخيرًا، ماذا إذا وضعنا عبارة طباعة حول mult مع 4, 5؟ ماذا سيطبع ذلك؟
أحد الحضور: 20 ثم None؟
آنا بيل: نعم، بالضبط، 20 ثم None. ستطبع mult نفسها كما هناك: تطبع 20. ثم يعيد الاستدعاء None. يُستبدل هذا الاستدعاء كله أساسًا بـNone، ويصبح السطر طباعة None. فسيطبع هذا None على الشاشة. هناك في الواقع أربعة— أربعة مخرجات مطبوعة من هذه الأسطر الأربعة. الأول لا يولّد شيئًا، لكن الأخير يولّد سطرين من المخرجات المطبوعة. هل من أسئلة عن هذا المثال؟ نعم؟
صفحة المصدر 5
أحد الحضور: هل يمكنك شرح لماذا يطبع None مرة أخرى؟
آنا بيل: هذا هنا؟
أحد الحضور: نعم.
آنا بيل: نعم، انظر إلى ما تفعله mult. إنها تنفّذ عبارة print، فتُطبع تلك 20 في وحدة التحكم. لكن ما القيمة المعادة من mult؟ لا توجد return، صحيح؟ إذا لم توجد return، تضيف Python الـNone. هذا شيء يُفعل ضمنيًا فقط. فالقيمة المعادة من mult، لأنها ليس فيها return صريحة فعلًا، هي None. نطلب منها طباعة القيمة المعادة، وهي None.
حسنًا، بضع كلمات عن return مقابل print. لا معنى لـreturn إلا داخل دالة. كمثال، إذا كان هذا الملف مفتوحًا ولدي فيه return 5 عشوائيًا، ليست داخل تعريف دالة، فأنا في مشكلة بالفعل. ترون علامة X الحمراء. وإذا شغّلت الشيفرة تعطيني Python خطأ صياغة (Syntax error). تنقيح هذا سهل جدًا: توجد return خارج دالة. نعم، ها هي.
لا معنى لـreturn إلا داخل دالة. تقول أساسًا إن هذه الدالة أنجزت عملًا لي، وتعيد هذه القيمة. يمكن وضع عبارات print حيث تريدون: داخل الدوال أو خارجها أو حيث تريدون، وتُنفّذ كلها. يمكن أن تكون لديك عبارات return كثيرة داخل دالة. مثلًا إذا كانت لديك دالة تعيد 0 إذا انطبق شرط، أو 1 إذا انطبق شرط آخر، فيمكن أن توجد عبارتا الإعادة هاتان.
لكن بمجرد أن تصل Python أثناء التنفيذ إلى عبارة return واحدة، تنهي الدالة فورًا، وتأخذ القيمة المعادة وتعيدها إلى من استدعاها. فلن تُشغّل أكثر من عبارة return واحدة. أما print، فتستطيعون تشغيل أي عدد من عبارات الطباعة داخل البرنامج. ويمكن الوصول إليها كلها، ويمكنها كلها توليد نوع من المخرجات في وحدة التحكم.
ترتبط بعبارة return قيمة. return 5، أو إعادة— كان لدينا الباقي يساوي 0، أو أي شيء؛ توجد القيمة المرتبطة بعبارة الإعادة. تلك القيمة هي التي تُمرر إلى من استدعى الدالة. ويمكنكم التفكير في عبارة print أيضًا كأن لها قيمة مرتبطة بها: الشيء الذي يُعرض في وحدة التحكم. لكن القيمة المرتبطة بعبارة الطباعة مجرد شيء يُخرَج إلى وحدة التحكم.
لا تُمرر في البرنامج إطلاقًا. إنها ثابتة نوعًا ما. توضع في وحدة التحكم، ثم هذا كل شيء. لا يستطيع أحد آخر حقًا استخدام تلك القيمة إلا إذا كانت متغيرًا، وعندئذ تستخدمون متغيرًا عاديًا فقط. آخر ما أريد إظهاره لكم— وهذا رائع نوعًا ما.
إذا كانت لدينا عبارة print هنا وشغّلناها، فمن الواضح أنها تطبع ذلك في وحدة التحكم. لكن ما هذه print؟ إنها دالة، صحيح؟ فيها كل العلامات المميزة للدالة: الاسم print، والأقواس موجودة، وأعطيها معاملًا واحدًا، 5. فإذا طبعت القيمة المعادة من دالة print، أي وضعت دالة print داخل دالة print أخرى، فما الذي تظنون أنه سيُخرج؟
سأشغّلها. تُخرج None. الـ5 الأولى بسبب هذا، فتظهر في وحدة التحكم. لكن print بوصفها دالة لا تعيد شيئًا بالفعل. تفعل شيئًا مفيدًا، مثل أخذ ما تريد وعرضه في وحدة التحكم، لكنها لا تعيد شيئًا إلى من استدعاها. فإذا وضعت دالة print داخل دالة print أخرى، فأنا أطبع أساسًا القيمة المعادة من دالة print، وهي None.
صفحة المصدر 6
وهذا مصدر الـNone الثانية. حسنًا، بطريقة أخرى في التفكير، يمكنك إنشاء متغير a يساوي print(5). وإذا طبعت a، فنحن نقول أساسًا إن القيمة المعادة من دالة الطباعة الأولى هي None فقط. نعم.
حسنًا، سأدعكم تعملون على هذه الشيفرة قليلًا. ليس هناك شيء تكتبونه، لكن هناك شيء تصلحونه. هذه دالة اسمها is_triangular. تأخذ معاملًا واحدًا، عددًا، عددًا صحيحًا أكبر من 0. أريد أن تعيد True إذا كان n عددًا مثلثيًا (Triangular)، وFalse خلاف ذلك. ومثلثي يعني عددًا صحيحًا يساوي 1 + 2 + 3 زائد مجموع من هذا النوع.
إذن 1 مثلثي، و3 مثلثي، و6 مثلثي، وهكذا وهكذا. انظروا إلى هذه الشيفرة؛ هي قرب السطر 49 تقريبًا. ابدأوا بتشغيلها ورؤية ما تحصلون عليه، وسأعطيكم نحو دقيقة لمحاولة إصلاحها. تأكدوا من عملها مع كل حالات الاختبار هذه.
حسنًا، ما أول ما ينبغي أن تفعلوه حين يُطلب منكم إصلاح شيفرة فيها أخطاء؟ نعم.
أحد الحضور: هل يمكنك [غير مسموع]؟
آنا بيل: نستطيع ذلك، لكن لنشغّلها أولًا مع شيء. لنشغّلها مع الأولى: طباعة is_triangular مع 4. نعرف أن الإجابة ينبغي أن تكون False. أعني أنني أخبرتكم، وهذا جيد. نعم، تعطيني False بالفعل، وهذا جيد. لكنها تطبع أيضًا None. ماذا يعني هذا لنا؟ نعم؟
أحد الحضور: لا توجد عبارة return فعلية [غير مسموع].
آنا بيل: نعم، بالضبط، ممتاز. لا توجد عبارة return فعلية. كما ذكرت في مثال is_even، إذا رأيتم قيم None تظهر في مواضع ما، فتحققوا من الإعادات. هل تعيد هذه الدالة شيئًا فعلًا؟ لا، تطبع النتيجة فقط. تطبع الشيء الصحيح في هذه الحالة. لنبدأ بتغيير عبارات الطباعة إلى عبارات إعادة. نعم.
أحد الحضور: حين أشغّلها تقول True ثم False؟
آنا بيل: لهذه؟
أحد الحضور: آه، لا عليك. أظن أنني شغّلت [غير مسموع] فقط.
آنا بيل: حسنًا. لنشغّلها. ممتاز. نعم، يبدو أن ذلك أصلحها. ماذا ينبغي أن نفعل بعد ذلك؟ نعم؟
أحد الحضور: تتحققين من بقية عبارات الطباعة إذا لم تعمل لواحدة.
آنا بيل: نعم، بالضبط. لنتحقق من بقية عبارات الطباعة. الثانية، 6، مثلثية، فتطبع True. والأخيرة، كما ذكرت، ستفشل معنا. تطبع False، لكن 1 مثلثي، صحيح؟ لأن 1 مجموع 1 فقط. هل تعرفون ما قد يكون الإصلاح؟
أحد الحضور: تغيير المجال إلى n + 1؟
آنا بيل: نعم، بالضبط. اكتشفت الأمر. ينبغي أن يكون المجال n + 1. إن لم تكتشفوا ذلك فورًا، كما أظن أن شخصًا ذكر هناك، أول ما ينبغي فعله هو البدء بوضع بعض عبارات الطباعة. وداخل الحلقة موضع رائع لوضع عبارة طباعة. نستطيع رؤية الشيء الذي نكرر عليه. إذا بقي هذا n ولم ننجح في إصلاحه، وشغّلناه، نرى أننا كررنا حين كانت i هي 0 هنا، ولم نصل إلى 1 بالفعل أبدًا.
صفحة المصدر 7
فإصلاح ذلك هو التأكد من أننا نمضي حتى n مع تضمينها. والآن إذا شغّلناها وأزلنا عبارة الطباعة هذه لأنها قد تكون مربكة قليلًا، تعطيني الإجابة الصحيحة. ينبغي أن تكون الخطوة الأخيرة غالبًا تشغيل الحالتين الأخريين مجددًا، تحسبًا لأن إصلاحي أفسد شيئًا آخر. ولم يفسد؛ الحالتان الأخريان كما هما. هل من أسئلة عن هذه الشيفرة؟ هل هي مفهومة؟
حسنًا، ذكرت في المحاضرة الماضية أنه بمجرد كتابة الدوال، يسهل جدًا تضمينها في قطع شيفرة أكبر، وتجعل تلك القطع سهلة القراءة على نحو جميل جدًا. لنحاول فعل الشيء نفسه بمثال أكثر تعقيدًا قليلًا. لنحاول إنشاء— أخذ شيفرة bisection_root وتحويلها إلى دالة. ثم سيكون هناك تمرين بعد بضع شرائح تستخدمون فيه هذه الدالة.
داخل هذه الدالة أساسًا ما كان لدينا قبل ثلاث محاضرات تقريبًا: شيفرة الجذر التربيعي بالتنصيف (Bisection). كل ما فعلته تغليفها بتعريف دالة. def، أعطيتها اسمًا— bisection_root اسم جميل جدًا— وحددت المدخل الذي ينبغي أن تأخذه. ينبغي أن يكون المدخل x التي أريد تقريب جذرها التربيعي.
شيء لم أفعله هو وضع سلسلة توثيق لهذه، وهذا خطئي. لكن سلسلة التوثيق ستقول إن x عدد صحيح موجب أكبر من 1، وتعيد تقريبًا للجذر التربيعي لـx، أو نحو ذلك. حسنًا، هنا نثبّت epsilon في الشيفرة على 0.01. لدينا طرفا المجال low وhigh، كتذكير بما تفعله bisection_root. ونبدأ بتخمين في المنتصف تمامًا بين low وhigh.
ستنجز حلقة while العمل لنا. شرطها: ما دامت قيمة الفرق المطلقة بين مربع تخميننا وx الفعلية التي نحاول إيجاد جذرها التربيعي أكبر من epsilon. ما دمنا أبعد من epsilon، فلدينا مزيد من التخمينات. نُجري التخمينات بتحديث الطرف الأدنى أو الأعلى بحسب هل كان تخميننا منخفضًا جدًا أم مرتفعًا جدًا. آمل أن يكون هذا مراجعة.
بعد تحديد الطرف الذي نحدّثه، نحدّث التخمين الجديد ليكون high + low مقسومًا على 2 مرة أخرى، أي منتصفهما، حيث تكون high أو low قد تغيرت للتو بسبب if else. وتواصل هذه الحلقة العمل مرارًا ومرارًا ومرارًا، وتُنتج تقريبات أفضل فأفضل حتى نصبح ضمن زائد أو ناقص epsilon من الجذر التربيعي لـx.
الاختلاف بين هذه الشيفرة وما كتبناه قبل بضع محاضرات هو هذا الجزء في الأسفل. حينها لم نستطع حقًا إلا كتابة عبارة طباعة نأخذ فيها التخمين الذي انتهينا إليه ونطبعه، مع أن ذلك التخمين قريب من جذر x الأصلية.
لكن بدلًا من ذلك، بما أننا نكتب دالة، أريد أخذ النتيجة، تقريبي لـx، وإعادتها. فيستطيع شخص استدعاء الدالة مرات كثيرة جدًا بقيم مختلفة لـx، وإيجاد مجموعة تقريبات مختلفة لكل قيم x المختلفة. هنا لدي الاستدعاءات: bisection_root مع 4، ومع 123. ثم يمكنني طباعة نتائجهما فقط.
هذه دالة bisection_root. حوّلت الطباعة إلى تعليق لأنني لا أحتاج إليها بالفعل. بقية الشيفرة ستفعل شيئًا مفيدًا بالتقريبات. في هذه الحالة أعطتني bisection_root لـ4 القيمة 2.0، وهذا هو التقريب. وقُرّب جذر 123 بالتنصيف إلى 11.09.
ما أريد منكم فعله، وستكون هذه شيفرة متشعبة قليلًا وتحتاج إلى بعض التفكير، هو كتابة دالة تسمى «عدّ الأعداد ذات الجذر التربيعي القريب من» n زائد أو ناقص epsilon. وسأساعدكم برسم شيء على السبورة، لكن أريد منكم كتابة شيفرتها.
صفحة المصدر 8
الفكرة هنا أن لديكم n ما معطاة كمدخل، ولديكم epsilon معطاة أيضًا كمدخل. ما تريدون معرفته هو عدد الأعداد الصحيحة التي يقع جذرها التربيعي ضمن زائد أو ناقص epsilon من n. يصعب قليلًا استيعاب هذا دون رسم صورة فعلًا. فهذا أيضًا شيء ينبغي أن تجربوه في الاختبارات القصيرة ومجموعات المسائل وما شابه.
لا تكتبوا الشيفرة فورًا. حاولوا رسم صورة تمثل ما نطلبه هنا. سنبدأ بخط. هذا خط أعدادنا لأننا نحسب الجذر التربيعي. نريد معرفة عدد الأعداد الصحيحة التي لها جذر تربيعي ضمن epsilon من n. لنبدأ بـn، ولدينا شيء زائد أو ناقص epsilon. هذه epsilon وهذه أيضًا epsilon.
في النهاية نريد معرفة عدد الأعداد الصحيحة التي لها جذر تربيعي لـi— سأفعلها هكذا في الواقع. الجذر التربيعي لـi يساوي شيئًا في هذا المجال. هل هذا مفهوم حتى الآن؟ هذا ما نحاول تحديده: الجذر التربيعي لـi في مكان ما في المجال. وهذا يعني أن i ستكون عددًا ضخمًا هنا بعيدًا. فيمكن لهذا الخط الامتداد أكثر.
في المثال هنا لدي n = 10. أعرف يقينًا، نحن كبشر، أن i تساوي 100 ستعمل، لأن الجذر التربيعي لـ100 سيُقرّب غالبًا إلى قيمة قريبة جدًا جدًا من 10. فأعرف أن تلك القيمة ستكون ضمن زائد أو ناقص epsilon من 10. لكن ربما يوجد عددان حول 100 يستوفيان هذا المعيار أيضًا.
إذا أخذت الجذر التربيعي لـ99، فوفقًا لهذا المثال يضعني ذلك التقريب ضمن زائد أو ناقص epsilon من 10. سيكون— جذر 99 سيكون مثل 9 فاصلة، أيًا كان هنا، .95. وهذا ضمن زائد أو ناقص epsilon.
وبالمثل، يعمل جذرا 101 و102 أيضًا، لأن أخذ جذريهما سيضعني أيضًا ضمن زائد أو ناقص epsilon من 10. الهدف هنا أساسًا تحديد هذه الأعداد: 99 و100 و101 و102.
ينبغي أن تستخدموا قوة الحوسبة وقدرة الحواسيب على أداء المهمة بسرعة شديدة جدًا، لتقولوا أساسًا: سأستخدم القوة الغاشمة (Brute force) في هذه المسألة، وأختبر كل عدد واحدًا واحدًا حتى عدد كبير إلى حد ما. تريدون التأكد من الوصول إلى 99 و100 و101 و102، ربما بالصعود حتى مكعب n.
إذا أخذتم الجذر التربيعي لبعض i مكعبة، فأنتم تعرفون أنكم ستصلون إلى كل هذه القيم ضمن زائد أو ناقص epsilon. ستستخدمون القوة الغاشمة فقط، وتنظرون إلى كل الأعداد الصحيحة بين 0 ومكعب n، وتحددون هل هذا الجذر التربيعي— التقريب لهذا الجذر— ضمن زائد أو ناقص epsilon من n. إن كان كذلك، تحتفظون بعداد وتزيدونه، وإن لم يكن فتتجاهلونه. وهذه فكرة السؤال.
حلقة وفحص، هذا كل شيء. ويمكنكم بالتأكيد استخدام دالة bisection_root التي كتبناها في شيفرتنا. ينبغي بالتأكيد استخدامها لأنها مفيدة جدًا. قرب السطر 96 يمكنكم كتابة الشيفرة. حسنًا، هل لدى أحد بداية لكتابتها؟ أو كيف ستفكرون فيها؟ نعم.
أحد الحضور: for i in [غير مسموع] مكعب n؟
صفحة المصدر 9
آنا بيل: for i in range مكعب n، نعم، نستطيع ذلك. سيعطيني هذا الأعداد من 0 إلى مكعب n. ممتاز. ولّدت أساسًا هذا التتابع الآن. ماذا أريد أن أفعل بعد حصولي على i؟ يمكنكم دائمًا كتابة تعليق صغير لأنفسكم عما تريدون فعله بعد الحصول على i. بالإنجليزية، ماذا تريدون أن تفعلوا حين يكون لديكم عدد كهذا؟
أخذ الجذر التربيعي، نعم. أخذ الجذر التربيعي لـi. كيف تريدون أخذ الجذر التربيعي لـi؟
أحد الحضور: [غير مسموع].
آنا بيل: نستطيع. هل نستخدم bisection_root الخاصة بنا؟ نستطيع أيضًا. نعم، نستطيع كليهما. لنستخدم الدالة التي كتبناها للتو. bisection_root لـi تعطيني الجذر التربيعي. الآن ستكون sqrt قيمة ما هنا. قد تكون 10، أو 99.5، أو 99.7. ماذا أفعل بهذا العدد الآن؟
نعم؟
أحد الحضور: هل يمكنك استخدام عبارة if [غير مسموع]؟
آنا بيل: نعم، بالضبط. لنستخدم عبارة if. إذن إذا— وهناك طرق كثيرة لاستخدامها. يمكننا أخذ القيمة المطلقة، وهو ما كنا نفعله بالفعل. إذا أخذنا n ناقص الجذر التربيعي— n ناقص هذه القيمة التي حسبناها للتو— أقل من epsilon.
هنا نعرف أن الجذر التربيعي ضمن epsilon. وماذا نريد أن نفعل حين نعرف ذلك؟ إذا لم نعرف، نستطيع النظر إلى سلسلة التوثيق. نحتاج إلى إعادة عدد الأعداد الصحيحة التي لها ذلك الجذر التربيعي ضمن epsilon من n.
أحد الحضور: [غير مسموع].
آنا بيل: نعم، بالضبط، عدّها. count += 1.
أحد الحضور: [غير مسموع].
آنا بيل: نعم، وعليّ تهيئة count. count = 0، قبل الـ— حسنًا، شيء آخر؟
أحد الحضور: دالة الإعادة؟
آنا بيل: نعم، نحتاج إلى الإعادة. في نهاية الحلقة نستطيع إعادة العداد. حسنًا، نشغّلها. من أين هذا؟ آه، من السطرين الآخرين هنا. إذن أربعة. أظن أن ذلك يعمل، لأن في المثال أربعة أعداد نجحت.
للتحقق مرة أخرى يمكننا— أو إذا حدث خطأ ولم يكن العدد الذي حصلتم عليه ما توقعتموه— مرة أخرى، عبارات الطباعة مفيدة جدًا. نستطيع طباعة قيمة i، هذا الشيء الذي نحاول إيجاد جذره، وطباعة الجذر التربيعي لتلك القيمة. وإذا أضفنا هذا فعلًا إلى الطباعة— إلى الشيفرة هنا، نرى القيم الأربع التي أخذناها: 99 و100 و101 و102.
بعد كتابة هذه الشيفرة، نستطيع إجراء تغييرات بسيطة جدًا عليها، ولدينا شيفرة مفيدة إلى حد كبير. إذا جعلنا الحد أكبر، 10 زائد أو ناقص 1، سنحصل على قيم أكثر تستوفي هذا المعيار. حصلنا بالفعل على 40 منها، من 81 حتى 120. كلها تستوفي المعيار: حين تأخذون الجذر التربيعي للقيمة، فهو زائد أو ناقص، من 9 إلى 11.
صفحة المصدر 10
هل من أسئلة عن هذا المثال؟ أعرف أنه متشعب نوعًا ما، لكن آمل أن رسم صورة بالفعل ساعد في تفسير ما كنا نحاول الوصول إليه. وعندئذ ينبغي أن يكون تحديد بنية الشيفرة نفسها سهلًا جدًا. هل من أسئلة؟ نعم؟
أحد الحضور: سؤال عن المجال. لماذا يجب أن يكون ذلك العدد الكبير؟ يمكن أن يكون أصغر.
آنا بيل: يمكن أن يكون أصغر، نعم. أعني أنه كان يمكننا استخدام n مرفوعة للقوة 4. لم نستطع فقط استخدام مربع n، لأننا قد نفوّت— حسنًا، سنفوّت بالتأكيد 101 و102 في ذلك المثال تحديدًا. وفي الواقع، إذا كانت epsilon كبيرة جدًا فقد— لست متأكدة من الرياضيات بالفعل، لكن إذا كانت كبيرة جدًا، فقد نحتاج إلى أكبر من مكعب n أيضًا. عليّ التفكير في ذلك. لكننا فقط— لا بأس. أعني أنه لا بأس بجعله كبيرًا. لا يستغرق حسابه وقتًا أطول كثيرًا، لأن تشغيل الدالة سريع جدًا بالنسبة إلى Python على أي حال. نعم، هناك سؤال.
أحد الحضور: لدي سؤال مشابه. هل يوجد سبب لاختيار مكعب n كعدد اعتباطي كبير بما يكفي؟
آنا بيل: نعم، عدد اعتباطي كبير بما يكفي. وكان يمكننا أيضًا، في السياق نفسه، فعل شيء أذكى قليلًا هنا: حين نجد عددًا يعمل بالفعل، عندما نبدأ زيادة العداد، يمكن أن يكون لدينا عَلَم (Flag) يتتبع أنه ما دمنا نزيد العداد، نواصل. لكن عند نقطة ما تعرفون أنكم ستصلون إلى عدد كبير جدًا.
عندئذ يمكنكم إنهاء الدالة مبكرًا فقط. تستطيعون الخروج من الحلقة، ولا تحتاجون إلى مواصلة البحث حتى مكعب n. كان يمكننا فعل شيء أذكى قليلًا لجعل الدالة أسرع قليلًا باستخدام الأعلام، ويمكنكم تجربته. انظروا هل تستطيعون جعل البرنامج يتوقف بمجرد الوصول إلى 103. هل تستطيعون كتابة برنامج يستخدم عَلَمًا لإطلاق هذا الحدث، ثم حين يصبح الحدث صحيحًا، تخرجون من الحلقة أو تعيدون فورًا أو نحو ذلك؟
أسئلة أخرى؟ حسنًا، لنبتعد قليلًا في النظر إلى الدوال. فعلنا هذا قليلًا في المحاضرة الماضية. هذه دالة كتبناها فعلًا في المحاضرة الماضية، مجموع الأعداد الفردية بين a وb. كانت أساسًا صندوقنا الأسود (Black box). تذكروا أننا الآن، بعدما نكتب دوال، نفصل أنفسنا نوعًا ما بوصفنا مبرمجين يكتبون دالة— تصنع أساسًا هذه القطعة الوحدوية الجميلة من الشيفرة القابلة لإعادة الاستخدام مرارًا.
نفصل هذا الجانب عن شخص يستخدم دالة. بمجرد وجود دالة مكتوبة لك مسبقًا، تستخدمها في الشيفرة فقط، كما استخدمنا bisection_root هنا. أعرف أننا كتبناها، لكنني أظن أنني كتبتها تقنيًا. هنا استخدمناها فقط نوعًا ما، واستخدمناها لكتابة هذه القطعة الأجمل والأكثر تعقيدًا. وهذا ما نفعله. ننشئ أساسًا هذا الصندوق الأسود. وبمجرد وجود مواصفته أو سلسلة توثيقه، لا تحتاجون إلى معرفة كيفية تنفيذه لكي تستخدموه.
لكن ما أردت ذكره هو شيء ذكرته في المحاضرة الماضية: تعريف الدالة ينشئ كائن دالة داخل الذاكرة فقط، واسم هذا الكائن هو اسم الدالة. إذا فكرنا في البرنامج، هناك الصندوق البرتقالي، ولدينا كائن يحدث أنه دالة، ترتبط به شيفرة، واسمه sum_odd.
وموازاة ذلك نوعًا ما هي حين ننشئ متغيرًا فقط، كما كنا نفعل حتى الآن. هنا ننشئ كائنًا 2 اسمه low. وبالطريقة نفسها، يقول ذلك الصندوق الأسود أساسًا إنني أنشئ كائن دالة ترتبط به شيفرة واسمه sum_odd. لدي في هذه الحالة sum_odd وlow وhigh كأنها ثلاثة كائنات داخل برنامجي.
صفحة المصدر 11
ولا تعمل الشيفرة المرتبطة بكائن الدالة إلا حين أجري استدعاء دالة. أثناء تعريف الدالة لا تعمل. تبقى داخل ذاكرة الحاسوب ككائن موجود فقط. وحين أجري الاستدعاء أستخدم ذلك الكائن. يأخذ الاستدعاء أساسًا متغيراتي ويطابقها مع تعريف الدالة.
تُطابق a مع low، وb مع high. وترتبط بـlow وhigh في الاستدعاء قيم فعلية، 2 و7. ثم تنطلق الدالة وتؤدي العمل. وفي النهاية ستعيد شيئًا، إما قيمة فعلية أو None. ثم تحل تلك القيمة الفعلية محل الاستدعاء كله. ففي برنامجي سيكون المتغير my_sum هنا مساويًا للقيمة المعادة. مجرد مراجعة صغيرة، لكن آمل أن يستمر هذا في تأكيد الفكرة.
سنتحدث الآن بتفصيل أكبر عما يحدث بالضبط حين نستدعي دالة. يمكنكم التفكير في البرنامج وكأنه يتوقف قليلًا. لدي برنامجي الرئيسي، وفيه استدعاء دالة. سيتوقف البرنامج الرئيسي قليلًا. ويمكنكم التعامل مع استدعاء الدالة كبرنامج صغير يحتاج إلى العمل والانتهاء، وإعادة قيمة، قبل أن يستطيع البرنامج الرئيسي استئناف التنفيذ.
ينشئ ذلك البرنامج الصغير، استدعاء الدالة، بيئته الصغيرة الخاصة التي يعيش فيها. في تلك البيئة يستطيع إنشاء متغيرات كما نفعل في برنامج عادي، وتعديل متغيرات، وطباعة أشياء. يستطيع إنجاز كل هذا العمل داخل جسمه. وعند نقطة ما ينهي عمله ومهمته، وتكون لديه قيمة هي نتيجة كل العمل الذي أنجزه.
وتلك القيمة هي ما نأمل أن يعيده إلى البرنامج الرئيسي. ثم يستطيع البرنامج الرئيسي إنهاء الـ— إنهاء عمله. الأمر الأساسي هنا أنكم في كل مرة تستدعون فيها دالة، تنشئون أساسًا بيئة جديدة. وهذه البيئة منفصلة تمامًا عن بيئة البرنامج الرئيسي. بمجرد انتهاء الاستدعاء، تختفي بيئته. وأي متغيرات أُنشئت داخل تلك البيئة تختفي أيضًا. فلا يبقى لنا إلا ما في البرنامج الرئيسي.
سنتحدث الآن قليلًا عن البيئات. إذا فهمتم هذا، ستفهمون 80% من الدوال وما تفعلونه بها. حين تشغّلون البرنامج أولًا، يدخل أساسًا ما نسميه البيئة العامة (Global environment)، بيئة البرنامج الرئيسي. وكلما استدعيتم دالة، ننشئ هذه البيئة الجديدة. ماذا يحدث بالضبط حين ننشئ هذه— حين نجري هذه الاستدعاءات؟ كيف تتفاعل البيئات؟
الإجابة أنها لا تتداخل في الواقع كثيرًا. لا تتداخل إلا عبر تمرير المعاملات وإعادة القيم. لكن بخلاف ذلك، يمكن أن توجد في البيئتين المختلفتين، بيئة البرنامج الرئيسي وبيئة استدعاء الدالة، متغيرات لها الاسم نفسه ولا تتداخل لأنها موجودة في بيئات مختلفة.
سننظر إلى هذا المثال لإظهار ذلك. هذه دالة بسيطة جدًا، لا تفعل الكثير. تأخذ معاملًا واحدًا، ربما عددًا، وتضيف إليه 1. تأخذ x وتنفّذ x + 1 وتُعيد إسناد x إلى النتيجة، ثم تنفّذ عبارة الطباعة هذه وتعيد قيمة x الجديدة. أضافت 1 إلى ما مررتموه إليها، وتعيد تلك القيمة الجديدة.
صفحة المصدر 12
هذا التعريف. مرة أخرى، يبقى في ذاكرة Python فقط، ولا يُشغّل فعلًا حتى نستدعي الدالة. المعاملات هنا حين كتبنا الدالة تُسمى معاملات شكلية (Formal parameters)، لأنه لا ترتبط بها قيمة فعلية. نكتب هذه الدالة بافتراض أننا سنحصل عند نقطة ما على قيمة لـx. لكن عند كتابة الدالة لا توجد قيمة لـx، بل هي هذا المتغير المجرد فحسب.
ونستخدم المتغير x داخل جسم الدالة، بافتراض أننا سنحصل عند نقطة ما على قيمة أولية له. فتكون x = 3، وعندها يستطيع الجسم التنفيذ. حين تستدعون دالة في نطاق البرنامج الرئيسي (Scope)، عندئذ تمررون— تجرون استدعاء بمعامل فعلي (Actual parameter). لاحظوا أنني أستخدم هنا الاسم نفسه x. لكن x داخل البرنامج الرئيسي مختلفة عن x التي هي المعامل الشكلي للدالة.
يُطابق هذا المعامل الفعلي، حين نستدعي الدالة، مع المعامل الشكلي. وعندئذ يمكن أن يأخذ المعامل الشكلي قيمة الاستدعاء، وهي 3. وفي الواقع لا يهم ما نسمي هذا المتغير في الخارج. يمكننا كتابة x = 3 واستدعاء f(x)، لكن يمكن أن تكون لدينا أيضًا y = 3 ونجري الاستدعاء نفسه تمامًا، f(y)، لأننا نريد تمرير 3 كمعامل لهذا الاستدعاء.
فـx هنا في الخارج مختلفة عن x هنا. إذن الـ— آه، نعم، تفضل.
أحد الحضور: أيهما الشكلي؟
آنا بيل: الشكلي هو الذي في تعريف الدالة. نسميه شكليًا لأنه لا ترتبط به قيمة حين تكتب الدالة أولًا. تكتب الدالة أولًا، ولا يحدث شيء هنا. ثم تكون لديك شيفرة تأخذ قيمًا بالفعل الآن، ويمكنك تشغيلها.
لنتتبع الشيفرة قليلًا قليلًا، لنرى بالضبط البيئات التي تُنشأ حين نستدعي دوال. مرة أخرى، هذا صندوقي الأسود، دالة. حين أشغّل البرنامج أولًا، انتهينا من تعريف الدالة. فنحن عند هذه النقطة في البرنامج، قبل تنفيذ x = 3 مباشرة. داخل ذاكرة Python في حاسوبي توجد بيئة واحدة منشأة: بيئة البرنامج الرئيسي. الشيء الوحيد في هذه البيئة هو f، لأنني عند هذه النقطة من البرنامج، حيث السهم الأحمر، لم يكن لدي إلا تعريف دالة.
مرة أخرى، هي تعريف— دالة اسمها f، وهي كائن. لم تُشغّل بعد. إنها كائن يحتوي شيفرة. الآن لدينا x = 3، وهذا سهل جدًا. داخل بيئة البرنامج الرئيسي لدي اسم متغير x، قيمته 3. ثم لدي استدعاء دالتي.
بمجرد أن ترى Python استدعاء دالة، تنشئ بيئة جديدة، وتُعلّق البيئة الحالية التي يجري منها الاستدعاء، أي بيئة البرنامج الرئيسي. هنا أستدعي الدالة f، فأنشئ الآن هذه البيئة الجديدة التي— فكروا فيها كالبرنامج الصغير، المهمة الصغيرة التي ينبغي إنجازها قبل أن يواصل البرنامج الرئيسي التنفيذ. علي تحديد ما يحدث في هذا البرنامج الصغير، استدعاء f هذا.
حسنًا، هذه بيئتي الجديدة، نطاق f. أول ما نحتاج إلى فعله تحديد معاملات f. ننظر إلى تعريف الدالة، ونرى معاملًا واحدًا اسمه x. سنأخذ x تلك، وأول ما سنفعله مطابقة المعامل الشكلي مع الفعلي. سنجعل المعامل الشكلي لـf المسمى x يأخذ القيمة 3.
صفحة المصدر 13
هذا ما كنا نفعله نوعًا ما بالفعل. لكننا الآن ندخل في التفاصيل، مجرد تفاصيل. طابقنا كل المعاملات، فيُنفّذ جسم الدالة. ومرة أخرى، جعلت هذه باهتة نوعًا ما لأننا لسنا في النطاق العام؛ نحاول تحديد ما تفعله f. يقول جسم f: خذ x، وأضف إليها 1، وأعد الإسناد إلى x. ما x داخل دالتي؟ 3. نضيف إليها 1، ونجعل x هي 4.
تجاوزت شيئًا واحدًا: إذا كانت لدي في البرنامج الرئيسي y = 3 وf(y)، فلم يكن ليتغير شيء فعلًا. المعامل الشكلي لـf ما زال x، وما زلت أطابق x مع القيمة الموجودة في الـ— هنا، في المعامل الفعلي.
في نطاق f لدي x هي 3. أزيدها بمقدار 1، فيعطيني 4، وأحفظها من جديد في x. ومرة أخرى لا يوجد تواطؤ— لا يوجد تصادم في التسمية، لأن نطاق f، بيئة f، فيه متغير اسمه x، وأفعل أشياء فقط بـx التي تعرفها f. لدي x أخرى داخل النطاق العام، لكنها معلقة الآن. حسنًا، نفّذت x = x + 1، ثم أنفّذ عبارة الطباعة.
في f(x)، تُطبع x = 4. ثم أعيد x. الشيء الذي يُعاد هو قيمة x، أي 4. وهذا يحل مجددًا محل استدعاء الدالة. يُعاد إلى من استدعاني، والبيئة التي استدعتني هي برنامجي الرئيسي فقط. سأعيد هنا 4، وسيستبدل هذا بذلك 4. بمجرد أن ترى الدالة الإعادة وتعيد القيمة، تختفي. لاحظوا أن x التي أنشأناها قد زالت.
الآن نحن في البرنامج الرئيسي، ولا يوجد التباس. لبرنامجي الرئيسي x الخاصة به. أما x الأخرى التي كانت جزءًا من تنفيذ f فزالت، لأن الدالة أنجزت عملها ولم تعد تحتاج إلى بيئتها. الآن تحل القيمة المعادة محل f(x)، ونرى z = 4. حسنًا، كان ذلك مفصلًا جدًا، لكن هذا تقريبًا ما يحدث خطوة خطوة حين نستدعي دالة وتُنشأ البيئات الجديدة.
إذا فهمتم ذلك، ينبغي أن يكون— أن يكون مباشرًا جدًا، ولن ترتبكوا حين ترون x في الخارج. لديكم f(x) كدالة، ثم ربما دالة أخرى g(x)، وهكذا. لمعرفة النطاق الذي أنتم فيه، والبيئة التي أنتم فيها، تحتاجون إلى معرفة التعبير الذي تقيّمونه. هنا كنا نقيّم هذا الاستدعاء، فيعني ذلك أننا داخل بيئة f.
مثال آخر، وهذا غريب قليلًا. يُظهر بعض تفاصيل Python الدقيقة، وهذه ليست بالضرورة صحيحة في لغات أخرى. سأرسم النطاقات هنا فقط. لنبدأ بالذي على اليسار. ترون هنا دالة واحدة f(y)، والبرنامج الرئيسي الذي ينشئ x = 5 ثم استدعاء إلى y.
داخل برنامجي الرئيسي لدي x = 5، ثم استدعاء للدالة f. استدعاء الدالة يعني أننا نحتاج إلى إنشاء نطاق جديد. تُعلّق هذه الآن حتى نحدد ما f(x) هنا. أول ما نحتاج إليه هو أخذ f وكل معاملاتها الشكلية— يوجد واحد، اسمه y— ومطابقتها مع المعاملات الفعلية. أستدعي f مع 5، فسأطابق y مع 5.
ستأخذ هذه الدالة— الآن جسمها، x = 1. فتنشئ أيضًا x قيمتها 1 داخل نطاقها فقط. تضيف 1 إلى x، فتصبح 2. ثم تطبع x، فستطبع 2. ثم تنتهي الدالة وتعيد None. لا توجد عبارة return، وتنتهي الدالة.
صفحة المصدر 14
انتهى هذا السطر الآن. وآخر ما تفعله الدالة بعدما تعيد هو اختفاء النطاق. وآخر ما علينا فعله الآن هو طباعة x. سيطبع ذلك قيمة x في النطاق العام، وهي 5. فمخرج قطعة الشيفرة الصغيرة هذه على اليسار هو 2 و5.
وماذا عن الشيفرة الوسطى؟ بالمثل، لدي تعريف دالة، ثم أنشئ x = 5، ثم أجري استدعاء g. حسنًا، x هي 5. بمجرد رؤية استدعاء دالة، أحتاج إلى إنشاء نطاق جديد، ومطابقة كل المعاملات الشكلية لـg. لها معامل شكلي واحد اسمه y. سيُطابق مع ما استدعيت به الدالة، 5. x هي 5 في الخارج.
تُطابق تلك مع 5. ماذا ستفعل هذه الدالة؟ تطبع x. ما x داخل نطاق g؟ هل لدي g داخل x— x داخل g؟ لا. فهذا شيء تفعله Python. تقول: إذا لم تكن في بيئتك متغير اسمه x في هذه الحالة، فانظر أبعد إلى الخارج، وانظر من استدعاك.
أي بيئة استدعت g؟ الرئيسية، صحيح؟ هل في بيئتك الأكبر، التي استدعتك، متغير اسمه x؟ نعم، صحيح؟ وهو 5. فتأخذ Python القيمة المرتبطة بتلك البيئة الأكبر. وإذا لم يوجد في تلك البيئة الأكبر، فستنظر أبعد فأبعد إلى الخارج حتى لا تبقى بيئة تنظر إليها.
ستطبع g قيمة x وهي 5. ثم ستطبع x + 1 وهي 6. ثم تنتهي وتعيد None. وبمجرد إعادتها None، يختفي هذا النطاق. وما يبقى لنا هو البرنامج العام، ونطبع x التي ما زالت خمسة. أريدكم أن تلاحظوا أن دالة g طبعت x + 1 لكنها لم تعدّل x أبدًا.
لم نقل شيئًا مثل x = x + 1 أو نحو ذلك. حددنا فقط ما x + 1 وطبعناه. حسنًا، مثال آخر، وهذا سينتهي بالفعل بخطأ. هنا لدي x = 5 كما من قبل، ثم استدعاء دالة h. مرة أخرى، استدعاء دالة يعني إنشاء نطاق جديد.
لدي متغير واحد y، وهو معاملي الشكلي. يُطابق مع ما أستدعي الدالة به، 5. عفوًا، تلك s. ثم ماذا تفعل هذه الدالة؟ السطر x += 1 هو x = x + 1.
هذا خطأ بالفعل. لا تسمح Python لكم بفعل ذلك. والخطأ الذي تعطيكم إياه هو بالفعل ما يقوله هناك: UnboundLocalError، يُشار إلى المتغير المحلي x قبل إسناد قيمة إليه. فلا تأخذ القيمة من النطاق الخارجي فعلًا كما فعلنا في الجزء الأوسط. لا تأخذها لأنها تعتقد أنكم تحاولون إنشاء متغير اسمه x داخل h، وتحاولون إضافة 1 إلى x. لكن لم يكن لديكم أصلًا سطر يقول إن x تساوي شيئًا داخل h.
حين تحاولون القول x = x + 1، تحاول البحث عن x داخل نطاق h، لكنه ليس فيه واحدة. ومن هنا يأتي الخطأ. وهذا ليس— أعني أنه مجرد تفصيل دقيق في Python، لكن من المهم نوعًا ما فهم أنكم تستطيعون الوصول إلى متغيرات، لكن لا تستطيعون تغيير متغيرات خارج نطاقكم. الأوسط يصل إلى متغير فقط، ويضيف إليه 1 ويطبعه، لكننا لم نقل أبدًا إن x تساوي هذه القيمة.
والأمر يشبه نوعًا ما، أظن أن الخطأ الذي تحصلون عليه يشبه لو جعلتم هذا شيئًا مختلفًا تمامًا، مثل z. ستحصلون على الخطأ نفسه: خطأ الإشارة إلى المتغير z قبل الإسناد. تستطيعون الحصول على x + 1، لكنني لا أعرف ما z. ينبغي أن تكون هذه z.
ملاحظة المترجم: وصف البحث «عمن استدعى الدالة» منقول كما ورد في الشرح الشفهي. في Python تُحل الأسماء وفق النطاقات المعجمية: المحلي، ثم المحيط بالتعريف، ثم العام، ثم المدمج (LEGB)، وليس وفق سلسلة المستدعين عمومًا. في أمثلة الدوال المعرفة في المستوى العام هنا، يأتي الاسم الخارجي من نطاقها العام. ويمكن تعديل ربط اسم عام بإعلان
global، كما يذكر المصدر في الصفحة التالية.
صفحة المصدر 15
أحد الحضور: هل يمكنك القول، بدلًا من التعريف مثلًا، z = x ثم تنفيذ x += 1؟ لأنك تأخذينها من الخارج و—
آنا بيل: إذا كنت— لا، لأنك إذا أردت— إذا أردت القول صراحة إنك تأخذها من الخارج، فهناك كلمة مفتاحية اسمها global تحتاج إلى كتابتها، تقول صراحة: أنا آخذ هذا المتغير الذي ليس جزءًا مني، إنه جزء من— موجود في البرنامج الرئيسي، النطاق العام.
حسنًا، آخر ما أريد الحديث عنه هو استخدام الدوال كوسائط لدوال أخرى. الطريقة التي كنت أشرح بها تعريف الدالة نوعًا ما تقول أساسًا إنه حين نعرّف دالة، تضع Python بعض الشيفرة في الذاكرة واسمها اسم الدالة. فاسم الدالة ينشئ لي أساسًا كائنًا داخل الذاكرة، يحدث أنه كائن دالة.
ولأريكم ما يعنيه ذلك فقط، لدينا دالة is_even. أنشأناها بالتأكيد. إذا قلنا ما نوع is_even، فهو function. إذن لدالة is_even نوع بالفعل، ونوعها دالة في Python. الدوال أساسًا كائنات فقط، تمامًا كما أن العدد الصحيح كائن والقيمة المنطقية كائن وعدد الفاصلة العائمة كائن. الدالة كائن، تبدو مختلفة فحسب؛ ترتبط بها مجموعة شيفرة.
إذا كانت الدالة كائنًا، فهذا يعني أننا نستطيع استخدام عامل إسناد على اسم دالة. يمكن أن يكون لدينا اسمان لدوال يشيران إلى شيفرة الدالة نفسها. نستطيع استخدام دالة وسيطًا لدالة أخرى، كمعامل لدالة، أو إعادة دالة من دالة أخرى.
هذا مثال. تخيلوا أن هذا ملف شيفرتنا، ولدينا الذاكرة. أول سطر هنا، التعريف، ينشئ لي كائن الدالة هذا أساسًا في الذاكرة. يشبه متغيرًا نوعًا ما. is_even اسم كائن الدالة هذا، ويرتبط هذا المتغير بكائن دالتي الذي ترتبط به شيفرة.
فكروا في الدالة ككائن فقط. وبالمثل، حين نكتب r = 2، أفكر في ذلك كالأمر نفسه. r هو الاسم، ولدي كائن عدد صحيح قيمته 2. هذا بالضبط ما يحدث حين ننشئ تعريف دالة. وبالمثل، pi = 22 / 7: pi الاسم المرتبط بكائن فاصلة عائمة له تلك القيمة.
ما نستطيع فعله، بعدما قررنا أن الدالة أساسًا كائن له اسم، هو قول سطر كهذا: my_func = is_even. الطرف الأيمن هنا اسم دالتي فقط، وليس استدعاء دالة. لاحظوا أنه لا توجد أقواس بعد is_even، ولا معامل ولا شيء من ذلك. إنه حرفيًا اسم دالتي.
ما فعلته في النهاية داخل الذاكرة أن لدي اثنين— عفوًا، لدي اسمان، my_func وis_even، يشيران كلاهما إلى كائن الدالة نفسه تمامًا. وهذا يعني أنه يمكن الإشارة إلى كائن الدالة، دالة is_even هذه، بكلا الاسمين. في السطرين التاليين، a تساوي هذا وb تساوي هذا، أشغّل الشيفرة نفسها، مشارًا إليها فقط بأسماء مختلفة.
سترتبط a بـFalse وb بـTrue، لأنني أصل أساسًا إلى الشيفرة نفسها بأسماء مختلفة. هل هذا مفهوم؟ نعم، رائع. كل شيء في Python كائن، بما فيه الدوال. فكرة غريبة، لكنها هكذا. لننظر إلى هذه الشيفرة. لدي ثلاثة تعريفات دوال واستدعاء دالة واحد فقط.
صفحة المصدر 16
ما تعريفات الدوال؟ واحدة سميتها calc، تأخذ ثلاثة معاملات. وواحدة add، تأخذ معاملين. وواحدة div، تأخذ معاملين. تفعل add شيئًا بسيطًا جدًا. ربما تحتوي div عبارة طباعة، لكنها تفعل أيضًا شيئًا بسيطًا جدًا. أما calc فهي الغريبة حقًا، صحيح؟ تأخذ هذه المعاملات الثلاثة، لكن ما الذي تفعله هنا؟ تتعامل نوعًا ما مع أحد المعاملات، op، العملية (Operation)، كدالة.
هذا هو الغريب في calc. لنتتبع الشيفرة لنرى بالضبط ما يعنيه لنا. حين أشغّل برنامجي أولًا، لدي ثلاثة تعريفات دوال، فأنشئ ثلاثة كائنات دوال داخل الذاكرة: calc كائن دالة له شيفرة، وadd كائن دالة له شيفرة، وdiv كائن دالة له شيفرة.
ثم نصل إلى الجزء الجيد: res تساوي استدعاء الدالة. ستكون res متغيرًا له قيمة. أي قيمة؟ نحتاج إلى تحديد ذلك. calc استدعاء دالة. في كل مرة يوجد استدعاء نحتاج إلى إنشاء بيئة جديدة. ننشئ الآن بيئة calc.
وضعنا نطاق البرنامج الرئيسي جانبًا الآن، ونركز على ما ستفعله calc. أول ما علينا فعله أخذ كل معامل ومطابقته مع المعاملات الفعلية. الأول op، يُطابق مع add. التالي x، يُطابق مع 2. والأخير y، يُطابق مع 3.
هل الجميع بخير حتى الآن؟ نعم، حسنًا. طابقت حرفيًا فقط أسماء المعاملات الشكلية مع الفعلية. انتهينا من مطابقة المعاملات. الآن نستطيع تشغيل جسم الدالة. return، ما هذا؟ لنستبدل op وx وy بالقيم الفعلية. يصبح هذا أساسًا إعادة استدعاء دالة، add مع 2, 3. استبدلت الأسماء فقط، هذا كل شيء.
ما add مع 2, 3؟ استدعاء دالة آخر، صحيح؟ ستحتاج calc إلى التعليق لأن علي تحديد ما ستعيده add. ماذا ستعيد لي؟ add مع 2, 3 ما أحاول تحديده. سأطابق a مع 2، وb مع 3. ستكون 5 هي القيمة المعادة. تعيد 5 إلى من استدعاها، ومن استدعاها هو calc هنا.
فيُستبدل هذا التعبير op(x, y)، الذي كان add(2, 3)، بـ5. هل الجميع بخير حتى الآن؟ رائع. تستطيع calc الآن الانتهاء. لاحظوا أن add أنهت عملها، فاختفت. الآن تستطيع calc أخيرًا إعادة قيمتها، فتستطيع الانتهاء أيضًا. ستعيد 5 إلى من استدعاها، البرنامج الرئيسي. وأخيرًا أنهت calc عملها وأعادت 5.
خطوة خطوة نتتبع الشيفرة نوعًا ما، والدوال من الخارج إلى الداخل، ونستبدل المتغيرات حيث يلزم. الآن دوركم. أخبروني ما قيمة res مع هذا الاستدعاء لـcalc، وما الذي سيُطبع؟ نستطيع حتى كتابة دوالنا. ماذا لدي في البرنامج الرئيسي؟
أحد الحضور: calc وdiv.
آنا بيل: نعم، calc وdiv دالتاي. هذا فقط؟
أحد الحضور: هل res موجودة أيضًا؟
آنا بيل: أي واحدة؟
أحد الحضور: res.
صفحة المصدر 17
آنا بيل: نعم، ستكون res النتيجة. نعم. وسنضع لـres علامة استفهام لأننا لا نعرف ما هي بعد. وما أول ما أحتاج إلى فعله؟ نعم، إنشاء نطاق جديد، بالضبط. هذا نطاق calc. سنطابق op مع div. ماذا لدي؟ x وy مع 2 و0. شكرًا. ماذا ستفعل op؟
أحد الحضور: [غير مسموع].
آنا بيل: نعم، بالضبط. ننشئ نطاقًا آخر لـdiv. a هي 2 وb هي 0. فنحن على عمق نطاقين نوعًا ما. ماذا ستفعل div؟
أحد الحضور: تطبع مقامًا [غير مسموع].
آنا بيل: نعم، تطبع div الشيء "Denom was 0". وماذا تعيد div؟
أحد الحضور: None.
آنا بيل: None، ممتاز. تعيد div هنا None إلى calc، ثم تختفي div. ثم تُعاد None من calc هنا، وتختفي calc. وكل ما يبقى لي res = None. بالضبط، القيمة المعادة من calc.
مثال آخر يُظهر النطاق، لإظهار هذه— الفكرة نفسها تقريبًا. لدي ثلاث دوال هنا، func_a وfunc_b وfunc_c. ترون أن func_a لا تأخذ معاملات، وfunc_b تأخذ معاملًا واحدًا، وfunc_c تأخذ معاملين. إذا فحصنا الشيفرة نرى أن أحدها يفعل شيئًا غريبًا. سيكون دالة بالفعل، لأنكم ترون أننا نستدعيه كدالة هنا في الداخل. فنعرف أن f لا بد أن تكون دالة.
إذا شغّلنا هذا البرنامج، فتعريفات الدوال الثلاثة الأولى تضع لنا شيفرة في الذاكرة أساسًا. وحين نستدعي func— آسفة، func_a، ينشئ هذا نطاقًا جديدًا. ليس لـa معامل— أو ليس لـfunc_a معاملات. فلا شيء نربطه. كل ما ستفعله الدالة هو طباعة "inside func_a" ثم إعادة None. إذن سيطبع ذلك كله None.
بعد ذلك، ستكون func_b استدعاء آخر هنا، فينشئ نطاق دالة هنا. نطابق المعامل الشكلي y مع 2. انتهينا من مطابقة كل المعاملات، وما نحتاج إليه بعد ذلك تنفيذ الجسم.
نطبع "inside func_b"، وتعيد فقط القيمة التي مررتموها إليها، فهي ليست دالة ذكية أو مثيرة للاهتمام جدًا. تطبع ذلك وتعيد 2 إلى من استدعاها، هنا. تصبح عبارة الطباعة هذه طباعة 5 + 2، القيمة المعادة. فيطبع ذلك 7 في وحدة التحكم.
وأخيرًا، ستكون الدالة المثيرة للاهتمام func_c. لاحظوا أنني أستدعيها بدالة فعلية لدي، func_b، واحدة من التي عرّفتها هنا. func_c استدعاء دالة، فهذا نطاقي. أطابق المعامل الشكلي f مع func_b، وz مع 3، واحدًا واحدًا فقط. ثم أنفّذ جسم func_c. يقول الجسم الآن اطبع هذا وأعد هذا. فنطبع العبارة.
ثم تقول الإعادة أساسًا: ما استدعاء الدالة f(z)؟ علينا تحديد القيم الفعلية، وهي func_b(3). هذا استدعاء آخر، فيعني نطاق دالة آخر. مرة أخرى، ليست func_b دالة ذكية أو مثيرة للاهتمام جدًا. تأخذ 3 فقط، وتطبع inside func_b، وتعيد 3 إلى من استدعاها.
صفحة المصدر 18
انتهت تلك الدالة. ثم تستطيع func_c الانتهاء وإعادة 3 إلى من استدعاها، هنا في الخارج. لاحظوا أنه بمجرد انتهاء استدعاء دالة وتنفيذه الإعادة، فورًا— تزول كل متغيراته وكل ما أُنشئ داخل تلك البيئة. تُمحى.
حسنًا، سأعطيكم نحو دقيقة لتجربة هذا. اكتبوا دالة تستوفي المواصفات التالية. لدي دالة اسمها apply. وcriteria معامل شكلي. فعند نقطة ما سيكون لديكم دالة تفعل هذا: تأخذ عددًا، عددًا صحيحًا، وتعيد قيمة منطقية. أيًا تكن طريقة الدالة في فعل ذلك، فهذا ما سيُمرّر. ثم n عدد صحيح.
أريد منكم إخباري بعدد الأعداد من 0 إلى n التي تستوفي ذلك المعيار. حين أطبّق دالة criteria على الأعداد من 0 إلى n، كم منها يعيد بالفعل True من تلك الدالة؟ ولأريكم شيئًا— ما يعنيه هذا بصورة ملموسة، هذه دالتي apply، وهذه دالة يمكنني استدعاء apply بها: is_even. آسفة، كذبت. أظن أننا سنرى is_even بضع مرات أخرى في هذه المحاضرة.
هذه دالة is_even. وأشغّل apply أساسًا بالقول إنني أريد تشغيل دالة apply مع الاسم is_even. هنا أطابق الاسم مع الأعداد من 0 إلى 10. سأعطيكم نحو دقيقة لتجربتها، ثم أستطيع كتابتها كي يكون لدينا بعض— كي ننتهي في الوقت المحدد. هل لدى أحد بداية؟
نعرف أننا نريد المرور بكل عدد من 0 إلى n لنرى هل ينطبق عليه هذا المعيار. ما البداية لتحقيق ذلك؟ نعم.
أحد الحضور: for i in range—
آنا بيل: for i in range لـn + 1 لأننا نريد تضمين n؟ كيف نطبّق دالة criteria على كل قيمة من هذه القيم؟
أحد الحضور: criteria ثم أقواس؟
آنا بيل: نعم، بالضبط. نقول criteria فقط، وسيُستبدل هذا الاسم بأي دالة سنستدعي بها apply، مع i. وستكون criteria(i) هذه أساسًا القيمة المعادة من criteria. ماذا قلت إنها تعيد؟ تأخذ عددًا وتعيد قيمة منطقية. فنعرف أن هذه قيمة منطقية. ماذا أريد أن أفعل بهذه القيمة؟
إذا كانت صحيحة أريد عدّها، وإن لم تكن فلا أفعل. إذن if criteria(i)، ثم count += 1. سأضع هذا هنا في الأعلى، ولنتذكر تهيئة عدادنا. ثم هذا كل شيء، صحيح؟ إذا لم تستوفِ المعيار، فلا يهمني فعل شيء بها. ثم نعيد count فقط.
لاحظوا أنني أستخدم دالتي هنا. إنها المعامل فقط، كعنصر نائب لأي دالة أخرى. دالة is_even هذه، حين تكون معاملًا لـapply، ستخبرني بـ6، صحيح؟ 0 و2 و4 و6 و8 و10، ست قيم تستوفي هذا المعيار.
والرائع أنني أستطيع بالفعل إنشاء أي دالة. إذا أردت دالة اسمها is_five مثلًا، تأخذ عددًا وتعيد True إذا كان ذلك العدد يساوي 5، فما زالت دالة تأخذ عددًا صحيحًا وتعيد قيمة منطقية.
كل ما أحتاج إلى فعله بعد ذلك تشغيل apply مع دالة is_five. غيّرت ذلك هنا فقط، ثم إذا شغّلتها ينبغي أن تعطيني قيمة واحدة فقط؛ الـ5 بالطبع هي التي تستوفي معيار is_five هذا بين 0 و10.
صفحة المصدر 19
نعم، هذا أساسًا كل شيء. رأينا بعض الدوال— هناك الكثير مما تستطيعون فعله بالدوال. إنها أساسًا كائنات في Python، فيمكن التعامل معها كما مع أي كائن آخر. يمكن أن تكون معاملات لدالة، وأن تُعاد من دالة، وأن تُسندوا اسمًا آخر إلى جسم الدالة هذا، وأشياء كهذه.
أريتكم كيف تفكرون في البيئات كي لا تصبح التسمية مربكة. بمجرد إجراء استدعاء دالة، يعني ذلك إنشاء بيئة أخرى. فلا تأثير للمتغيرات المنشأة داخل تلك البيئة على متغيرات أخرى منشأة في بيئات أخرى. والدوال طريقة جميلة جدًا لكتابة شيفرة يسهل البناء عليها. حسنًا، هذا كل شيء. شكرًا لكم.
أحد الحضور: شكرًا لك.