مقدمة في علوم الحاسوب والبرمجة باستخدام Python › المحاضرة 9: دوال Lambda، والصفوف (Tuples)، والقوائم (Lists) 11 / 30 السابق التالي

التفريغ الكامل للمحاضرة 9: دوال لامبدا والصفوف والقوائم

التفريغ الكامل للمحاضرة 9: دوال لامبدا والصفوف والقوائم

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

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

منهج المطابقة: عناوين صفحات المصدر تقابل فواصل الصفحات في الاستخراج الرسمي 1lMet5GPIW1ZhnesX_FN2udiX7bcr0mJd_transcript.txt، وعددها 11 صفحة غير فارغة. تُترجم الفقرات بالترتيب مع التكرار والتصحيحات الشفهية وأسئلة الحضور والإشارات الصوتية. تبقى أسماء Python ورموزها دون ترجمة. كل إضافة توضيحية موسومة بملاحظة المترجم. عنوان التسجيل في المصدر: MITOCW | 6.100L Lecture 9 Version 2.mp4.

صفحة المصدر 1

[صرير]

[حفيف]

[نقر]

آنا بيل: اليوم سنختتم الحديث عن الدوال (Functions) بالحديث عن أشياء تسمى دوال لامبدا (Lambda functions)، كطريقة لإنشاء دوال مجهولة الاسم (Anonymous functions). وهذا سينهي تقريبًا استكشافنا لإنشاء الدوال. وفي الجزء الأخير من المحاضرة، سنقدم نوعين جديدين من الكائنات: الصفوف (Tuples) والقوائم (Lists).

لنتذكر ما فعلناه في المرة الماضية. انتهينا بهذا المثال. أنشأنا دالة، كتبتموها لي أنتم، ثم كتبناها ونقحناها معًا. أنشأنا هذه الدالة التي تسمى apply.

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

هنا استخدمنا criteria اسم متغير فقط، بافتراض أن نوع criteria دالة. ووفقًا لهذا التوثيق، نفترض أنها تأخذ عددًا وتعيد قيمة منطقية (Boolean). فكتبنا جسم الدالة بافتراض صحة ذلك. وهنا بالضبط استخدمنا هذه الدالة المسماة criteria.

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

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

والمثير للاهتمام في المثال أن دالة is_even هذه بسيطة جدًا، سطر واحد أساسًا. لا تجري أي حسابات داخل جسمها. تأخذ أساسًا قيمة، مدخلًا، وتعيد شيئًا.

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

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

صفحة المصدر 2

تبدأ الكلمة المفتاحية lambda الدالة المجهولة. وتخبر Python أننا ننشئ هذه الدالة المجهولة. ليست lambda اسم الدالة؛ إنها تخبر Python فقط أننا سننشئ دالة في سطر واحد، بلا اسم. وسيكون x أي معاملات نتوقع أن تأخذها هذه الدالة.

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

لاحظوا هنا أننا لا نضع كلمة return فعلًا حين ننشئ دالة لامبدا. نجري فقط العملية التي نريد إعادة قيمتها. فـx%2=0 هي أساسًا جسم دالة لامبدا هنا. الأمر الأساسي في دوال لامبدا أنها تتيح لكم إنشاء كائن دالة سريع جدًا تريدون أساسًا استخدامه مرة واحدة فقط. ولذلك لا نعطيه اسمًا.

ملاحظة المترجم: يظهر الاختبار في التفريغ الرسمي بصيغة x%2=0؛ صيغة اختبار المساواة في Python هي x%2==0، مثل lambda x: x%2==0.

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

في هذه الحالة، تخبرني هذه الدالة فقط هل المدخل يساوي 5. وهنا توقفنا في المرة الماضية حين شغّلنا apply مع دالة is_5. فيطبع ذلك أن apply مع is_5 تساوي 1. لا يوجد إلا عدد صحيح واحد بين 0 و10 يعيد تطبيق هذه الدالة عليه True.

والآن مع دالة مجهولة، لأريكم كيف نكتب دالة لامبدا لـis_5 هذه، سيكون شكلها هكذا. مرة أخرى، نخبر Python أننا ننشئ دالة مجهولة. لها مدخل واحد فقط x، ثم نقطتان رأسيتان، ولا return، وجسم الدالة هو فقط الشيء الذي نريد إعادته: x==5.

مرة أخرى، لاحظوا أننا لا نمرر الاسم بالفعل. لا يوجد اسم لهذه الدالة المجهولة. لكنها تعمل بالطريقة نفسها تمامًا كما لو أنشأنا هذه الدالة هنا. ويمكنني تشغيلها مجددًا، وترون أن apply مع اسم الدالة تساوي 1. ومن الواضح أن apply مع هذه الدالة المجهولة تعيد 1 أيضًا.

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

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

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

حسنًا، لاختتام كيفية استدعاء دوال لامبدا: حين استدعينا apply(is_even, 10)، فإن المكافئ لاستدعاء اسم الدالة ذاك لكن باستخدام دالة لامبدا هو وضع جسم دالة لامبدا بأكمله داخل استدعاء الدالة الأخرى. هنا نعرّفها ونخبر Python في الوقت نفسه أن هذا هو مدخلي للدالة.

صفحة المصدر 3

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

لكن إذا نظرنا إلى الجسم هنا، فإن fn— المدخل— تُستدعى بالفعل كدالة داخل الجسم. نستطيع فورًا معرفة أن fn ستكون دالة حين نجري الاستدعاء الفعلي لـdo_twice. وبالفعل، حين نستدعي do_twice هنا في الأسفل، تُطابق n مع 3، ويُطابق المعامل الثاني، fn، مع دالة لامبدا المجهولة هذه.

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

حين نجري استدعاء الدالة أولًا، صحيح— أو آسفة، حين نشغّل هذا البرنامج أولًا، إذا كان يحتوي هذه الأسطر الثلاثة من الشيفرة، تنشئ Python بيئتنا العامة (Global environment). داخل البيئة لدينا تعريف دالة واحد هنا، فيكون هذا كائن الدالة. ثم لدي الشيء الذي يبدأ بالفعل استدعاءات الدوال، برنامجي.

لدي عبارة print ستطبع نتيجة فعل شيء. أول ما أراه هنا أن لدي استدعاء دالة do_twice. فأمضي من اليسار إلى اليمين.

أول ما أفعله حين يوجد استدعاء دالة هو إنشاء بيئة جديدة. داخل بيئة do_twice هذه، علي أن أرى ما تأخذه. ما معاملاتها الشكلية؟ يوجد واحد اسمه n وآخر اسمه fn. فهناك معامل n، والآخر fn.

والآن أطابق أساسًا، واحدًا واحدًا، المعامل الشكلي مع المعامل الفعلي. تُطابق n مع 3، لأنه المعامل الأول لـdo_twice. وتُطابق fn مع كائن الدالة هنا. إذن تُطابق fn مع دالة لامبدا هذه.

حسنًا، هذا بالضبط ما قلته. أجرينا المطابقة، والآن بعد المطابقة نستطيع تنفيذ جسم do_twice. الجسم يقول return. ثم علي استبدال كل موضع أرى فيه fn بدالة لامبدا هذه، وكل موضع أرى فيه n بالعدد 3 هذا.

ستكون fn استدعاء دالة. كلما رأينا استدعاء دالة نحتاج إلى إنشاء نطاق دالة (Function scope). قبل أن أستطيع تنفيذ return، قبل أن تنتهي do_twice وتعيد قيمتها، ترى استدعاء دالة. حين يوجد استدعاء دالة نحتاج إلى إنشاء نطاق آخر، بيئة أخرى.

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

حسنًا، في هذه الدالة، مرة أخرى باتباع القواعد واحدة واحدة، علينا تحديد معاملاتها. يوجد واحد اسمه x. فهذا معاملي x. ثم أحتاج إلى تحديد ما يُطابق معه. إنه يُطابق مع المعامل داخلها. لكن المعامل داخلها هو fn(n).

صفحة المصدر 4

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

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

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

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

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

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

أحد الحضور: لماذا لامبدا— لماذا كان هناك اثنان منهما مرة أخرى؟

آنا بيل: كان هناك اثنان لأن fn الخارجية تستدعي fn داخلية، لذلك نحن—

أحد الحضور: آه.

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

أحد الحضور: مع دوال لامبدا، هل نستخدم print أم apply، لأننا إذا [غير مسموع]؟

آنا بيل: apply كانت مجرد دالة كتبتها أنا. ففي هذا المثال الجديد كنت أطبع فقط نتيجة استدعاء تلك الدالة.

أحد الحضور: حسنًا.

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

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

ما أنواع البيانات (Data types) التي رأيناها حتى الآن؟ رأينا الأعداد الصحيحة وأعداد الفاصلة العائمة— أعدادًا أساسًا. رأينا القيم المنطقية كقيم صدق. ورأينا نوع None هذا، النوع الذي له قيمة واحدة هي None. ورأينا أيضًا نوع بيانات السلسلة النصية.

صفحة المصدر 5

يمكننا التفكير في نوع بيانات السلسلة النصية بوصفه نوع بيانات مركبًا (Compound data type)، مثل تتابع من المحارف المفردة. وفي الواقع، كنا نستخدم السلسلة بهذه الطريقة، لأننا استطعنا الفهرسة (Indexing) داخلها للحصول على المحرف عند الفهرس 0، واقتطاع (Slicing) السلسلة الفرعية نوعًا ما للحصول على طول السلسلة.

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

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

حسنًا، الصفوف تتابعات مرتبة من الكائنات قابلة للفهرسة (Indexable ordered sequences). هذا وصف طويل نوعًا ما، فلنفككه. أولًا، هو تتابع من الكائنات، تمامًا كما كانت السلسلة تتابعًا من المحارف المفردة. سيكون الصف تتابعًا، ليس من المحارف فقط، بل من أي نوع من الكائنات.

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

كيف ننشئ هذه الصفوف؟ علي أن أذكر أن بعض الناس ينطقونها «تَبلز»، لأنها أشبه بشيء من نوع صف ذي n عناصر. يمكنكم نطقها «توبلز» أو «تَبلز»، كما تحبون.

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

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

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

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

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

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

صفحة المصدر 6

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

وبقية هذا في الواقع عمليات رأيناها بالفعل على السلاسل. لن أمر عليها بتفصيل كبير. يمكننا استخدام الأقواس المربعة للفهرسة داخل الصف، للحصول على العنصر عند فهرس معين.

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

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

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

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

أو بمجرد إنشاء سلسلة abc في الذاكرة، لا تستطيعون الدخول إلى الذاكرة وتغيير السلسلة. تستطيعون بالتأكيد إنشاء كائنات جديدة مبنية على هذه السلسلة، لكن لا تستطيعون الدخول وتعديل ذلك الكائن بعدما يُنشأ. بمجرد صنع تتابع الصفوف، لا تستطيعون الدخول وتغييره. نعم؟

أحد الحضور: لدي سؤال. إذا أعدت كتابة t تساوي، ثم [غير مسموع] شيء مختلف، فهل يكون مثل خطأ؟

آنا بيل: إذا كتبت t تساوي، ثم شيئًا مختلفًا؟

أحد الحضور: نعم، [غير مسموع] لا تستطيعين تعديل شيء واحد.

آنا بيل: نعم، سؤال جيد. المتغير t، أي الاسم t والكائن الذي يرتبط به، شيئان مختلفان. سيظل الكائن الذي يرتبط به موجودًا في الذاكرة. سنفقد الارتباط به فحسب.

يشير t في البداية إلى هذا. لكن إذا قلت لاحقًا إن t تساوي شيئًا آخر، فما زال هذا موجودًا هناك. لكن t ستشير إلى هذا الشيء الجديد.

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

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

صفحة المصدر 7

العنصر الثالث العدد الصحيح 4، والرابع كائن صف يحدث أن له عنصرين داخله. لكن كائن الصف هذا الذي أشير إليه بـseq، لا يوجد في seq إلا أربعة عناصر. ويحدث فقط أن الأخير صف.

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

وبقية هذه الأمثلة هي أساسًا ما رأيناه مع السلاسل، باستثناء هذا هنا. إذا فهرسنا العنصر الأخير في seq هنا، 1، فاصلة، 2، فهذا صف آخر، أليس كذلك؟ فينبغي أن يترتب على ذلك أنني أستطيع أخذ ذلك الصف والفهرسة داخله أكثر. وهذا ما يفعله هذا السطر.

حين نقرأ تعبيرًا، نمضي من اليسار إلى اليمين. فـseq عند الفهرس 3 تعطيني أساسًا 1، فاصلة، 2. ثم إذا فهرست أكثر داخل 1، فاصلة، 2 عند الفهرس 0، فسآخذ العدد 1. أوصل أساسًا كل عمليات الفهرسة هذه معًا في سلسلة.

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

هناك أمر أريد ذكره: نستطيع التكرار على صف (Iteration) كما استطعنا التكرار على سلسلة. لا أقصد على الفهارس، بل على العناصر مباشرة. حين كررنا على سلسلة مباشرة، استطعنا أخذ المحارف عند كل فهرس في متغير الحلقة.

وبالمثل، نستطيع التكرار على صف لأخذ العناصر عند كل فهرس مباشرة. هنا for e in seq ستجعل متغير الحلقة e يأخذ كل عنصر من الصف مباشرة، لا الفهرس، بل كل عنصر. أثناء التكرار، ستكون قيمة e أولًا 2، ثم a، ثم 4، وأخيرًا هذه القيمة 1، فاصلة، 2.

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

في ماذا نستخدم الصفوف؟ كان هناك مثال منذ زمن في بداية 6.100L، حاولنا فيه تبديل المتغيرات (Swapping). وقلنا أساسًا إن هذه الطريقة لم تعمل لأننا كتبنا فوق المتغير.

كتبنا فوق المتغير، ثم لم نستطع العودة إلى القيمة التي كُتب فوقها. كان حلنا إنشاء هذا المتغير المؤقت لحفظ القيمة قبل الكتابة فوقها، ثم الكتابة فوق المتغير، ثم استخدام المتغير المؤقت لاستعادة تلك القيمة المحفوظة.

يتبين أن الصفوف تتيح لنا بالفعل تنفيذ أسطر الشيفرة الثلاثة هذه في سطر واحد هنا. نستطيع القول x, y = y, x. هذا إسناد (Assignment). وهو مسموح لأن الطرف الأيسر أساسًا مجموعة متغيرات في تتابع، ويُقيَّم الطرف الأيمن أولًا كما نفعل مع عبارة إسناد.

صفحة المصدر 8

تأخذ y القيمة 2 لأنها قيمتها في الأعلى، وتأخذ x القيمة 1. فـy هي 2 وx هي 1 هنا في الطرف الأيمن.

ثم تطابق Python، واحدة واحدة، القيم على اليمين مع القيم على اليسار المفصولة بفواصل. فما لدينا أساسًا هو x = 2 وy = 1، ثم أُعيد ربط القيم. مفيد جدًا جدًا جدًا— استخدام جيد جدًا للصفوف هنا.

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

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

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

في هذا المثال تحديدًا، لدي دالة تحسب خارج القسمة (Quotient) وباقيها (Remainder) حين تُقسم x على y. نعم، الدالة نفسها تستخدم القسمة الصحيحة لإيجاد خارج القسمة، وعامل الباقي لإيجاد الباقي، ثم تعيد حساب q هذا، عددًا ما، وحساب r هذا، عددًا آخر، كعنصرين في صف.

تعيد Python كائن الصف الواحد هذا باستخدام هذا السطر الذي يعيد هذا الكائن. وحين أجري استدعاء quotient_and_remainder مع 10، فاصلة، 3، ستدخل وتحسب خارج القسمة 3، والباقي 1، وتعيد كائنًا واحدًا، 3، فاصلة، 1. ثم يُسند إلى هذا المتغير الذي سميته both في هذه الحالة.

إذا أردت الوصول إلى جزء خارج القسمة من both، أكتب both[0]. وجزء الباقي من both يكون both[1]، للوصول إلى العنصر ذي الفهرس صفر والعنصر ذي الفهرس واحد من القيمة المعادة. والآن إذا أردت حفظ خارج القسمة والباقي صراحة كمتغيرين بعد إعادتهما، أستطيع فعل الحيلة التي رأيناها في الشريحة السابقة. كانت الحيلة x, y تساوي صفًا آخر.

وهذا ما أفعله هنا. أجري استدعاء دالة quotient_and_remainder مع 5، فاصلة، 2. سيعيد ذلك 2، فاصلة، 1. ثم سيكون لدي quot, rem = 2, 1. ستطابق Python، واحدًا واحدًا، quot مع 2 وrem مع 1.

وما يعنيه هذا لنا من حيث الشيفرة هو أننا نستطيع بعدها فعل ما نريده في بقية الشيفرة، بافتراض أن quot وrem مجرد متغيرين عاديين. هنا أريكم فقط أن بإمكانكم طباعتهما في عبارتي الطباعة. فيكون لدي هنا خارج القسمة 2 والباقي 1، بهذه الأسطر الاثنين من الشيفرة.

صفحة المصدر 9

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

لنعطكم دقيقتين للعمل على هذا. اكتبوا دالة تستوفي المواصفة. اسمها char_counts. لدي مدخل هو سلسلة s، محارف صغيرة. افترضوا أنها تحتوي حروف علة وحروفًا ساكنة فقط. أعيدوا لي صفًا يكون العنصر الأول فيه عدد حروف العلة في s.

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

يمكنكم محاولة كتابة الشيفرة قرب السطر 65 تقريبًا. ثم يمكننا كتابتها معًا. حسنًا، كيف ستقاربون هذه المسألة؟ ما الخطوة الأولى هنا؟ نعم.

أحد الحضور: إنشاء سلسلة تحتوي كل حروف العلة؟

آنا بيل: نعم، نستطيع إنشاء سلسلة تحتوي كل حروف العلة. vowels تساوي aeiou بالحروف الصغيرة. نعم، جميل. التالي؟ نعم.

أحد الحضور: إذا كان الـ[غير مسموع] في تلك القائمة، فيمكن أن يكون لدينا عدد [غير مسموع] لتتبعه، مثل زائد يساوي [غير مسموع]؟

آنا بيل: نعم. نعم، حروف العلة زائد يساوي 1. وإلا؟ نعرف أنه ليس حرف علة، فنحتفظ بعداد الحروف الساكنة زائد يساوي 1. هذا عداد الحروف الساكنة، وهذا عداد حروف العلة. لكن ما char في هذه الحالة؟

أحد الحضور: [غير مسموع] char في ذلك؟

آنا بيل: نعم، بالضبط. علينا استخدام حلقة: for char in s. نحتاج إلى النظر إلى كل محرف داخل s. وهنا، بما أننا نتعامل الآن مع أشياء قد لا تكون أعدادًا صحيحة في حلقات for، نستطيع كتابة ملاحظات صغيرة لأنفسنا مثل «char هي a ثم b ثم c»، أو نحو ذلك، لتذكيرنا بأن char ليست الفهرس، بل شيء فعلي. وماذا نحتاج إلى فعله أيضًا؟

أحد الحضور: [غير مسموع]

آنا بيل: نعم، نستطيع تهيئة c وv بالصفر. نستطيع استخدام الحيلة التي تكتبون فيها c, v = 0, 0، أو كتابتها في سطرين منفصلين، كله جيد. وأخيرًا، هذا ينجز العمل لنا. لكن الدالة تحتاج إلى شيء تُظهره نتيجة لذلك.

أحد الحضور: [غير مسموع]؟

آنا بيل: نعم، بعد الحلقة سنعيد الصف c، فاصلة، v— آسفة، v، فاصلة، c على الأرجح. وإذا شغّلناها، تطابق ما توقعناه: 1, 3 و0, 5. ويمكنكم تخيل إضافة بضع حالات اختبار أخرى، ربما شيء بسلسلة فارغة ينبغي أن يعيد 0, 0، وربما شيء كله حروف علة، ينبغي أن يعيد عددًا ما، فاصلة، 0.

صفحة المصدر 10

حسنًا، شيء آخر نستطيع فعله بالصفوف هو إنشاء هذه الدوال التي تأخذ عددًا متغيرًا من الوسائط (Variable number of arguments) كمعامل. تذكروا أننا حين نعرّف الدوال، نخبر Python أساسًا بعدد المعاملات التي نتوقع أن تأخذها. لكن يمكن أن تكون هناك دوال، مثل max أو min، تستطيع أخذ معاملين هنا. ولاحظوا أنه لا توجد أقواس إضافية.

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

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

وفي هذه الحالة تحديدًا، لا نكتب max أو min أو sum الخاصة بنا، بل دالة المتوسط (Mean) الخاصة بنا. ستأخذ دالة المتوسط هذه عددًا متغيرًا من الوسائط، وتحدد متوسط كل هذه القيم. والطريقة بسيطة جدًا الآن بعدما عرفنا أننا نستطيع التعامل مع args كصف لمجموعة أعداد.

نمر بحلقة على كل العناصر في args، ونجمع هذا المجموع الجاري، وفي النهاية نعيد المجموع مقسومًا على عدد الوسائط المعطاة: نعيد total مقسومًا على طول args. ثم حين نستدعي الدالة التي كتبناها، mean هنا، ستصبح args صفًا يحوي كل المعاملات الموجودة هناك.

وهذا هو المثال، الذي يعني أننا نستطيع استخدام دالتنا للحصول على متوسط 1, 2, 3, 4, 5, 6. لكن نستطيع استخدام الدالة نفسها تمامًا للحصول على متوسط 6, 0, 9 مثلًا. في الحالة الأولى لدي ستة معاملات كمدخل، وفي الثانية لدي ثلاثة فقط. وتتيح لي تلك النجمة الصغيرة في وسائطي فعل ذلك.

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

أريد تقديم القوائم اليوم. وكثير من الشرائح هنا منسوخ وملصق أساسًا من شرائح الصفوف. الاختلاف الوحيد في الشرائح التي لدي عن القوائم هو طريقة تعريف القائمة.

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

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

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

صفحة المصدر 11

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

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

هذا مثال نريد فيه إيجاد مجموع عناصر قائمة. الشيفرة على اليسار صعبة التحليل قليلًا، صحيح؟ لدينا متغير حلقة يمر على مجال طول n. ثم علي الاحتفاظ بمجموعي الجاري.

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

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

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

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

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

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

نستطيع إجراء تغيير صغير واحد على جسم الحلقة. ما زال متغير الحلقة يتكرر على كل العناصر في القائمة l. ثم إذا كتبنا ملاحظة لأنفسنا أن s ستكون ab، ثم def، ثم g، وأردنا شيفرة تأخذ مجموع كل الأطوال في القائمة، فإن total += e على اليسار تصبح total += len(s) على اليمين.

سيأخذ طول s، واحدًا واحدًا، القيمة 2 لأن ab طولها 2، ثم 3 لأن def طولها 3، ثم 1 لأن g طولها 1. تبدو الشيفرة متشابهة جدًا، لكن لها وظائف مختلفة بحسب ما نريد فعله.

صفحة المصدر 12

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