النص الكامل للمحاضرة 26: الوصول إلى القوائم والتجزئة والمحاكاة والختام
النص الكامل للمحاضرة 26: الوصول إلى القوائم والتجزئة والمحاكاة والختام
المصدر الأصلي: النص الرسمي الإنجليزي، PDF، صفحة المحاضرة الرسمية، فيديو المحاضرة الرسمي.
النسبة والترخيص: آنا بيل (Ana Bell)، مقرر 6.100L: مقدمة في علوم الحاسوب والبرمجة باستخدام Python، خريف 2022، معهد ماساتشوستس للتكنولوجيا، MIT OpenCourseWare. هذه ترجمة عربية تعليمية غير رسمية، وليست معتمدة من MIT ولا تعني تأييده لها. تخضع المواد الأصلية المشمولة والترجمة لرخصة المشاع الإبداعي: النسبة–غير تجاري–المشاركة بالمثل 4.0 (CC BY-NC-SA 4.0): يجب حفظ النسبة، والاستخدام غير التجاري، ومشاركة التكييفات بالرخصة نفسها. لا تشمل الرخصة الافتراضية مواد الأطراف الثالثة المستثناة؛ انظر شروط MIT OCW.
منهج الترجمة والتغطية: تُترجم هنا صفحات التفريغ الرسمي الثماني عشرة غير الفارغة، وفق فواصل الصفحات الفعلية في ملف PDF المستخرج، بما فيها صفحة الشكر والتصفيق الأخيرة. يُحفظ ترتيب الفقرات وكلام المتحدثة والطلاب والتكرار والتردد والتصحيحات الشفهية والإشارات الصوتية والمواضع غير المسموعة، دون تلخيص. تُحفظ أسماء Python والمعرّفات والرياضيات، وتُقرن المصطلحات المهمة بالإنجليزية. الملاحظات الموسومة «ملاحظة المترجم» إضافات منفصلة لتوضيح التبسيطات أو الزلات، لا كلام منسوب إلى المصدر. لا تُستنسخ هنا صور الشرائح أو الميمات.
صفحة المصدر 1
MITOCW | المحاضرة 26 من 6.100L، متعدد.mp4
[صرير] [حفيف] [نقرات]
آنا بيل (ANA BELL):
مرحبًا بكم إذًا في الحصة الأخيرة. من فضلكم لا تأتوا يوم الأربعاء. لن أكون هنا. اليوم سنربط فقط بعض الخيوط المتبقية المتعلقة ببعض الموضوعات التي رأيناها خلال المقرر. ثم سأقدّم ختامًا للأشياء التي تعلمناها، والمقررات المحتملة التي قد ترغبون في دراستها بعد هذا.
حسنًا. كما ذكرت، سنربط اليوم بعض الخيوط المتبقية المتعلقة بالقوائم (Lists) والقواميس (Dictionaries). سيُجمع هذان الموضوعان في جزء واحد من هذه المحاضرة. وسيشمل أيضًا قليلًا عن التعقيد (Complexity) — بعض الأشياء التي تعلمناها فقط، وتوضيح بعض التفاصيل التي تجاوزتها خلال المحاضرات القليلة الماضية.
ثم سنتحدث عن المحاكاة (Simulations). المحاكاة مفيدة جدًّا جدًّا — هي فكرة مفيدة جدًّا يمكنكم تطبيقها بالفعل بما تعلمتموه في هذا المقرر. وسأريكم بعض المواضع المفيدة التي يمكنكم فيها تطبيق الحوسبة (Computation) والمحاكاة لفعل بعض الأشياء المثيرة للاهتمام. ثم سنقدّم الختام.
فلنبدأ أولًا الحديث عن القوائم. كانت القوائم أول بنية بيانات (Data Structure) صادفناها. وكانت مفيدة حقًّا، أليس كذلك؟ رأينا السلاسل النصية (Strings). ورأينا الصفوف (Tuples) وأشياء كهذه. لكن بمجرد أن رأينا القوائم، انفتح عالم جديد كامل من الإمكانات لكيفية التعامل مع البيانات. فالقوائم تسلسلات (Sequences) من الكائنات (Objects).
تجاوزت نوعًا ما كيفية تنفيذها فعليًّا في الذاكرة (Memory). لذا أريد الحديث عن ذلك قليلًا. لكن ما تحدثنا عنه هو التعقيد، التعقيد التقاربي (Asymptotic Complexity) لعمليات القوائم. كان بعض هذه واضحًا جدًّا. فهذه التي هي Θ(n) هنا في الأسفل كانت واضحة، لأنك، حسنًا، للتحقق من التساوي بين قائمتين، تحتاج طبعًا إلى النظر إلى كل عنصر في القائمة.
إذًا، ذلك هو Θ لطول القائمة. للتحقق مما إذا كان عنصر في قائمة، أو للمرور عبر قائمة، من الواضح أنه Θ(n)، لأن عليك النظر إلى كل عنصر في القائمة. لكننا لم نتحدث حقًّا عن التعقيدات هنا في الأعلى. فالوصول تحديدًا إلى عنصر في القائمة هو Θ(1).
هذا يعني إذًا أنه، إذا كانت لديك قائمة فيها مجموعة كاملة من العناصر، فإن الحصول على العنصر عند موقع محدد في الذاكرة له تعقيد زمني ثابت (Constant Time Complexity). فلا يعتمد في الأساس على طول القائمة. إنه فوري. سنرى إذًا لماذا يكون ذلك.
لنفترض أولًا، للتبسيط، أننا نخزن في الذاكرة قائمة من الأعداد الصحيحة (Integers) فقط. أعرف أن القوائم يمكنها تخزين قوائم أخرى وقواميس وأشياء كهذه. لكن في هذه الشريحة الأولى فقط، لنفترض أن كل ما نفعله هو تخزين أعداد صحيحة. والطريقة التي تفعل بها Python ذلك هي أنك، عندما تنشئ قائمة — لنقل إنك تملؤها أولًا بطول L.
تخصص Python أولًا كتلة ذاكرة متجاورة (Contiguous Memory Block) تضم L مواقع ذاكرة. فإذا كانت لديك قائمة فيها 100 عنصر، مملوءة في البداية بـ100 عنصر، فإن Python ستنشئ لك أولًا تسلسلًا من مواقع الذاكرة المحجوزة لهذه القائمة.
ثم تقول: حسنًا، إذا كانت هذه ستحتوي على أعداد صحيحة فقط، فسأقول إن كل واحد من مواقع الذاكرة هذه سيحمل أربعة بايتات (Bytes) لذلك العدد الصحيح. هكذا نمثل عددًا صحيحًا. وقد تكون ثمانية بايتات، أو شيئًا آخر على أجهزة مختلفة. لكن في هذا المثال المحدد، فلنقل فقط إن كل واحد من مواقع الذاكرة تلك سيخزن عددًا صحيحًا. وطوله أربعة بايتات.
ملاحظة المترجم: هذا نموذج تعليمي مبسّط لشرح الوصول بالفهرس. قوائم Python المعتادة في CPython تخزن مراجع إلى الكائنات، بما فيها الأعداد الصحيحة؛ لا تخزن الأعداد الصحيحة نفسها بوصفها خانات ثابتة من أربعة بايتات. أحجام الأعداد والمؤشرات الفعلية تعتمد على التنفيذ والمنصة.
صفحة المصدر 2
حسنًا، إذا كانت هذه القائمة متجاورة — مجموعة من كتل الذاكرة كلها بالترتيب — فللوصول إلى العنصر ذي الفهرس i، لا تحتاج إلا إلى قليل من الرياضيات. لدي هنا عدد صحيح في موضع واحد في كتلتي المتجاورة. ثم ربما لدي عدد صحيح آخر عند الموضع التالي، وهكذا وهكذا، حتى يكون لدي عدد صحيح آخر عند الموضع i.
وبما أنها متتالية، فللوصول إلى موقع العنصر في هذه الخانة i، كل ما أحتاج إليه هو الانتقال بذلك العدد من مواقع الذاكرة من بداية قائمتي. هذه رياضيات فقط. البايت الواحد ثمانية بِتّات (Bits). فإذا كان لدي 4 * 8 بِتّات مضروبة في i، زائد الموقع الأول، فسيخبرني ذلك بالموقع الدقيق للعدد الصحيح ذي الفهرس i.
كل هذا ممكن لأن مواقع الذاكرة هذه خُصصت بالترتيب. لو لم تكن مخصصة بالترتيب، فربما لم يكن هذا بالسهولة نفسها. نعم؟
طالب (STUDENT):
لماذا هي 32؟
آنا بيل (ANA BELL):
32 لأن — إذا قلت إن عددًا صحيحًا يُخزَّن في أربعة بايتات، فبالبِتّات هذا 4 * 8، لأن البايت الواحد فيه ثمانية بِتّات. إذًا 8 * 4 هي 32 بِتًّا لبايت واحد، نعم.
ملاحظة المترجم: «لبايت واحد» زلة منقولة من التفريغ؛ 32 بِتًّا تساوي أربعة بايتات. وفي حساب عنوان ذاكرة مقاس بالبايتات، تكون إزاحة خانة حجمها أربعة بايتات
4*i، لا32*i؛ يستخدم الشرح هنا القياس بالبِتّات لتوضيح ضرب حجم الخانة في الفهرس.
حسنًا، لكن هذا يفترض أنني أخزن أعدادًا صحيحة. ومن الواضح أن القوائم يمكن أن تحتوي على قوائم أخرى. ويمكن أن تحتوي على صفوف. ويمكن أن تحتوي على قواميس. وبعض تلك الكائنات قد لا يناسب هذا العدد المحدد من البايتات، أربعة بايتات، لأن بعض تلك الكائنات قد يكون كبيرًا جدًّا جدًّا بذاته.
في تلك الحالة المحددة — لنقل إن القائمة غير متجانسة (Heterogeneous) — فهذا لا يربكنا، لأننا يمكن أن نقول: حسنًا، بدل تخزين الكائن نفسه عند كل موقع ذاكرة — نجح ذلك للأعداد الصحيحة، لكنه قد لا ينجح عندما نضطر إلى تخزين قائمة من 1,000 عنصر عند موقع ذاكرة معين — بدل تخزين العنصر نفسه، فلنخزن مؤشرًا (Pointer).
والمؤشر ليس إلا عددًا يخبرك بموقع الذاكرة الذي قد تكون فيه تلك القائمة مخزنة، أو ذلك القاموس مخزنًا. فإذا خزّنا مؤشرًا عند موقع ذاكرة معين، فهذا يعني أن هذه هي مجددًا ذاكرتي المتجاورة المخصصة لقائمة بطول L أو شيء كهذا — ثم هنا ما زلت أخزن عددًا صحيحًا. وذلك العدد الصحيح يخبر Python بأيّ موقع ذاكرة تقفز إليه للحصول على العدد الصحيح المخزن هناك، أو شيء كهذا.
وهنا قد تكون لدي قائمة أخرى أخزنها. لكنني لا أخزنها بالضبط في ذلك الموقع من الذاكرة. يشير إليها هذا المؤشر الذي يخبر Python، مجددًا، بالقفز إلى موقع ذاكرة مختلف، قد تكون فيه تلك القائمة نفسها مخزنة على نحو متجاور. في هذا المثال هنا، ما زلت أخزن أعدادًا. لكن هذه الأعداد تقابل موقع ذاكرة يخبر Python أين تذهب للحصول على عنصري في تلك القائمة.
إذًا، من حيث الحساب للحصول على العنصر ذي الفهرس i في القائمة، سيكون الأمر نفسه. ما زلت أخصص في قائمتي الأصلية أربعة بايتات لتخزين مؤشري — مجددًا، مجرد عدد. وللحصول على الموقع i، كل ما أحتاج إليه هو إخبار Python بأن تذهب إلى بداية هذه القائمة، زائد 32*i موقعًا إلى الأسفل، للوصول إلى ذلك العنصر.
إذًا، هذه الصيغة هنا، إضافة بداية موقع الذاكرة لهذه القائمة إلى 32*i، هي رياضيات فقط. لا يوجد شيء هنا يعتمد على طول القائمة. فللحصول على العنصر عند الموقع i، في مكان ما داخل هنا، كل ما نفعله هو بعض الرياضيات — عملية جمع وعملية ضرب.
وبما أن ذلك مجرد — لا يعتمد أيّ من ذلك على طول القائمة، فإن تعقيد الوصول إلى العنصر ذي الفهرس i في القائمة ثابت، رياضيات فقط. ونستخدم فكرة أننا نعرف بالضبط عدد مواقع الذاكرة التي نحتاج إلى القفز عبرها للوصول إلى الموقع i. هل هذا مفهوم؟ حسنًا.
صفحة المصدر 3
يقودنا ذلك إلى السؤال. حسنًا، نحن نخزن قائمة من العناصر. ونستخدم فكرة أن للقائمة فهارس تخبرنا بالقيمة — يوجد عنصر عند الفهرس 0، وعنصر عند الفهرس 1، وعنصر عند الفهرس 2، وهكذا. إذًا، للقائمة ترتيب. وبفضل ذلك الترتيب، نستطيع الوصول بالفهرس إلى عنصر عند الموقع i في زمن ثابت.
لكن لنفترض أننا أردنا تخزين قاموس. القاموس ليس له ترتيب. وماذا يخزن القاموس؟ يخزن زوج مفتاح–قيمة (Key-Value Pair). في القائمة، يمكنك التفكير في «المفتاح»، بين علامتي اقتباس، على أنه الفهرس — 0، 1، 2، 3، 4، وهكذا — وفي القيمة على أنها العنصر عند ذلك الفهرس. لكن في القاموس، المفتاح ليس مرتّبًا. ويمكن أن يكون المفتاح أيّ شيء.
ملاحظة المترجم: قواميس Python الحديثة تحفظ ترتيب الإدراج (Insertion Order)، لكنه لا يوفر فهرسة عددية ولا يفرض ترتيبًا أبجديًّا على المفاتيح. كذلك ليست «أيّ شيء» حرفيًّا: يجب أن تكون المفاتيح قابلة للتجزئة (Hashable)، كما تشرح المتحدثة لاحقًا.
هنا لدي قاموس يربط ربما اسمًا بقيمة منطقية (Boolean). ربما هل الطالب في هذا المقرر، True أو False. تنفيذ ساذج (Naive Implementation) للقاموس قد يكون أن نقول: حسنًا، فلننفّذ عناصر القاموس — أي زوج المفتاح–القيمة — على هيئة قائمة، عنصرين فقط. العنصر الأول في تلك القائمة هو مفتاحي. والعنصر الثاني في قائمتي هو قيمتي.
إذًا، هنا تنفيذ ساذج حقًّا يستخدم القائمة. ولدي أربع مدخلات في قاموسي. العناصر عند الفهرس 0 كلها سلاسل نصية. والعنصر عند الفهرس 1 في كل موضع هو قيمتي المرتبطة بذلك المفتاح. حسنًا، إذا أردت الوصول داخل هذه القائمة للحصول على القيمة المرتبطة بـEric، مثلًا، هل يمكنني فعل ذلك في زمن ثابت؟
لا، أليس كذلك؟ لأنه لا يوجد فهرس عددي هنا. لا يوجد ترتيب لهذه المجموعة من القيم. إنها ليست حتى بالترتيب الأبجدي — A، ثم J، ثم E، ثم S. ولا يوجد ترتيب مضمون للقواميس على أيّ حال. لذا، للبحث عن عنصر في هذا التنفيذ الساذج للقاموس، حيث تضع كل العناصر بالترتيب في قائمة فقط، يكون الأمر Θ(n)، حيث n طول قائمتنا.
إذًا، هذا التنفيذ للقاموس لا ينجح. ومع ذلك، عندما عرضت عليكم هذه الشريحة قبل بضع محاضرات، رأينا شيئًا مثيرًا للاهتمام. هذا ما «أثبتناه» للتو، بين علامتي اقتباس، أن زمن الوصول في القائمة ثابت. لكن زمن الوصول في القاموس ثابت أيضًا في الحالة المتوسطة (Average Case).
في أسوأ حالة (Worst Case)، هو Θ(n). الوصول إلى عنصر في قاموس هو Θ(n) لأننا، في أسوأ حالة، قد نخزن القاموس هكذا. إنه فقط قائمة بكل مدخلات قاموسنا، كلها بالترتيب. فللبحث عن فهرس واحد، سنضطر إلى المرور عبر القائمة كاملة والتحقق مما إذا كان العنصر عند الفهرس 0 هو الذي نبحث عنه، ثم أخذ العنصر عند الفهرس 1 بوصفه قيمته.
لكن في الحالة المتوسطة — وهذا ما سنراه بعد قليل. في الحالة المتوسطة، فإن الوصول، الزمن الذي يستغرقه البحث عن مفتاح في قاموس، ثابت. إنه فعلًا Θ(1)، وهو ما يجعل القواميس بنى بيانات قوية جدًّا لاستخدامها في مواقف كثيرة. فلماذا يكون ذلك؟
حسنًا، الأمر يتعلق بفكرة التجزئة (Hashing). فالطريقة التي تُخزَّن بها القواميس فعلًا في الذاكرة ليست قائمة من مجموعة من المدخلات. أظهرنا للتو أن ذلك غير عملي. يؤدي ذلك إلى زمن بحث Θ(n). لذا بدلًا من ذلك تستخدم شيئًا يسمى جدول تجزئة (Hash Table). تحدثنا عن هذا باختصار.
جدول التجزئة مثل قائمة طويلة فحسب. وفهارس جدول التجزئة هي فعلًا أشياء تبحث عنها باستخدام دالة تجزئة (Hash Function). كيف يعمل هذا إذًا؟ حسنًا، أيّ مفتاح تود إضافته إلى قاموس تُشغَّل عليه فعلًا دالة تجزئة. وهذه الدالة تأخذ ربما عددًا صحيحًا، وربما عددًا عشريًّا، وربما سلسلة نصية، وربما صفًّا.
صفحة المصدر 4
أيّ كائن قابل للتجزئة، تجزّئه، أي تأخذ ذلك الكائن. إذا كان سلسلة نصية، فستعطينا عددًا صحيحًا. وإذا كان صفًّا، فستعطينا عددًا صحيحًا. إذًا، إذا جزّأته، فهذا يعني أنها تأخذه إدخالًا وتعطينا عددًا، عددًا صحيحًا. وهذا العدد الصحيح هو ما يُستخدم فهرسًا للبحث عنه في جدول التجزئة، للبحث عن القيمة المرتبطة به في جدول تجزئة.
بهذا المعنى، يكون البحث نفسه في زمن ثابت، لأننا أظهرنا للتو أن البحث عن عنصر في قاموس باستخدام الفهرس في زمن ثابت. وإذا كانت دالة التجزئة تلك أيضًا في زمن ثابت، فالزمن الذي يستغرقه البحث عن عنصر في قاموس هو أيضًا زمن ثابت.
ملاحظة المترجم: ورد «قاموس» في وصف البرهان بالفهرسة، لكن البرهان السابق كان عن القائمة. عمليًّا تُحوَّل قيمة التجزئة إلى موضع مناسب لحجم الجدول، ثم تُعالج التصادمات. كما أن حساب تجزئة مفتاح طويل قد يعتمد على طول المفتاح؛ وصف الزمن الثابت يفترض كلفة مناسبة لحساب التجزئة ومقارنة المفاتيح.
هذه بعض أمثلة تشغيل دالة التجزئة في Python فعلًا على كائنات مختلفة. هنا في الأعلى، إذا شغّلت — إنها حرفيًّا دالة في Python — hash لبعض المعاملات — في هذه الحالة 123 — فهي تعيد القيمة نفسها فقط. فتجزئة عدد ما هي العدد نفسه. يمكننا تجزئة سلسلة نصية. سيعطينا ذلك هذا العدد المحدد — مجددًا، عددًا صحيحًا. وتجزئة صفّ تعطينا أيضًا عددًا ما.
إذًا، هذه كلها مجرد دالة تعمل وراء الكواليس، تأخذ هذا الكائن القابل للتجزئة وتعطينا عددًا. هذا كل شيء. لا يمكننا الآن تشغيل دالة تجزئة على قائمة، لأن القائمة قابلة للتغيير (Mutable)، ولذلك فهي غير قابلة للتجزئة (Unhashable). إذا تغيّر الكائن، فإن دالة التجزئة المشغَّلة على هذا الكائن ستعطينا قيمة مختلفة.
إذا شغّلتم هذا فعلًا على حاسوبكم، فقد تحصلون على إجابات مختلفة. أو إذا شغّلتموه في أوقات مختلفة، فقد تحصلون على إجابات مختلفة، لأن Python تضيف قليلًا من العشوائية إلى قيم التجزئة، تحسّبًا لرغبتكم في تشفير البيانات وأشياء كهذه. لكن عمومًا، ستحصلون دائمًا على عدد صحيح إذا شغّلتم دالة التجزئة على كائن غير قابل للتغيير (Immutable).
ملاحظة المترجم:
hashليست دالة تشفير (Encryption). تستخدم Python عشوائية التجزئة لبعض الأنواع للحد من هجمات التصادم، وتبقى تجزئة المفتاح نفسه ثابتة خلال عملية التشغيل الواحدة، وإن اختلفت بين العمليات. ولا تعني عدم قابلية التغيير وحدها أن كل كائن قابل للتجزئة؛ مثلًا، صفّ يحتوي على قائمة غير قابل للتجزئة. كذلك ليست تجزئة جميع الأعداد هي العدد نفسه؛ المثال 123 صالح، لكن العبارة العامة تبسيط.
يطرح ذلك إذًا السؤال: ما الحجم الذي ينبغي أن يكون عليه جدول التجزئة؟ إذا كان جدول التجزئة في الأساس مجرد قائمة طويلة، وإذا شغّلت دالة على كائن ما لتعطيني قيمة فهرس داخل تلك القائمة، فما حجم هذا الجدول؟ كم فهرسًا ينبغي أن يكون لدي؟ 1,000؟ مليون؟ عشرة ملايين؟ ما العدد الجيد؟
حسنًا، لنأخذ مثال سلسلة نصية. بالنسبة إلى سلسلة نصية، ما يمكننا فعله هو — ويمكننا استخدام اسمي مثالًا. إذا أردنا تجزئة اسمي بحيث يُجزَّأ كل اسم إلى شيء فريد، فما يمكننا فعله هو التالي.
يمكننا أخذ كل محرف في اسم شخص. ووراء الكواليس، لكل واحد من هذه المحارف فعلًا عدد صحيح — عذرًا، رمز ASCII مرتبط به، وهو شيء عددي. وما يمكننا فعله هو تحويل ذلك العدد إلى النظام الثنائي (Binary) فقط. فالحرف الكبير A يصادف أن قيمته الثنائية هذه — 01000001. والحرف الصغير n له هذه القيمة. والحرف الصغير a له هذه القيمة، وهكذا.
لدي هنا سبع مجموعات مختلفة من ثمانية بِتّات تقابل كل حرف في اسمي. إذا أخذت الآن تلك البِتّات. وضممتها معًا فقط، ووصلتها (Concatenate) لتعطيني عددًا كبيرًا جدًّا جدًّا واحدًا — فسيكون هذا كله عددًا كبيرًا جدًّا واحدًا — فإن العدد المقابل بالأساس 10 هو هذا الشيء الطويل جدًّا.
إذا فعلت ذلك، فما دام اسم الشخص فريدًا، فسينتهي به الأمر إلى عدد فريد مرتبط باسمه. ومن ثم يمكن استخدام ذلك العدد الفريد فهرسًا فريدًا في جدول تجزئة كبير جدًّا. فلنفكر في تجزئة أسماء طلاب المرحلة الجامعية الـ4,000 في MIT.
صفحة المصدر 5
لنفترض أن أطول اسم طوله 20 محرفًا. سيكون هناك إذًا 20 من هذه الحروف المختلفة التي نحتاج إلى تجزئتها. نستخدم العملية نفسها هنا. كل واحد من تلك المحارف العشرين يحصل على تمثيله الخاص ذي الثمانية بِتّات. إجمالًا، عدد البِتّات الذي سأستخدمه لتمثيل ذلك الاسم ذي العشرين محرفًا سيكون 8 * 20، أي 160 بِتًّا مختلفًا.
هذا عدد كبير من البِتّات. وإذا وصلت كل ذلك معًا، فالعدد الذي يقابله هو 2**160، وهو هذا الشيء هنا. إذا أردت أن تكون كل تركيبة حروف من الأبجدية قيمة فريدة في هذه القائمة الطويلة، فسأحتاج إلى قائمة بهذا الطول. لن أحاول حتى معرفة مدى كِبَر هذا العدد أو نطقه، لكنه كبير جدًّا جدًّا.
وجود قائمة، أي جدول تجزئة، بهذا العدد من المدخلات سيضمن لي أن الأسماء التي طولها 20 محرفًا يُجزَّأ كلٌّ منها إلى شيء فريد. لكن ليس لدي إلا 4,000 اسم أود وضعها في جدولي. لدي 4,000 اسم أود وضعها في جدول بهذا العدد من المواضع. هذه مساحة مهدرة كثيرة. نعم؟
طالب (STUDENT):
عذرًا، هل هي 160 لأنك حوّلته إلى الثنائي؟
آنا بيل (ANA BELL):
نعم، بالضبط. لكل محرف ثمانية بِتّات مرتبطة به. سيكون هناك إذًا 160 من هذه الأصفار أو الآحاد على التوالي. لمعرفة العدد المرتبط بذلك، نقول في الأساس — نحسب في الأساس 10000000 — وفي النهاية 20 صفرًا. وهذا يعطيني 2**160. هذا هو مدى كِبَر عددي.
ملاحظة المترجم: كلام «20 صفرًا» منقول كما ورد؛ كتابة
2**160بالثنائي هي 1 يتبعه 160 صفرًا. كذلك2**160هو عدد الأنماط الممكنة لـ160 بِتًّا؛ القيم نفسها تمتد من 0 حتى2**160-1، لا أن كل اسم يقابل العدد2**160نفسه.
طالب (STUDENT):
[غير مسموع]؟
آنا بيل (ANA BELL):
سيكون ذلك — سيكون ذلك لواحد — سيكون هذا عدد الخانات التي سأحتاج إليها كي تُربط تركيبات الحروف الفريدة بخانة واحدة. أي 0001 ستُربط بشيء واحد. و00010 ستُربط بشيء آخر. و0011 ستُربط بشيء آخر. إذًا، كل تركيبات الحروف هذه ستُربط كل واحدة منها بشيء فريد.
ولضمان ذلك، أحتاج إلى هذا العدد من الخانات. مجددًا، بما أنه ليس لدي إلا 4,000 طالب جامعي، فهذه مساحة مهدرة كثيرة. لا أستخدم إلا 4,000 من هذه الخانات لحفظ أسماء الطلاب. وذلك لأن كثيرًا من تركيبات الحروف تلك ليس صالحًا حقًّا.
فما الحل؟ توجد مساحة مهدرة كثيرة هناك. الحل إذًا أن نقول: حسنًا، أتعرفون؟ لا مانع لدي من جدول تجزئة أصغر. لا أحتاج إلى ذلك العدد الهائل من المدخلات في جدول تجزئتي. لا مانع لدي من وجود 10,000 موضع مثلًا، ثم بعض الأسماء التي يُصادف أن تُجزَّأ إلى الشيء نفسه، أو القول: لا مانع لدي من جدول تجزئة ذي مليون موضع. ومن تلك الأسماء الـ4,000، سيُستخدم بعض المواضع، ولن يُستخدم بعض المواضع، وقد تتصادم بعض الأسماء عند قيمة التجزئة نفسها. وهذا مقبول تمامًا.
إذا سمحنا بالتصادمات (Collisions)، فكيف سيبدو هذا؟ هذه صورة لجدول تجزئتنا. فكروا في جدول التجزئة مثل قائمة. السبب الذي يجعلنا نفكر فيه على أنه قائمة هو أن الوصول بالفهرس داخل قائمة في زمن ثابت. نستفيد من فكرة أننا إذا وصلنا بالفهرس داخل قائمة، فسيكون ذلك ثابتًا.
لنفترض إذًا أننا نضيف بعض الأسماء والدرجات إلى جدول تجزئتنا. هذا هو تمثيلنا للقاموس. القيم هنا تقول إن لدي جدول تجزئة فيه 16 مدخلًا مختلفًا، من 0 إلى 15. ومن 0 إلى 15 تقابل فهرس القائمة. فإذا كان لدي اسم ودرجة أود إضافتهما إلى جدول تجزئتي، فأحتاج إلى تشغيل دالة تجزئة على المفتاح. المفتاح هو الاسم. والدرجة هي القيمة المرتبطة بالاسم.
صفحة المصدر 6
لإضافة Ana بدرجة C إلى جدول تجزئتي، أحتاج إلى أخذ Ana لدي، وهي المفتاح، وتشغيل دالة تجزئة عليها، بحيث عندما أشغّل دالة التجزئة على هذا الاسم، A-n-a، تعطيني عددًا، عددًا صحيحًا بين 0 و15. وإذا استطعت فعل ذلك، فأعرف أنني أضفت مدخلتي هنا إلى إحدى هذه الحاويات (Buckets).
دالة تجزئة معقولة لتشغيلها على الاسم — ورأينا هذا في محاضرة القواميس — هي أن نقول: حسنًا، فلنجعل A يقابل 1، وB يقابل 2، وC يقابل 3، وهكذا. لاسمي، لدي 1 + 14 + 1 = 16. لكن بما أنني أريد ضمان أن تعطيني دالة التجزئة عددًا بين 0 و15، فلنأخذ باقي قسمته (Modulo) على 16.
يمكنني جمع كل الحروف في اسمي بلا مشكلة. ثم فلننهِ العملية بالقول: باقي القسمة على 16، ليعطيني الباقي — إما 0 أو 1 أو 2 أو 15. وإذا فعلت ذلك، أضمن أن زوج المفتاح–القيمة هذا سيُضاف إلى إحدى هذه الحاويات من 0 إلى 15. في هذه الحالة المحددة، تُربط Ana بدرجة C بـ0. هذا ما أخبرتني دالة التجزئة على اسمي أن أضيف — الموقع الذي أخبرتني دالة التجزئة على اسمي أن أضيف إليه. ها أنا إذًا أضع اسمي هناك.
فلنضف شخصين آخرين. هذا Eric. يُجزَّأ اسمه إلى 35 % 16. وهذا 3. سأضيف إذًا Eric ودرجته إلى الحاوية رقم 3. ثم يمكننا إضافة John بدرجة B. يُجزَّأ اسمه إلى 47 % 16. وهذا 15. يمكننا إذًا إضافة John هنا في الأسفل في الحاوية 15.
ثم فلنضف Eve بدرجة B. تُجزَّأ إلى 32 % 16، وهو أيضًا 0. وأتعرفون؟ كانت Anna بالفعل في الحاوية 0. لكن لا بأس بذلك، لأن أتعرفون؟ لدي أربعة أسماء هنا، أي أربع مدخلات أريد إضافتها إلى قاموس جدول تجزئتي. واثنان منها تصادما. لا بأس. ما زالت لدي حاويات كثيرة أخرى فارغة هنا.
ملاحظة المترجم: ورد الاسم
Annaفي هذه الجملة في التفريغ، بينما المفتاح الذي أُضيف أولًا هوAna؛ أبقينا الزلة ظاهرة.
إذا كان لدي 10 طلاب في مقرري، فمن المرجح ألا يُجزَّأوا جميعًا إلى 0. سيُجزَّأون على الأرجح إلى مكان ما داخل هنا، بحيث يكون التوزيع متوازنًا على نحو جميل. وربما من أصل 10 طلاب في مقرري، لم يتصادم إلا اثنان. وهذا أفضل بكثير من أن يكون كل طلاب المقرر مسرودين في قائمة طويلة واحدة.
عندما أبحث عن Ana، فالطريقة التي يعمل بها ذلك هي أنك تجزّئ الاسم Ana مرة أخرى. عندما تريد البحث عن درجة Ana، فهذا هو المفتاح. تجزّئ القيمة Ana مرة أخرى. تقول: مهلًا، Ana جُزِّئت إلى 0. ثم سأنظر إلى الحاوية 0 وأقول: حسنًا، دعوني أسرد كل من في الحاوية 0، وأرى هل يمكنني إيجاد Ana مع درجتها. يصادف أنها الأولى. لكن لو كانت لاحقًا، لظللت قادرة على الحصول عليها أسرع كثيرًا مما لو كان الجميع في قائمة طويلة واحدة. هل هذا مفهوم؟ مثل فكرة — نعم، تفضل.
طالب (STUDENT):
إذًا، ما زال يمكنك الوصول إليهم نوعًا ما. لكنك قد تحصلين على إجابتين بدل واحدة.
آنا بيل (ANA BELL):
بالضبط، نعم. ما زال يمكنك الوصول إليهم. قد تضطر فقط إلى النظر عبر قائمة من اثنين. هنا عند الحاوية 0 أخزن عمليًّا قائمة بكل ما جُزِّئ إلى 0، وهذا لا بأس به. نعم، هذان اثنان عليّ النظر عبرهما. ليسا أربعة. وليسا عشرة. وليسا مئة. ليس الجميع في صف واحد. نعم؟
طالب (STUDENT):
هل التعقيد هو Θ(n)؟
آنا بيل (ANA BELL):
تعقيد هذا سيكون فعلًا أصغر من Θ(n). وسيعتمد على دالة التجزئة التي نستخدمها. يجب أن تكون دالة التجزئة هذه متوازنة على نحو جيد. لا ينبغي أن تضع الجميع في الحاوية 0. عندئذ تكون دالة تجزئة عديمة الفائدة. ويعتمد أيضًا على حجم جدول التجزئة.
صفحة المصدر 7
إذا كان لدي ربما 1,000 شخص أخزنهم في 15 حاوية، فسيكون لدي كثير من التصادمات. لكن إذا كنت أخزن هؤلاء الأربعة فقط، أو ربما ثمانية، أو عشرة، أو شيئًا أقل من حجم جدولي، فستكون التصادمات أقل بكثير. سيكون أكثر توازنًا على نحو جيد، نعم.
طالب (STUDENT):
كنت أقصد Θ(n) مثل الأشياء التي في 0.
آنا بيل (ANA BELL):
آه، Θ(n) للأشياء التي في 0، نعم. ولا بأس بذلك، لأن ما نهتم به عادةً هو Θ لطول الإدخال. في هذه الحالة، هو Θ لـ — إذا كان لدي أربعة طلاب في مقرري، فلم يُربط إلا اثنان منهم بـ0. هنا إذًا هو الطول مقسومًا على 2. لكن لو كان لدي طلاب أكثر، فسيكون العدد أقل بكثير. سيكون اثنين من أصل عشرة، أو ربما اثنين من أصل 15 يُجزَّآن إلى الشيء نفسه، نعم.
ملاحظة المترجم: القسمة على ثابت لا تغيّر رتبة
Θ(n)؛ أما الزمن المتوقع الثابت للبحث في جدول التجزئة فيحتاج إلى توزيع جيد وحجم جدول يحافظ على عدد متوقع محدود من العناصر التي ينبغي فحصها، لا إلى عامل النصف وحده.
نعم، كما قال السؤال: ما الذي يجعل زوج جدول التجزئة ودالة التجزئة جيدًا؟ لأن هذا لا ينجح إلا إذا كانت لديك دالة تجزئة جيدة حقًّا، وجدول تجزئة جيد يرافقها. هذه فعلًا مسألة في علوم الحاسوب، مسألة بحثية قائمة بذاتها. فالناس يدرسون هذا فعلًا طوال حياتهم، ابتكار دوال تجزئة وجداول تجزئة جيدة.
بعض القواعد الأساسية — تريد أن يُربط المجال الذي يهمك — أي في هذه الحالة المحددة، صفّ، أو سلسلة نصية، أو أيًّا كان — بأعداد صحيحة بين 0 وحجم جدول التجزئة. في المثال السابق، لا نريد دالة تجزئة تأخذ باقي القسمة على 2، لأن كل شيء عندئذ سيُجزَّأ إلى 0 أو 1. إذا كان جدول تجزئتنا فيه 15 شيئًا، فعلينا ضمان أن دالة تجزئتنا ستعطينا عددًا بين 0 و15.
ملاحظة المترجم: الحد الأعلى الصحيح لفهارس جدول ذي
mخانات هوm-1. المثال السابق كان من 16 خانة بفهرسة من 0 إلى 15؛ ورد «15 شيئًا» في الكلام هنا.
ثانيًا، تريد أن تكون قيمة التجزئة محددة تمامًا بالقيمة التي تُجزَّأ. في هذه الحالة، لا نريد حدوث أيّ نوع من العشوائية، لأنه، حسنًا، إذا أردت البحث عن درجة Eve لاحقًا في الكود أو أيًّا كان، فأحتاج إلى تشغيل دالة التجزئة نفسها تمامًا على اسمها لتحديد الدرجة. إذا كانت هناك عشوائية في دالة التجزئة هذه، فقد لا تحصل على القيمة نفسها التي جُزِّئت إليها أصلًا. فستبحث في الحاوية الخطأ. وستقول على نحو غير صحيح إنه ليست لها قيمة، أو إنها ليست هناك.
ثالثًا، تريد استخدام الإدخال كله لتجزئة — ينبغي أن تستخدم الدالة الإدخال كله لتشغيل دالة التجزئة. مجددًا، في هذا المثال، لا نريد استخدام الحروف الأولى فقط من أسماء الناس، لأن ذلك سيؤدي إلى تصادمات أكثر كثيرًا مما لو استخدمنا مجموع كل حروف الأبجدية — أو كل حروف اسمهم.
هذه أفكار كبيرة حقًّا. ثم ما نريده من دالة تجزئتنا هو كل القيم. إذا شغّلت دالة التجزئة هذه على مجموعة من المدخلات المختلفة، حيث تخزن أسماء، أو تخزن — لا أدري — صفوفًا أو أيّ شيء تخزنه، فأنت تريد أن تعطيك هذه الدالة توزيعًا منتظمًا (Uniform Distribution) جيدًا من القيم. في جدول تجزئتنا هنا سابقًا، إذا أضفت مزيدًا من الأسماء إليه، فأريد ضمان أنها ستقع في الحاويات 2، 5، 6، 7، 8، 9، 10، 11، 12، 13، 14. لا أريد أن يُجزَّأ كل شيء إلى الرقم 0. سيكون ذلك سيئًا جدًّا.
وللتذكير جانبًا، في محاضرة القواميس، قلت فعلًا شيئًا مثل: في الوقت الحالي، فكروا في الكائنات التي يمكن أن تكون مفاتيح لقاموس على أنها كائنات غير قابلة للتغيير. وقلت إنها تقنيًّا قابلة للتجزئة، لكننا لا نحتاج إلى معرفة معنى ذلك. حسنًا، قابل للتجزئة يعني هذا فقط — يمكنك تشغيل دالة تجزئة على الكائن، وستحصل على القيمة نفسها مهما كان عدد المرات التي تشغّل فيها دالة التجزئة على ذلك الكائن.
صفحة المصدر 8
نظرنا إذًا إلى هذا المثال. ماذا يحدث إذا أضفنا طالبًا اسمه ليس غير قابل للتغيير، وليس قابلًا للتجزئة؟ القوائم كائنات قابلة للتغيير. ولذلك فهي ليست قابلة للتجزئة. هذا يعني أنه، إذا شغّلنا دالة تجزئة على قائمة اليوم، ثم غيّرنا القائمة ربما، فلن تُجزَّأ تلك القائمة غدًا إلى الشيء نفسه.
رأينا هذا المثال. لنفترض أن Kate بحرف K أُضيفت إلى جدول تجزئتنا. يُجزَّأ اسمها حاليًّا إلى 37 % 16، وهو 5. فأضفناها هناك. لنفترض الآن أننا نريد غدًا البحث عن درجاتها لنفعل أيّ شيء، لإدماجها في جدول بيانات أكبر. لقد غيّرت اسمها بين أمس واليوم. أصبحت الآن Cate بحرف C. إذا شغّلنا دالة التجزئة نفسها مرة أخرى على اسمها، فسيقودنا ذلك إلى النظر في حاوية مختلفة. هي ما زالت هناك. هي Kate بحرف K، كما أضفناها أصلًا. لكن اسمها الآن Cate بحرف C. نشغّل دالة التجزئة نفسها. تخبرنا بأن ننظر إلى الحاوية 13. وهي ليست هناك. لهذا لا نريد إلا كائنات قابلة للتجزئة أن تُضاف — أن تكون مفاتيح القاموس، لأننا نريد أن تعود إلينا القيمة نفسها عندما نشغّل دالة التجزئة عليها.
ملاحظة المترجم: المثال تمثيل تعليمي لتبدّل المفتاح؛ السلسلتان النصيتان
KateوCateفي Python مفتاحان مختلفان غير قابلين للتغيير، وليس تغيير الاسم البشري تعديلًا داخل كائن السلسلة الأصلي.
حسنًا. يمكننا الآن أن نرى أنه في أسوأ سيناريو، يُربط كل شيء بالحاوية نفسها في جدول تجزئتي، قائمتي. كل شيء أضيفه تُشغَّل عليه دالة تجزئة سيئة حقًّا. لنفترض أن دالة التجزئة تعيد 3 دائمًا. إذا كانت دالتي تعيد 3 دائمًا مهما كان ما أضيفه إلى قاموسي — مهما كان ما أجزّئه — فإن كل عنصر يوضع في الأساس في قائمة طويلة جدًّا عند الحاوية رقم 3.
عندما أبحث عن قيمة، حسنًا، مفاجأة، تُجزَّأ إلى 3. والآن أحتاج إلى النظر عبر كل شيء في تلك الحاوية رقم 3 لإيجاد ما أبحث عنه. هذا سيئ جدًّا جدًّا فحسب. وفي أسوأ سيناريو، هذا هو التعقيد. إنه Θ(n)، حيث n طول كل العناصر التي لدينا ونضيفها إلى حاوياتنا — إلى جدول تجزئتنا.
لكن في الحالة المتوسطة — وهذا فقط عندما يكون لدينا جدول تجزئة كبير إلى حد مناسب نسبة إلى الأشياء التي نضيفها إليه، وعندما تكون لدينا دالة تجزئة جيدة بما يكفي، ولها توزيع منتظم جيد من القيم — في تلك الحالة وحدها، في الحالة المتوسطة، يكون الزمن الذي يستغرقه حصولنا على قيمة من قاموس Θ(1)، ثابتًا.
لهذا فإن القواميس بنى بيانات مفيدة جدًّا جدًّا لتخزين الأشياء واسترجاع الأشياء منها. عندما كنت أعمل على مشروع صغير، لم أكن أعرف قواميس Python. كنت قد تعلمت اللغة للتو. وكنت أستخدم فعلًا قوائم لقراءة ملفات البيانات الجينومية (Genomic Data). وكنت أخزن كل شيء في قوائم — أسماء جينومية وأشياء كهذه.
وكان ذلك بطيئًا حقًّا. كان مشرفي يقول: هل انتهى كودك بعد؟ وأقول: لا، مضت يومان. ما زال ينتظر. ثم أخبرني أحدهم: مهلًا، استخدمي قاموسًا فقط لتخزين القيم. وعندئذ سيكون البحث أسرع كثيرًا. وانتهى خلال ثانيتين تقريبًا.
فهو مفيد جدًّا جدًّا، التعقيد الزمني — لأن البيانات الجينومية كميات هائلة من البيانات. لذا فإن Θ(n) مقابل Θ(1) هو حقًّا — يُحدث فرقًا كبيرًا جدًّا عندما تتعامل مع مجموعات بيانات كبيرة. ليس على الورق فقط. يُحدث فعلًا فرقًا كبيرًا. حسنًا. هل هناك أسئلة عن هذا؟ آمل أن يربط هذا — نعم؟
طالب (STUDENT):
إذًا تستخدم Python دالة تجزئة محددة؟
آنا بيل (ANA BELL):
نعم.
طالب (STUDENT):
إذا حُدّثت Python أو أيّ شيء، فهل يمكنهم تغيير تلك الدالة دون أن يتغير شيء؟ أم إنه —
صفحة المصدر 9
آنا بيل (ANA BELL):
نعم، تستخدم Python الآن دالة تجزئة محددة. في إصدار مستقبلي، قد يستخدمون دالة تجزئة مختلفة. لا نستخدم حقًّا الأعداد المرتبطة بدوال التجزئة. أعني، يمكنك استخدامها لبرامجك. لكنها ستكون، أظن، نسبية إلى أيّ قيمة تحصل عليها، أليس كذلك؟ فلا تثبّت برمجيًّا (Hard-Code) قيمة الصفّ 123 على أنها شيء معين، أليس كذلك؟ تحصل فقط على ما تحصل عليه. وهذا هو، أليس كذلك؟ لكنها قد تعطيك دالة تجزئة مختلفة. إذا شغّلته على حاسوبك، فعلًا، فقد تحصل على قيمة تجزئة مختلفة عني، نعم.
إذًا، هذا الموضوع يربط نوعًا ما بنى البيانات — رأينا القوائم والقواميس — مع قليل من النظر وراء الكواليس إلى كيفية تخزين الأشياء، ويضع قليلًا من التعقيد هناك بالحديث عن الخوارزميات وأزمنة التشغيل (Runtimes). فهو يربط مجموعة من الموضوعات التي رأيناها في هذا المقرر على نحو جميل جدًّا جدًّا.
شيء آخر أود الآن الحديث عنه هو فكرة المحاكاة. وآمل أن يكون هذا أكثر فائدة قليلًا لكم إذا قررتم دراسة مقرر آخر في علوم الحاسوب أو مقرر حوسبة في مجال مختلف، أيًّا كان ما تريدونه.
المحاكاة الحاسوبية (Computational Simulations) أدوات مفيدة جدًّا في علوم الحاسوب. تسمح لك إذًا بوصف العالم حاسوبيًّا. إذا رأيت حدثًا في العالم، فيمكنك فعلًا محاكاته حاسوبيًّا. بما تعلمتموه حتى الآن، يمكنكم تمامًا محاكاة مجموعة كاملة من الأشياء. ونستخدم العشوائية (Randomness) لمحاكاة هذه الأحداث التي قد ترونها في العالم الحقيقي.
مثلًا، ربما رأيتم مسارات الأعاصير، عندما ترون في الأخبار أو أيّ مكان المسار الأرجح الذي قد يسلكه إعصار. لكن لديهم أيضًا نماذج صغيرة تُظهر مسارات أخرى محتملة. لقد حاكوا، باستخدام مجموعة من البيانات لديهم، المسار الأرجح لذلك الإعصار.
موضع آخر تكون فيه المحاكاة مفيدة هو إذا رأيت حدثًا في العالم الحقيقي معقدًا نوعًا ما فعلًا. يمكنك أخذ مجموعة أبسط من القواعد ومحاكاتها، ثم إضافة مزيد من القواعد لجعلها أقرب فأقرب إلى الشيء الذي شاهدته فعلًا في العالم الحقيقي.
ففكرة المحاكاة هي أن لديك حدثًا ما في العالم الحقيقي. وتريد حساب شيء عنه. سنستخدم الحوسبة لتصميم تجربة (Experiment). وسنستخدم العشوائية لذلك. بعد أن نفعل ذلك، سنكرر التجربة مرات كثيرة جدًّا حاسوبيًّا. وهذا يعني فقط أننا سنضع حلقة for حول أيّ تجربة صممناها حاسوبيًّا.
إذا كنت مهتمًّا بنتيجة (Outcome) ما، نتيجة محددة، مثل — كما سنرى، سنرمي حجر نرد. ونهتم بعدد المرات التي يظهر فيها 4. فسنتتبع تلك النتيجة. وتتتبعها مهما كان عدد المرات التي حدثت فيها تلك النتيجة خلال تكراراتك الكثيرة. ثم بعد نهاية التكرارات، يمكنك الإبلاغ عن قيمة تهمك — ربما احتمال (Probability) أن يظهر 4 عند رمية نرد.
هذا هو المثال. سيكون بسيطًا جدًّا، لأنه شيء نستطيع حسابه أصلًا من البداية. لكنه سيعطيكم فكرة عن كيفية كتابة كود حول حدث كهذا من العالم الحقيقي. هنا نهتم فقط برمي حجر نرد ورؤية احتمال الحصول على نقطة، نقطة، نقطة، نقطة، أي الحصول على 4 على النرد، في إحدى رميات النرد، أو احتمال الحصول على نقطة، أيًّا كان.
صفحة المصدر 10
الحدث هنا إذًا هو أننا نرمي حجر نرد. ثم نهتم بالحصول على احتمال وجه ما. سنصمم تجربة لرمية النرد تلك. وهذه مجرد طريقة واحدة لتصميم التجربة. توجد طرق أخرى كثيرة جدًّا جدًّا لتصميمها. هذه مجرد طريقة اخترتها وشعرت بأنها توضّح أكثر كيف يمكننا أخذ مثال من العالم الحقيقي ووضعه في كود.
حجر النرد له ستة أوجه. ما فعلته هنا هو أنني أنشأت قائمة بكل واحد من تلك الأوجه. كان يمكنكم استخدام أعداد بوصفها عناصر في القائمة. في هذه الحالة، استخدمت سلاسل نصية فقط لتكون ألطف قليلًا، لكن لا يهم. أيًّا كانت الطريقة التي تودون بها تمثيل كل واحد من أوجه النرد تلك، ها هي قائمة فيها ستة أشياء.
ثم أستخدم دالة choice هذه من مكتبة random هذه. مجددًا، مكتبة random مكتبة مفيدة جدًّا جدًّا. ستختار random.choice عمليًّا أحد العناصر في هذه القائمة لي. فإذا كتبت random.choice في وحدة التحكم الآن، فقد تعطيني نقطة، نقطة. وإذا كتبتها بعد ذلك مباشرة، فقد تعطيني نقطة، نقطة، نقطة، نقطة، أيًّا كان. سيكون الأمر عشوائيًّا في كل مرة أشغّل فيها هذه الدالة.
لكن هذا السطر من الكود يحاكي عمليًّا أخذي حجر نرد ورميه. ثم يمكننا تكرار هذه التجربة مرات كثيرة جدًّا. إذا كنت آخذ حجر نرد وأرميه، فهذا مثل ماذا؟ ثانية أو ثانيتين لكل رمية؟ لا أظن أن لدي وقتًا لتكرار تلك التجربة 10,000 مرة. لكن بالمحاكاة والحوسبة والبرمجة، يمكننا محاكاتها 10,000 مرة أو مليون مرة، ثم الانتظار ثانيتين فقط. فهو تطبيق مفيد جدًّا جدًّا للبرمجة.
كيف تحاكي رمية النرد هذه 10,000 مرة؟ ضع حلقة for حول ذلك السطر من الكود فحسب. لأيّ عدد في range(10000)، يعني ذلك أنني سأشغّل هذا السطر من الكود 10,000 مرة. فجأة، رميت حجر نرد 10,000 مرة. وبينما أفعل ذلك، أهتم بنتيجة حدث ما. لنقل إذًا: كم مرة ظهرت نقطة، نقطة، نقطة، نقطة، أي 4؟
حسنًا، في كل مرة في حلقة for، يمكنني تتبع قيمة roll. إذا كانت 4، أزيد عدّادًا (Counter). وإذا لم تكن 4، فلا أهتم، ولا أفعل شيئًا. في كل مرة لدي إذًا عدّاد يخبرني بعدد المرات التي ظهر فيها 4. ثم بعد انتهاء حلقة for، أكون قد كررت تجربتي 10,000 مرة. ويمكنني الإبلاغ عن احتمال الحصول على 4 — أي العدّاد مقسومًا على 10,000.
هذا هو الكود. هذا كل شيء. إنه بسيط جدًّا. كتبت دالة. وتأخذ فعلًا معاملًا. إذا كنا مهتمين باحتمال ظهور نقطة، نقطة، نقطة، نقطة، فإننا نمرر قيمة ذلك المحدد — ذلك الوجه. وإذا كنت مهتمة باحتمال ظهور نقطة، فأمرر النقطة على هيئة سلسلة نصية.
ماذا يفعل إذًا؟ حسنًا، كما على الشريحة السابقة، لدي حلقة for هذه هنا، التي تخبر Python بعدد مرات تكرار التجربة. لدي عدد التجارب هنا على هيئة متغير. يمكنني بسهولة تغييره فقط ليصبح شيئًا آخر. ثم لدي roll هنا. هذا هو تنفيذي الفعلي للتجربة. هنا أرمي النرد فقط. وهذه قيمة roll. ثم أتحقق من قيمة الرمية وأزيد العدّاد إذا كانت الوجه الذي يهمني، الشيء الذي مررته معاملًا. ثم في النهاية أجري print فقط. لكن يمكنكم تصور إجراء return أو شيء كهذا.
إذا شغّلته، فسنحصل على أن احتمال ظهور الوجه نقطة كان 0.167، واحتمال ظهور نقطة، نقطة، نقطة، نقطة كان 0.1602. يقول الحدس إنهما ينبغي أن يكونا متساويين. لكن أتعرفون؟ هذا حدسنا. نحن نعرف المسألة بالفعل. لو لم نكن نعرف كيف نحسب احتمال ظهور أحد هذه الأوجه، فسيكون هذا جيدًا جدًّا.
صفحة المصدر 11
جمال الحوسبة هو أننا نستطيع إضافة صفرين آخرين هناك فحسب، وتشغيله، وربما إزالة التعليق عنه كي نرى القيم فعلًا. نشغّله، وننتظر ثانيتين. لكن الاحتمال الآن يقترب أكثر فأكثر من الاحتمال الحقيقي. فكلما أجريت تجارب أكثر، أصبحت إجابتي أفضل. ولم أضطر إلا إلى الانتظار ثانيتين. إذا زدته بعامل 10 آخر، فسأضطر إلى الانتظار 10 مرات أكثر، عشرة أمثال المدة، أي ربما 20 ثانية. لن أفعل ذلك. فما زال تخمينًا تقديريًّا. لكنه تخمين تقديري قريب جدًّا.
الجميل الآخر في كتابة الكود هو أننا نستطيع الآن أن نسأل: حسنًا، هذا نرد عادل (Fair Die)، أليس كذلك؟ يظهر كل واحد من هذه الأوجه باحتمال متساوٍ. ما التغيير الذي تظنون أن عليّ إجراءه لجعله نردًا غير عادل؟ لنقل إنه منحاز على نحو غير عادل نحو النقطة، 1. نعم؟
طالب (STUDENT):
يمكنك إضافة النقطة فقط [غير مسموع].
آنا بيل (ANA BELL):
نعم، بالضبط. دعوني أضف نقطة أخرى هنا فحسب. هنا لدي نقطة أخرى. والآن أصبح النرد منحازًا على نحو غير عادل. يظهر 1 مرات أكثر من أيّ شيء آخر. إذا شغّلت الكود مرة أخرى، وانتظرت ثانيتين، فاحتمال الحصول على 1 ضعف احتمال الحصول على 4. تغيير سهل حقًّا، ساعدني على الإجابة عن سؤال آخر، تعديل صغير لمسألتي الأصلية. ولم أضطر إلى رمي حجر نرد 10,000 مرة في العالم الحقيقي.
كانت تلك محاكاة سهلة حقًّا — احتمال حساب ظهور أوجه النرد بسيط إلى حد كبير. فلماذا تكلفنا الكود؟ لأننا نستطيع فعله رياضيًّا فحسب. والسؤال الجانبي الذي طرحته كان أيضًا بسيطًا نوعًا ما لحلّه.
لأننا نستطيع فعلًا طرح أسئلة أصعب وتعديلات أصعب لمسألتنا الأصلية، ويمكننا بالتأكيد إيجاد حلول رياضية لهذه المسائل الأصعب أيضًا. لكنني لن أكون واثقة من إجاباتي عنها بقدر ما سأكون بمجرد كتابة الكود. بالنسبة إليّ، سيكون تصحيح أخطاء الكود (Debugging) أسهل قليلًا من كتابة الاحتمالات رياضيًّا لأسئلة أصعب كثيرًا. ويمكنكم أن تروا أنه، بعد أن تكتبوا الكود، وبعد أن تصوغوا تجربتكم بهذه الطريقة، يصبح من السهل حقًّا المضي وتغييره قليلًا فحسب. فالكود سهل التغيير بعد كتابته.
فلننظر إلى سؤال جديد. يقول هذا إن التجربة الواحدة لم تعد مجرد رمي حجر نرد مرة واحدة. التجربة الواحدة الآن هي أننا نرمي حجر نرد سبع مرات. وأهتم بمعرفة احتمال الحصول على نقطة، نقطة، نقطة، نقطة ثلاث مرات على الأقل من أصل تلك الرميات السبع. سؤال أصعب كثيرًا من قبل. سيحتاج إلى قليل من التفكير وبعض الورق لحلّه.
لكن من حيث الكود، سيكون بسيطًا جدًّا. التجربة الواحدة الآن لم تعد اختيارًا واحدًا فقط من قائمتي لأوجه النرد. بل ستكون سبعة اختيارات من قائمتي لأوجه النرد، تمثل الرميات السبع التي لدي لتجربة واحدة. ومن أصل تلك الرميات السبع، ما أهتم بفعله هو تتبع زيادة عدّاد كلما رأيت أربع نقاط: نقطة، نقطة، نقطة، نقطة.
ثم، كما من قبل، ضع حلقة for حول ذلك فقط، وكرر تلك التجربة 10,000 أو مليون مرة، أيّ عدد تريده، ثم تتبع كم مرة ظهر ذلك 4 ثلاث مرات أو أكثر من أصل الرميات السبع. هذا هو حدثنا. عدّ كم مرة من أصل 10,000، في تلك الحالة — لكن يمكن أن يكون مليونًا أو أيّ عدد — زاد عدّادنا ليكون أكبر من 3، أكبر من أو يساوي 3. ثم القيمة التي تهمنا هي احتمال حدوث ذلك. فخذ ذلك العدّاد واقسمه على 10,000، لأن هذا عدد المرات التي كررنا فيها تجربتنا.
صفحة المصدر 12
هذا هو الكود. إنه أطول قليلًا. وقد قسمته فعلًا إلى جزأين — هذا هنا في الأعلى، ثم هذا هنا في الأسفل. الكود في الأعلى سيجري المحاكاة 10,000 مرة. لدي حلقة for واحدة هنا تمرّ عبر 10,000 أو مليون — أيّ عدد من المرات تريد تكرار التجربة فيه.
داخل هنا — عذرًا — وعذرًا، نسيت ذكر أن لدي هنا دالة عممت فيها مجموعة من الأشياء. يمكننا تشغيلها بقيم مختلفة. فبدل ثلاث مرات من أصل سبع رميات، يمكن أن يكون لدينا 15 مرة من أصل 3,000. صحيح، يمكننا تعميم هذا.
هنا، داخل حلقة for هذه، لدي محاكاة الرمي سبع مرات. هنا لدي نطاق n رميات. في الشريحة السابقة قلت إنها سبع. لكنها يمكن أن تكون أيّ شيء تريدونه. ثم لدي اختيار أحد الأوجه سبع مرات، وتتبع كم مرة من أصل تلك السبع حصلت على نقطة، نقطة، نقطة، نقطة.
إذًا، في نهاية حلقة for هذه هنا، أكون قد عددت كم مرة حصلت على نقطة، نقطة، نقطة، نقطة. ثم سأتتبع ذلك العدد في هذه القائمة، عدد ما طابق. ستكون قائمة عدد ما طابق من 10,000 عنصر، 10,000 عنصر، عنصر واحد لكل تجربة من تجاربي.
في المرة الأولى ربما ظهرت ثلاث حالات من نقطة، نقطة، نقطة، نقطة من أصل 7، ثم في المرة التالية واحدة، ثم في المرة التالية خمس، ثم في المرة التالية أربع، أيّ عدد. أصبح لدي الآن قائمة بعدد مرات ظهور نقطة، نقطة، نقطة، نقطة من أصل 7 رميات. الكود هنا في الأسفل — لهذا قسمته، لأن التفكير فيه أسهل قليلًا لي.
الكود هنا في الأسفل سيمرّ الآن عبر قائمة التجارب الـ10,000 هذه، ويقول أيّ واحدة منها أكبر من أو تساوي 3 — هذه، وهذه، وهذه. فأزيد عدّادًا كلما كان ذلك صحيحًا. وفي نهاية هذه الحلقة هناك في الأسفل، سأعرف كم مرة من أصل تلك المحاولات الـ10,000 ظهر لدي وجه نقطة، نقطة، نقطة، نقطة ثلاث مرات أو أكثر من أصل سبع.
يمكنني تشغيل الكود. وهنا لدي المسألة نفسها تمامًا من الشريحة السابقة. إذا كنت مهتمة باحتمال ظهور 4 ثلاث مرات على الأقل أو أكثر من أصل سبع رميات، فهو 0.0955. ثم كتبت ذلك أيضًا هنا في الأسفل بهذه الصورة. وهذا الاحتمال 0.16. ما هذه المسألة هنا في الأسفل؟ هل تبدو مألوفة؟
احتمال ظهور نقطة، نقطة، نقطة، نقطة مرة واحدة على الأقل من أصل رمية واحدة. هذه فقط المسألة السابقة على الشريحة السابقة. لدي رمية واحدة فقط. وأحسب احتمال الحصول على 4. فهذا يطابق ما حصلت عليه بالدالة السابقة التي كتبتها. لكن مهلًا، كتبت الآن فعلًا دالة أفضل، أكثر عمومية. ويمكنني تشغيلها أيضًا للحصول على الاحتمال من الكود السابق. فهذا في الواقع كود أفضل كثيرًا لتشغيله.
حسنًا. هل هناك أسئلة عن هذا؟ مثير للاهتمام؟ أعني، إنها رميات نرد. ما مقدار الإثارة الذي يمكن أن يكون فيها؟ لكن نعم، فلننظر إلى مسألة أكثر إثارة، شيء قد تطبقونه على العالم الحقيقي.
قد ترون هذا في مقرر حساب التفاضل والتكامل (Calculus)، وقد لا ترونه. لكنه أقرب إلى مسألة تفاضل وتكامل. لدي ماء يتدفق عبر صنبور عشوائيًّا بمعدل بين غالون واحد في الدقيقة وثلاثة غالونات في الدقيقة. هذه هي المعطيات. ما الزمن اللازم لملء حوض سباحة سعته 600 غالون؟ هل لدى أحد حدس عن كيفية حل هذا؟ إذا لم يكن، فيمكنني النقر على التالي فحسب.
طالب (STUDENT):
[غير مسموع]
آنا بيل (ANA BELL):
نعم، بالتأكيد بين أدنى معدل وأعلى معدل، أي بين 200 غالون في الدقيقة — عذرًا، بين 200 دقيقة و600 دقيقة، 200 في أفضل حالة إذا تدفق الماء بثلاثة غالونات في الدقيقة، و600 دقيقة في أسوأ حالة إذا تدفق الماء ببطء شديد، غالون واحد في الدقيقة.
صفحة المصدر 13
يمكننا أن نقول: حسنًا، فلنأخذ متوسط التدفق. المتوسط بين 1 و3 غالونات هو 2. إذا أخذنا 600 غالون مقسومة على غالونين في الدقيقة، فسيعطينا ذلك 300 دقيقة. تخمين معقول. لكنه ليس فعلًا الإجابة الصحيحة.
طريقة أخرى يمكن أن نقول بها: حسنًا، فلنأخذ أبطأ وأسرع ما يمكن أن يعمل به، ما يمكن أن يستغرقه. هنا لدي 600 دقيقة و200 دقيقة، وآخذ متوسط هذين العددين، وأقسم على 2. هذا إذًا 400 دقيقة. لكنه ليس صحيحًا أيضًا في الواقع. نعم؟
طالب (STUDENT):
هل يمكنك أخذ تكامل 600 على [غير مسموع] والحصول على القيمة المتوسطة؟
آنا بيل (ANA BELL):
يمكنك ذلك، نعم. لكنني لا أريد إجراء التكاملات (Integrals). نعم، لكن هذه بالضبط الإجابة الصحيحة. نعم، عليك إجراء تكامل. نعم، نحن ندرّس علوم الحاسوب هنا. لذا لن نجري تكاملات في هذا المقرر. بدلًا من ذلك، سنكتب كودًا. وسيكون الكود نحو خمسة أسطر لإيجاد الإجابة عن هذا.
كل ما سنفعله هو أخذ مجموعة كبيرة من الأعداد بين 1 و3، مليون منها إذا أردتم. ستمثل هذه مجموعة من القيم العشوائية المحتملة لمعدل تدفق الماء (Flow Rate). ثم سنقول: لكل واحد من هذه الأعداد المختارة عشوائيًّا، كم سيستغرق ملء الحوض؟
فتجري 600 مقسومة على ذلك المعدل، مجرد الزمن اللازم. ثم سنأخذ متوسط كل هذه المعدلات. لدينا مليون من هذه الأعداد، أزمنة محتملة قد يستغرقها ملء الحوض، نجمعها كلها، ونأخذ متوسطها. هذا هو الكود. يبدو كثيرًا. لكن هنا في الأسفل، النصف السفلي من هذا هو مجرد إبلاغنا عن النتائج. هنا عبارتا print. وهنا أرسم أيضًا فعلًا شكل النقاط، كل معدلات التدفق.
الكود الفعلي لإجراء المحاكاة هو هذه — حسنًا، كذبت، سبعة أسطر من الكود، لا خمسة. لدي دالة — fill_pool. يمكنها أخذ معامل للسعة (Size). ويمكننا حتى تحديد حد أدنى وحد أعلى، إذا أردنا، لمعدل التدفق. في الوقت الحالي، سنثبته برمجيًّا فقط ليكون بين 1 و3.
لدي قائمتان سأملؤهما بمجموعة من الأعداد العشوائية المختلفة. سيُختار معدل التدفق بين 1 و3. هنا لدي random.random، دالة عشوائية مفيدة أخرى من مكتبة random، تعطيني عددًا بين 0 و1. للحصول على عدد بين 1 و3 عشوائيًّا، سأضرب في 2 وأضيف 1 فقط. في الحالة الدنيا، سيكون 1. وفي الحالة العليا، سيكون 2 * 1 + 1، أي 3.
إذًا، r، كل ما نحتاج إلى معرفته، ستكون عددًا عشوائيًّا بين 1 و3، عددًا عشريًّا. يمكن أن تكون أيّ شيء. ألحق ذلك العدد العشوائي بقائمتنا لمعدلات التدفق. ثم باستخدام معدل التدفق الذي اخترناه للتو عشوائيًّا، اعرف الزمن اللازم لملء الحوض، الحوض ذي السعة size، أي size مقسومة على المعدل. رياضيات بسيطة جدًّا فحسب.
ثم أصبحت لدينا قائمة مملوءة بكل هذه الأزمنة اللازمة لملء الحوض. وما داخل حلقة for هنا هو تجربة واحدة. أخذت عددًا عشوائيًّا واحدًا. وعرفت الزمن اللازم لي لملء حوضي. ثم كررت ذلك 10,000 مرة.
هنا في الأسفل، سأبلغ عن متوسط معدل التدفق، الذي ينبغي أن يكون 2، لأننا إذا اخترنا عددًا عشوائيًّا بين 1 و3، فمن المفترض أن يكون متوسط تلك الأعداد 2. ثم أبلغ عن الشيء الذي نهتم به فعلًا، وهو متوسط زمن الملء (Average Fill Time)، الذي لم يكن أيًّا من هذين الشيئين اللذين حدسناهما. لكنه التكامل.
ملاحظة المترجم: النموذج هنا يختار معدلًا عشوائيًّا ثابتًا لكل تجربة ملء، ثم يحسب الزمن
600/r، ولا يحاكي معدلًا يتغير كل دقيقة أثناء الملء. متوسط الأزمنة في هذا النموذج ليس600مقسومًا على متوسط المعدلات. وrandom.random()يعيد قيمة في المجال0 <= r < 1، لذا فالحد 3 بعد التحويل حد أعلى غير مشمول.
صفحة المصدر 14
وهنا في الأسفل أجري بعض الرسوم. هذه هي الأشياء التي رسمتها. على الجانب الأيسر لدي، على محور x — أعتذر، نسيت تسمية محوريّ ووضع عنوان لهذا. سأتحدث عنه فقط. محور x هو الأعداد من 0 حتى 10,000، تمثل كل واحدة في الأساس — 0، 1، 2، 3، 4 تمثل واحدة من تجاربي، اختيار عدد عشوائي. ومحور y هو العدد العشوائي الذي اختير.
يبدو هذا انتشارًا جميلًا من العشوائية هنا، وهو ما أردناه. ثم لكل واحدة من هذه القيم، سيكون لدي معدل ملء مقابل. مثلًا هنا، إذا صادف أن معدل الملء عند النقطة 0 هو 1، فهذا يعني أن الزمن الذي استغرقته لملء الحوض ينبغي أن يكون هناك في الأعلى عند نحو 600. ربما تكون تلك النقطة الصغيرة هناك تمامًا.
هذا رسم لأعداد عشوائية بين 1 و3، اختير 10,000 منها. وهذا رسم الأزمنة المقابلة التي استغرقتها لملء الحوض بكل واحدة من هذه النقاط التي اخترناها عشوائيًّا. نلاحظ أن الرسم على اليمين ليس منتشرًا بانتظام. في الواقع، كثافة النقاط فيه أعلى نحو الأسفل. إذًا، تخمينانا كانا أن معدل الملء إما 300 أو 400 في المتوسط. ولم يكن أيّ منهما صحيحًا.
فلننظر إلى هذه الرسوم بطريقة مختلفة قليلًا. سأقوم فعلًا بـ… القيم. الآن، كان مجرد عدد عشوائي حصلنا عليه بين 1 و3. لكن يمكنني ترتيبها (Sort). لا يهم الترتيب الذي حصلت به على هذه الأعداد العشوائية. يمكنني ترتيبها. وإذا رتبتها، أحصل على شيء يبدو هكذا.
ملاحظة المترجم: الفعل مفقود في جملة «سأقوم فعلًا بـ… القيم» في التفريغ نفسه؛ الجمل التالية تنص على ترتيبها. كذلك «معدل الملء إما 300 أو 400» في الفقرة السابقة زلة لفظية، إذ التخمينان لزمن الملء بالدقائق.
مجددًا، لدي أعداد اختيرت عشوائيًّا، 10,000 منها. وباحتمال متساوٍ — لهذا نرى هذا الخط الجميل — اخترت عددًا بين 1 و3. هل هذا مفهوم؟ حسنًا. ثم الزمن المقابل الذي استغرقته لملء حوضي لكل واحد من هذه الأعداد هو عدد بين 200 و600، كما خمنّا. الآن، متوسط الزمن الذي يستغرقه — معدل الملء هو 2، وهذا صحيح. ليس مفاجأة لنا لأنه عدد عشوائي بين 1 و3. لكن المتوسط الفعلي للزمن اللازم لملء حوضي هنا في الأسفل. إذا أخذت متوسط كل واحدة من هذه القيم، فهو هنا في الأسفل عند نحو 330. ليس 300. وليس 400. إنه بالتأكيد بينهما، لكنه ليس قريبًا حقًّا من أحدهما أو الآخر. ذلك لأن لدي نقاطًا أكثر، موزعة بكثافة أكبر نحو الأسفل من الأعلى.
القيمة الفعلية التي حصلت عليها لـ10,000 عدد مختلف اختير عشوائيًّا هي 331. أظن أن القيمة الفعلية، إذا أجرينا التكامل، هي نحو 329 وشيء بعد الفاصلة. فنحن قريبون جدًّا. لكن مجددًا، لم نجرِ إلا 10,000. يمكننا إجراء مليون. وسنقترب كثيرًا من القيمة الفعلية.
إذًا ليست 300 أو 400. وذلك لأنه، كما ذكر أحد زملائكم الطلاب، توجد علاقة عكسية (Inverse Relationship) بين زمن الملء ومعدل الحوض. فهي سعة الحوض مقسومة على المعدل. ما نحتاج إلى فعله فعلًا، إذا أردنا معرفة القيمة، هو حل التكامل بين 1 و3 لـdx/x، أو أيًّا كان ما سيكون عليه.
ملاحظة المترجم: المتوسط الدقيق في النموذج ذي المعدل الموزع بانتظام هو
(1/(3-1)) * integral[1,3](600/r) dr = 300 * ln(3) ≈ 329.584دقيقة. هذا توضيح رياضي إضافي، لا استبدال لعبارة المتحدثة عن التكامل.
لا أريد أن أتعب نفسي بذلك. لكنني سأكتب سبعة أسطر من الكود، ثم أنتظر خمس ثوانٍ فقط ليتكرر ذلك الكود خمسة ملايين مرة، أو مليون مرة، أو عشرة ملايين مرة. هل هذا رائع؟ وهذا تمامًا في متناولكم. ليس كودًا صعبًا. مجرد حلقات for. إنه استخدام مكتبة عشوائية لأخذ مجموعة كاملة من الأعداد عشوائيًّا فقط، ثم معرفة المسألة المطروحة — ملء حوض بمعدل معين، رياضيات بسيطة. ثم تحصلون على إجابة جميلة، تقريب (Approximation) جميل، لكنه يظل إجابة عن السؤال.
صفحة المصدر 15
آمل إذًا أن يريكم هذا مدى قوة الحوسبة. هذا مجرد مثال آخر لكيفية تطبيق الحوسبة والبرمجة على مسألة ترونها في الحياة الواقعية. إذا اخترتم المشاركة في Europe أو دراسة مقررات مستقبلية في مجال مختلف، ربما ليس في علوم الحاسوب، فستطبقون على الأرجح الحوسبة على مفاهيم وأفكار ومسائل في تلك المجالات. وسيكون شيء مشابه لهذا. تلاحظون شيئًا. تحاولون التفكير فيه حاسوبيًّا، وتمثّلونه بنموذج فيه مجموعة من العشوائية، ثم تحسبون شيئًا يهمكم.
ملاحظة المترجم: وردت
Europeفي التفريغ؛ المقصود من سياق المقرر هو UROP، برنامج فرص البحث لطلاب المرحلة الجامعية (Undergraduate Research Opportunities Program)، لا أوروبا.
حسنًا. هذه نهاية 6.100L. لدي ختام صغير لتذكير أنفسنا أين كنا وإلى أين سنذهب. ماذا تعلمنا؟ آه، أيضًا، ستكون هذه الشرائح مثل تفريغ للميمات (Memes)، لكل ميم متبقٍّ لدي. لذا ستكون كثيرة الميمات جدًّا.
ماذا تعلمنا إذًا؟ لدينا، طبعًا، تعلمنا Python، البرمجة. تعلمنا كثيرًا من صياغة Python (Syntax). بعض المحاضرات كان أثقل في صياغة Python من غيره. لكن آمل أنكم تمكنتم من ذلك. تعلمنا، طبعًا، تدفق التحكم (Flow of Control) مع التفرعات (Branches)، وعبارات if، [غير مسموع]. تسمح لنا عبارات else بفعل شيء أو شيء آخر في الكود، وإنشاء نقطة قرار.
الحلقات — حلقات for وحلقات while، وكذلك الاستثناءات (Exceptions) بوصفها طريقة للتعامل مع الأشياء غير المتوقعة التي تظهر خلال المقرر. الأفكار هنا، تدفق التحكم، قابلة فعلًا للنقل إلى أيّ لغة برمجة أخرى. إذا كنتم تعرفون الأفكار، وإذا كان لديكم حدس عن نوع تدفق التحكم الذي تودون استخدامه، فكل ما عليكم فعله هو تغيير الصياغة. ثم فجأة، تستطيعون كتابة بعض الكود بلغة أخرى.
طبعًا، تعلمنا بنى البيانات. تناولنا القوائم، بنى بيانات مفيدة حقًّا؛ والقواميس، بنى بيانات مفيدة جدًّا؛ والصفوف، وأشياء كهذه. يمكنكم تعلم بنى بيانات أكثر تقدمًا في مقرر مستقبلي إذا أردتم. لكن هذه هي فعلًا أهم بنيتين أو ثلاث من حيث الفائدة.
تحدثنا كثيرًا — في الواقع، كان تعليقًا — لم نتحدث كثيرًا على نحو محدد. لكنه كان موضوعًا متكررًا في هذا المقرر: تنظيم الكود (Organization of Code). فأفكار التفكيك (Decomposition) والتجريد (Abstraction) هذه ظهرت كثيرًا عندما تحدثنا عن الدوال. كان ذلك أول تعارف لنا معها.
ساعدتنا الدوال على أخذ جزء وظيفي ما من الكود، كود يفعل شيئًا، وتجريده إلى وحدة صغيرة جميلة (Module)، وتفكيكه إلى وحدة صغيرة واحدة. تحتاج إلى كتابته مرة، وربما كتابته بضع مرات، لكن تصحيح أخطائه بضع مرات، ثم استخدامه مرات كثيرة جدًّا دون القلق من أنه سيتغير. فهي طريقة جميلة جدًّا لتفكيك أجزاء الكود الوظيفية.
ثم رأيناها تظهر مجددًا عندما تناولنا الأصناف (Classes)، البرمجة كائنية التوجه (Object-Oriented Programming). استطعنا جمع السلوكيات والبيانات معًا في كائن صغير جميل واحد، يمكننا بعد ذلك إنشاء كثير منه في أجزاء مختلفة كثيرة من الكود، ثم التعامل مع كل واحد على حدة.
موضوع متكرر آخر عبر هذا المقرر كان الخوارزميات (Algorithms). تحدثنا عن خوارزمية البحث بالتنصيف (Bisection Search) في بداية المحاضرات تمامًا. وتحدثنا عنها في مجموعة المسائل الأولى. وظهرت مجددًا قرب النهاية عندما تحدثنا عن التعقيد وخوارزميات البحث والترتيب وأشياء كهذه.
كانت تلك نوعًا ما الخوارزمية الكبيرة الوحيدة التي رأيتموها في هذا المقرر. لكنها تريكم كيف يمكنكم تنفيذ كود بطريقة مختلفة تمامًا ليتصرف بطريقة مختلفة تمامًا — ليكون أسرع كثيرًا مع بعض الشروط، مثل أن تكون البيانات مرتّبة وأشياء كهذه.
صفحة المصدر 16
وأخيرًا، كان الجزء الأخير من المقرر أثقل قليلًا من الناحية النظرية. تحدثنا عن التعقيد الحسابي (Computational Complexity) وترميز ثيتا الكبيرة (Big Theta Notation). أعطاكم ذلك فكرة عن كيفية تصميم الخوارزميات ربما. فإذا كان لديكم تصور أولي لكود شبه برمجي (Pseudocode) على ورقة، فيمكنكم أن تروا: حسنًا، إذا احتجت إلى تشغيل هذا الكود على مجموعة بيانات كبيرة حقًّا، فلن ينجح لأنه بطيء أكثر من اللازم.
لديكم ثلاث حلقات متداخلة أو شيء كهذا. لذا قد يجبركم ذلك على إعادة التفكير في تصميم الخوارزمية قبل أن تكونوا قد نفّذتموها بالفعل. لكن إذا كنتم تتعاملون مع مجموعات بيانات صغيرة، فربما لا تهتمون بأن لديكم ثلاث حلقات for متداخلة، أو خوارزمية عودية غير كفؤة حقًّا.
هذه إذًا هي الأشياء الكبيرة التي تعلمناها في هذا المقرر. أصنف تجربتكم بثلاث طرق مختلفة. ربما كانت لديكم موهبة طبيعية. إذا انضممتم إلى هذا المقرر وفهمتم المنطق فورًا، وعرفتم فورًا كيف تحلون مجموعات المسائل، فلا بأس بذلك تمامًا. ما زلت آمل أنكم استفدتم شيئًا من هذا المقرر وتعلمتم شيئًا.
ربما انضممتم إلى المقرر متأخرين. إذا وجدتم 6.100A سريعًا أكثر من اللازم أو صعبًا أكثر من اللازم، فربما انضممتم إليه، انضممتم إليه متأخرين، وتركتموه جانبًا وقلتم: دعوني أنضم إلى 100L. رحبت بكم. أجرينا قليلًا من البحث، ووجدنا أنه حتى إذا انضممتم متأخرين، فإن ذلك لا يعيق أداءكم فعلًا في المقرر. فآمل أن هذه كانت تجربتكم.
هل عملتم بجد؟ ربما لم تفهموا كل المفاهيم فورًا. وربما واجهتم قليلًا من الصعوبة في مجموعات المسائل. وربما واجهتم قليلًا من الصعوبة في مجموعة المسائل أو في الاختبارات القصيرة. لكنني ما زلت أعتقد أنكم تعلمتم كثيرًا. والاختبار دائمًا هو العودة والنظر إلى مجموعة المسائل الأولى.
إذا فعلتم ذلك عندما تعودون إلى المنزل الليلة، ونظرتم مجددًا إلى مجموعة المسائل الأولى في هذا المقرر، ونظرتم مجددًا إلى الكود الذي كتبتموه، فسيبدو سهلًا جدًّا. أعدكم بذلك. وهذا لأنني أعتقد أنكم جميعًا أديتم عملًا جيدًا جدًّا. بذلتم أقصى جهدكم في هذا المقرر. أعرف أنه ليس سهلًا. وأعرف أن وتيرته أبطأ. لكنه ما زال ليس مقررًا سهلًا. وأعتقد أنكم تعلمتم كثيرًا. النظر مجددًا إلى مجموعة المسائل الأولى سيريكم ذلك بالتأكيد.
فما التالي؟ وردت بعض الأسئلة عن بعض المقررات المستقبلية التي قد ترغبون في دراستها، أو ما يمكنكم فعله بعد أن تنتهوا هنا. ها نحن ذا. لدينا 6.100B. إنه الخطوة التالية الأكثر طبيعية. مقرر مدته نصف فصل دراسي، في النصف الثاني من الفصل. فهم ينهونه الآن في الأساس.
إنه نظرة عامة على موضوعات مثيرة للاهتمام حقًّا في علوم الحاسوب، لكن مع تركيز على علم البيانات (Data Science). وأنا أدير ذلك المقرر أيضًا في الواقع. ما نتحدث عنه هناك هو خوارزميات التحسين (Optimization Algorithms). مثلًا، لنفترض أنك تريد إنشاء جدول لمقرراتك الفصل القادم. وستكون لديك بعض القيود (Constraints). لا تريد محاضرات عند الثامنة صباحًا، أو التاسعة صباحًا، أو العاشرة صباحًا، أو الحادية عشرة صباحًا. وتريد أن يكون كل ذلك ضمن حد زمني ما أو أشياء كهذه. قد تكون خوارزمية التحسين شيئًا تكتبه. ويمكنك تطبيقها فقط على شيء لديك.
المحاكاة، بالضبط ما رأيناه اليوم في مسألة الفيزياء الخاصة بملء الحوض — سترون مزيدًا من أمثلة ذلك، وتطرحون أسئلة مختلفة عنه أيضًا. سترون أشياء مثل حساب أشياء مثل الانحرافات المعيارية (Standard Deviations). كم مرة نحتاج إلى تكرار هذه التجربة كي نكون ضمن فترة ثقة (Confidence Interval) ما؟ ما مدى ثقتكم بإجاباتكم إذًا؟ سنفعل مزيدًا من الأشياء من هذا النوع.
ثم هناك، طبعًا، جانب تعلّم الآلة (Machine Learning). إذا كانت لديكم مجموعة تجارب تجرونها، وتحصلون منها على مجموعة كاملة من البيانات، فكيف يمكنكم ملاءمة منحنى (Fit a Curve) لتلك التجارب؟ ثم لتجربة مستقبلية، ما القيمة المتوقعة (Expected Value)؟ هذا قليل من تعلّم الآلة على البيانات التجريبية.
صفحة المصدر 17
ثم مزيد من تعلّم الآلة بمعنى أكثر تقليدية هو التعامل مع خوارزميات التجميع (Clustering Algorithms)، وتصنيف البيانات (Classification)، وأشياء كهذه. أعرف كثيرًا عن 6.100B لأنني أدرّسه أيضًا. إنه مقرر تالٍ جيد حقًّا، إذا أردتم أن تكونوا مؤهلين للحصول على تدريب عملي (Internship). إذا درستم هذا، فأعتقد أنكم ستكونون جاهزين.
6.101 أيضًا مقرر جميل حقًّا لدراسته بعد ذلك. إذا استمتعتم حقًّا بالبرمجة في هذا المقرر، فسيكون 6.101 خطوتكم التالية. اسمه أساسيات البرمجة (Fundamentals of Programming). وهو بلغة Python. ووتيرته سريعة إلى حد كبير. ستكون هناك مجموعات مسائل كل أسبوع. وستكون صعبة جدًّا.
ستتضمن مجموعات المسائل تلك كثيرًا من تصحيح الأخطاء. وكنت في الواقع مدرّسة جلسات مراجعة (Recitation Instructor) لذلك المقرر. وللحصول على أول — في مجموعات المسائل على الأقل، لا يستغرق الحصول على شيء يعمل زمنًا طويلًا. لكن جعله يعمل وفق المواصفات (Specifications) التي لديهم سيحتاج إلى قليل من تصحيح الأخطاء وإعادة العمل.
ذلك لأنهم يتعاملون مع كثير من البيانات الواقعية. لذا فإن كتابة كود كفؤ حقًّا أمر مهم جدًّا. مجددًا، كتابة حلقات for متداخلة، طبعًا، يمكننا فعل ذلك تمامًا. لكن جعله كفؤًا باستخدام بنى بيانات مثل المجموعات (Sets) لجعل الكود كفؤًا، واستخدام خوارزميات مناسبة كفؤة، جزء مهم جدًّا من هذا المقرر. لكنكم ستتعلمون كثيرًا إذا درستم هذا المقرر. وستكونون مؤهلين جدًّا للحصول على تدريب عملي في شركة ما تعمل في علوم الحاسوب.
6.102 أيضًا مقرر تالٍ جميل، إذا كنتم مهتمين ببناء البرمجيات (Software Construction). وهو في الواقع بلغة مختلفة. بلغة TypeScript هذه الأيام. كان بلغة Java. يمكنكم أخذ ما تعلمتموه هنا. وإذا كنتم مهتمين بتعلم لغة مختلفة، فهذه لغة جميلة لتجربتها.
شعارهم هو أنكم تكتبون كودًا آمنًا من الأخطاء، وسهل الفهم، وجاهزًا للتغيير. لديهم أيضًا مجموعات مسائل كثيرة. لكنكم تعملون أيضًا في فريق. فتتعلمون كيفية العمل جيدًا في فريق مع طلاب آخرين، وكيفية البرمجة معًا، وكيفية كتابة كود ذي توثيق (Documentation) جيد، وكثيرًا من تصحيح الأخطاء، وأشياء كهذه. لذا فإن مزيدًا من أفكار التفكيك والتجريد تلك التي تعلمناها في هذا المقرر سيكون بالتأكيد بارزًا في هذا المقرر، في 6.102.
ثم، طبعًا، لدينا مقررات أخرى يسعدني الحديث عنها. فتعلّم الآلة خيار جميل، مجددًا، إذا كنتم متمكنين من البرمجة جيدًا جدًّا، وتريدون تجربة تطبيق البرمجة على تعلّم الآلة فحسب. ومقرر في الخوارزميات خطوة تالية جيدة أيضًا، إذا كنتم مهتمين أكثر بجزء التعقيد الذي رأيناه في هذا المقرر؛ هذه أيضًا أشياء معقولة جدًّا جدًّا لتجربتها بعد هذا المقرر.
نعم، الشريحة الأخيرة. إذا لم تكونوا ستبرمجون لفترة، لكنكم تظنون أنكم قد تبرمجون بعد فصلين دراسيين أو شيء كهذا — تريدون أخذ مقرر أكثر حوسبة في مجال ترغبون فيه — فسأقول إن عليكم محاولة ممارسة البرمجة مرة واحدة على الأقل أسبوعيًّا.
في موقعنا، لدينا قائمة مساعدة صغيرة يمكنكم الذهاب إليها — أدرجنا بعض المواقع الأخرى. هناك قليل من التدريب على البرمجة يمكنكم فعله. لا يحتاج إلى أن يكون كثيرًا — 30 دقيقة مرة في الأسبوع، شيء كهذا، فقط كي لا تنسوا البرمجة، يمكن أن يُحدث أثرًا كبيرًا جدًّا، لأنني أعرف أنني، خلال الصيف، أحيانًا لا أبرمج شهرًا أو نحو ذلك، لأنني أفعل أشياء أخرى غير البرمجة في حياتي.
والعودة إليها تستغرق قليلًا من الوقت. ومن دون ممارسة، مثل أيّ شيء آخر، يسهل النسيان. وتصعب العودة إليها. لذا حتى لو مارستم قليلًا من البرمجة فقط، وكتبتم برنامجًا بسيطًا حقًّا مرة في الأسبوع، فسيكون لذلك أثر كبير.
صفحة المصدر 18
هذا كل شيء إذًا. أريد أن أشكركم جميعًا على مشاركتكم في هذا المقرر. وشكرًا على حضور هذه المحاضرة الأخيرة. أعرف أنكم لم تكونوا مضطرين إلى ذلك. لكنني أقدّر ذلك فعلًا. برمجة سعيدة. وبالتوفيق في الامتحانات. وإجازة طيبة للجميع. شكرًا.
[تصفيق]