-
المساهمات
1406 -
تاريخ الانضمام
-
تاريخ آخر زيارة
-
عدد الأيام التي تصدر بها
63
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو عبد اللطيف ايمش
-
كانت لحظة مرعبة حينما جلست على حاسوبي بعد أن «عضّ القرش» كبل الإنترنت وانقطع، وأدركت حينها كم أقضي وقتًا على الإنترنت حينما أستعمل حاسوبي؛ فلدي عادة أن أتحقق من بريدي يدويًا (مع أن التنبيهات تصلني أولًا بأول!) وأفتح تويتر (إكس، سمهِّ ما شئت) وأنظر ما آخر المستجدات. كثيرٌ من عملنا على الحاسوب يتطلب وصولًا إلى الانترنت، ومصطلح «استخراج البيانات من الويب» Web scraping يستعمل مع البرامج التي تنزل المحتوى من الإنترنت وتعالجه. فمثلًا لدى غوغل عدد من البوتات لتنزيل محتوى صفحات الويب وفهرستها وأرشفتها لتستعملها في محرك البحث. سنتعلم في هذا المقال عن عددٍ من الوحدات في بايثون التي تسهل علينا استخراج البيانات من الويب: webbrowser: حزمة تأتي مع بايثون وتفتح متصفحًا على صفحة ويب معينة. requests: تنزل الملفات وصفحات الويب من الإنترنت. bs4: تفسّر شيفرات HTML التي تكتب فيها صفحات الويب. selenium: تشغل وتتحمل في متصفح ويب، والوحدة selenium قادرة على ملء الاستمارات ومحاكاة ضغطات الفأرة في المتصفح. مشروع: برنامج mapIt.py مع وحدة webbrowser الدالة open() في الوحدة webbrowser تفتح صفحة ويب معينة في نافذة متصفح جديدة. جرب ما يلي في الصدفة التفاعلية: >>> import webbrowser >>> webbrowser.open('https://academy.hsoub.com/') ستجد أكاديمية حسوب مفتوحةً في لسانٍ جديد في المتصفح. هذا كل ما تستطيع الوحدة webbrowser فعله، لكن مع ذلك يمكننا أن نجري بعض الأمور اللطيفة مع الدالة open()، فمثلًا قد تكون مهمة فتح خرائط جوجل والبحث عن عنوان معين أمرًا مملًا، ونستطيع التخلص من بضع خطوات لو كتبنا سكربتًا يفتح خرائط جوجل ويوجهها إلى عنوان الشارع المنسوخ في الحافظة لديك، وبالتالي سيكون عليك أن تنسخ العنوان إلى الحافظة وتشغل السكربت، وستفتح الخريطة لديك. هذا ما يفعله البرنامج: يحصل على عنوان الشارع من وسائط سطر الأوامر أو من الحافظة يفتح نافذة متصفح ويوجهها إلى صفحة خرائط جوجل المرتبطة بعنوان الشارع. هذا يعني أن على الشيفرة البرمجية أن تفعل ما يلي: تقرأ وسائط سطر الأوامر تقرأ محتويات الحافظة تستدعي الدالة webbrowser.open() لتفتح صفحة الويب. احفظ ملفًا جديدًا باسم mapIt.py، ولنبدأ برمجته. الخطوة 1: معرفة الرابط الصحيح اعتمادًا على التعليمات الموجودة في الملحق ب، اضبط برنامج mapIt.py ليعمل من سطر الأوامر كما في المثال الآتي: C:\> mapit 870 Valencia St, San Francisco, CA 94110 سيستخدم البرنامج وسائط سطر الأوامر بدلًا من الحافظة، وإذا لم نمرر إليه أيّة وسائط فحينها سيقرأ محتويات الحافظة. علينا بدايةً أن نحصِّل ما هو عنوان URL الذي يجب فتحه للعثور على شارع معين. إذا فتحت خرائط جوجل في المتصفح وبحثت عن عنوان فسيكون الرابط في الشريط العلوي يشبه: https://www.google.com/maps/place/870+Valencia+St/@37.7590311,-122.4215096,17z/data=!3m1!4b1!4m2!3m1!1s0x808f7e3dadc07a37:0xc86b0b2bb93b73d8 نعم العنوان في الرابط، لكن هنالك نص كثير إضافي غيره. تضيف المواقع عادةً بيانات إضافية لتتبع الزوار أو تخصيص المواقع. لكن إن حاولت الذهاب إلى الرابط التالي: https://www.google.com/maps/place/870+Valencia+St+San+Francisco+CA/ فسترى العنوان مفتوحًا أمامك. وبهذا كل ما نحتاج إليه هو فتح صفحة ويب ذات العنوان التالي: https://www.google.com/maps/place/your_address_string حيث your_address_string هو العنوان الذي تريد عرضه في الخريطة. الخطوة 2: التعامل مع وسائط سطر الأوامر يجب أن تبدو الشيفرة لديك كما يلي: #! python3 # mapIt.py - تشغيل خريطة في المتصفح باستخدام عنوان # من سطر الأوامر أو الحافظة. import webbrowser, sys if len(sys.argv) > 1: # Get address from command line. address = ' '.join(sys.argv[1:]) # TODO: الحصول على العنوان من الحافظة. بعد سطر !# سنستورد الوحدة webbrowser لتشغيل المتصفح والوحدة sys لمحاولة قراءة وسائط سطر الأوامر. يخزن المتغير sys.argv اسم الملف ووسائط سطر الأوامر، وإذا احتوت هذه القائمة على أكثر من عنصر واحد (الذي هو اسم الملف) فيجب أن تكون قيمة استدعاء len(sys.argv) أكبر من 1، وهذا يعني أن هنالك وسائط في سطر الأوامر. عادةً ما يُفصَل بين وسائط سطر الأوامر بفراغات، لكن في هذه الحالة نريد أن نفسر جميع الوسائط على أنها سلسلة نصية واحدة، ولما كانت قيمة sys.argv هي قائمة تحتوي على سلاسل نصية، فيمكننا استخدام التابع join() معها، مما يعيد سلسلة نصية واحدة؛ لكن انتبه أننا لا نريد اسم الملف ضمن تلك السلسلة النصية، فعلينا استخدام sys.arv[1:] لإزالة أول عنصر في القائمة، ثم سنتخزن تلك القيمة في المتغير address. يمكنك تشغيل البرنامج بكتابة ما يلي في سطر الأوامر: mapit 870 Valencia St, San Francisco, CA 94110 وستكون قيمة المتغير sys.argv هي القائمة: ['mapIt.py', '870', 'Valencia', 'St, ', 'San', 'Francisco, ', 'CA', '94110'] وبالتالي تكون قيمة المتغير address هي السلسلة النصية '870 Valencia St, San Francisco, CA 94110'. الخطوة 3: التعامل مع محتويات الحافظة وتشغيل المتصفح تأكد أن الشيفرة الخاصة بك تشبه الشيفرة الآتية: #! python3 # mapIt.py - تشغيل خريطة في المتصفح باستخدام عنوان # من سطر الأوامر أو الحافظة. import webbrowser, sys, pyperclip if len(sys.argv) > 1: # Get address from command line. address = ' '.join(sys.argv[1:]) else: # الحصول على العنوان من الحافظة. address = pyperclip.paste() webbrowser.open('https://www.google.com/maps/place/' + address) إذا لم تكن هنالك وسائط ممررة عبر سطر الأوامر، فسيفترض البرنامج أن العنوان منسوخ إلى الحافظة، ويمكننا الحصول على محتوى الحافظة باستخدام pyperclip.paste() وتخزينها في المتغير address. آخر خطوة هي تشغيل المتصفح مع توفير رابط URL صحيح لخرائط غوغل عبر webbrowser.open(). ستوفر عليك بعض البرامج التي ستطورها ساعات من العمل، لكن بعض قد يكون بسيطًا ويوفر عليك بضع ثوانٍ في كل مرة تجري فيها مهمة تكرارية مثل فتح عنوان ما على الخريطة، الجدول التالي يقارن بين الخطوات اللازمة لعرض الخريطة: فتح الخريطة يدويًا استخدام سكربت mapIt.py تحديد العنوان نسخ العنوان فتح متصفح الويب الذهاب إلى خرائط غوغل الضغط على حقل الإدخال لصق العنوان الضغط على enter تحديد العنوان نسخ العنوان تشغيل mapIt.py مقارنة الخطوات اللازمة لعرض الخريطة أفكار لبرامج مشابهة تساعدك الوحدة webbrowser إذا كان لديك عنوان URL لصفحة معينة تريد اختصار عملية فتح المتصفح والتوجه إليها، يمكنك أن تستفيد منها لإنشاء: برنامج يفتح جميع الروابط المذكورة في مستند نصي في ألسنة جديدة. برنامج يفتح المتصفح على صفحة الطقس لمدينتك. برنامج يفتح مواقع التواصل الاجتماعي التي تزورها عادة. تنزيل الملفات من الويب باستخدام الوحدة requests تسمح لك الوحدة requests بتنزيل الملفات من الويب دون أن تفكر في مشاكل الشبكة أو الاتصال أو ضغط البيانات. لا تأتي الوحدة requests مضمنةً في بايثون، وإنما عليك تثبيتها أولًا من سطر الأوامر بتشغيل الأمر pip install --user requests. أتت الوحدة requests لتقدم حلًا بديلًا لوحدة urllib2 في بايثون لأنها معقدة زيادة عن اللزوم، وأنصحك أن تزيل الوحدة urllib2 من ذهنك تمامًا، لأنها وحدة صعبة دون داعٍ، وعليك استخدام الوحدة requests دومًا. لنجرب الآن أن الحزمة requests مثبتة صحيحًا بإدخال ما يلي في الصدفة التفاعلية: import requests>>> import requests إذا لم تظهر أي رسالة خطأ فهذا يعني أن الحزمة requests مثبتة عندك. تنزيل صفحة ويب باستخدام الدالة requests.get() الدالة requests.get() تقبل سلسلةً نصيةً فيها رابط URL لتنزيلها. إذا استعملت الدالة type() على القيمة المعادة من الدالة requests.get() فسترى أن الناتج هو كائن Response، الذي يحتوي على الرد الذي تلقاه برنامجك بعد إتمام الطلبية إلى خادم الويب. سنستكشف سويةً الكائن Response بالتفصيل لاحقًا، لكن لنكتب الآن الأسطر الآتية في الطرفية التفاعلية على حاسوب متصل بالإنترنت: >>> import requests ➊ >>> res = requests.get('https://automatetheboringstuff.com/files/rj.txt') >>> type(res) <class 'requests.models.Response'> ➋ >>> res.status_code == requests.codes.ok True >>> len(res.text) 178981 >>> print(res.text[:250]) The Project Gutenberg EBook of Romeo and Juliet, by William Shakespeare This eBook is for the use of anyone anywhere at no cost and with almost no restrictions whatsoever. You may copy it, give it away or re-use it under the terms of the Proje الرابط الذي طلبناه هو مستند نصي لمسرحية روميو وجولييت على موقع الكتاب الأصلي ➊، يمكنك أن ترى نجاح الطلبية إلى صفحة الويب بالنظر إلى السمة status_code من الكائن Response، إذا كانت القيمة الخاصة بها تساوي requests.codes.ok فهذا يعني أن كل شيء على ما يرام ➋. بالمناسبة، رمز الاستجابة الذي يدل على أن الأمور على ما يرام OK في HTTP هو 200، ومن المرجح أنك تعرف الحالة 404 التي تشير إلى رابط غير موجود. يمكنك العثور على قائمة برموز الاستجابة في HTTP ومعانيها في الصفحة قائمة رموز الاستجابة في HTTP من ويكيبيديا. إذا نجحت الطلبية، فستخزن صفحة الويب التي نزلناها كسلسلة نصية في المتغير text في كائن Response. يحتوي هذا المتغير على سلسلة نصية طويلة فيها المسرحية كاملةً. وإذا استدعيت len(res.text) فسترى أنها أطول من 178,000 محرف. استدعينا في النهاية print(res.text[:250]) لعرض أول 250 محرف. إذا فشل الطلب وظهرت رسالة خطأ مثل "Failed to establish a new connection" أو "Max retries exceeded" فتأكد من اتصالك بالإنترنت. لا نستطيع نقاش جميع أسباب عدم القدرة على الاتصال بالخوادم لتعقيد الموضوع، لكن أنصحك بالبحث في الويب عن المشكلة التي تواجهك لترى حلها. التأكد من عدم وجود مشاكل كما رأينا سويةً، يملك الكائن Response السمة status_code التي تأكدنا أنها تساوي requests.codes.ok (وهو متغير فيه القيمة الرقمية 200) للتحقق من نجاح عملية التنزيل. هنالك طريقة أخرى سهلة للتحقق من نجاح التنفيذ هو استدعاء التابع raise_for_status() على الكائن Response، التي ستؤدي إلى إطلاق استثناء إذا حدث خطأ حين تنزيل الملف، ولن تفعل شيئًا إن سارت الأمور على ما يرام. جرب ما يلي في الصدفة التفاعلية: >>> res = requests.get('https://inventwithpython.com/page_that_does_not_exist') >>> res.raise_for_status() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\Al\AppData\Local\Programs\Python\Python37\lib\site-packages\requests\models .py", line 940, in raise_for_status raise HTTPError(http_error_msg, response=self) requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://inventwithpython .com/page_that_does_not_exist.html يضمن لنا التابع ()raise_for_status أن البرنامج سيتوقف إذا حدثت مشكلة في التنزيل، وهذا مناسب جدًا إذا أردنا إيقاف البرنامج حين حصول مشكلة في التنزيل. أما لو كنا نريد استمرار البرنامج حتى لو فشل تنزيل الملف فيمكننا حينئذٍ أن نحيط التابع raise_for_status() بالتعبيرات try و except لمعالجة هذا الاستثناء: import requests res = requests.get('https://inventwithpython.com/page_that_does_not_exist') try: res.raise_for_status() except Exception as exc: print('There was a problem: %s' % (exc)) التابع raise_for_status() في الشيفرة السابقة سيؤدي إلى طباعة ما يلي: There was a problem: 404 Client Error: Not Found for url: https:// inventwithpython.com/page_that_does_not_exist.html احرص دومًا على استدعاء التابع raise_for_status() بعد استدعاء requests.get() لتضمن أن برنامجك قد نزَّل الملف دون مشاكل قبل إكمال التنفيذ. حفظ الملفات المنزلة إلى نظام الملفات يمكنك الآن حفظ صفحة الويب إلى نظام الملفات لديك باستخدام الدالة open() والتابع write()، هنالك بعض الاختلافات البسيطة عمّا فعلناه سابقًا: علينا أن نفتح الملف في وضع الكتابة بالنظام الثنائي write binary بتمرير السلسلة النصية 'wb' كوسيط ثانٍ إلى الدالة open() حتى لو كان الملف المنزل نصيًا (مثل مسرحية روميو وجولييت في المثال أعلاه)، لأننا نريد كتابة البيانات الثنائية بدلًا من البيانات النصية للحفاظ على ترميز النص encoding. لنستعمل حلقة for مع التابع iter_content() للكائن Response لكتابة صفحة الويب إلى ملف: >>> import requests >>> res = requests.get('https://automatetheboringstuff.com/files/rj.txt') >>> res.raise_for_status() >>> playFile = open('RomeoAndJuliet.txt', 'wb') >>> for chunk in res.iter_content(100000): playFile.write(chunk) 100000 78981 >>> playFile.close() يعيد التابع iter_content() قطعًا من النص في كل تكرار لحلقة التكرار، وكل قطعة يكون لها نوع البيانات bytes وتحدد لها كم بايتًا يجب أن يكون طول كل قطعة، وأرى أن مئة ألف بايت هو حجم مناسب، لذا لنمرر 100000 إلى التابع iter_content(). أنشأنا الآن الملف RomeoAndJuliet.txt في مجلد العمل الحالي، لاحظ أن اسم الملف في موقع الويب هو rj.txt بينما اسم الملف المحفوظ لدينا مختلف. تذكر أن الوحدة requests تنزل محتويات صفحات الويب لديك، وبعد تنزيلها يكون على عاتقك التعامل معها وحفظها إن شئت أينما تشاء. للمراجعة، هذه هي الخطوات الكاملة لتنزيل وحفظ ملف: استدعاء التابع requests.get() لتنزيل ملف. استدعاء open() مع الخيار 'wb' لإنشاء ملف جديد وفتحه للكتابة في الوضع الثنائي. المرور على التابع iter_content() للكائن Response. استدعاء التابع write() في كل تكرار لكتابة المحتوى إلى الملف. إغلاق الملف close(). هذا كل ما يتعلق بالوحدة requests! قد تبدو لك حلقة for مع iter_content() معقدةً مقارنةً باستخدام open() و write() و close() التي استخدمناها لكتابة الملفات النصية؛ لكننا فعلنا ذلك لنضمن أن برنامجنا لن يستهلك ذاكرة كثيرة إذا نزلنا ملفات ضخمة. يمكنك قراءة المزيد حول ميزات الوحدة requests الأخرى من requests.readthedocs.org. لغة HTML قبل أن نستخلص المعلومات من صفحات الويب، لنتعلم بعض أساسيات HTML أولًا، ولنرى كيف نصل إلى أدوات المطور في متصفح الويب، التي ستجعل عملية استخراج البيانات من الويب أمرًا سهلًا جدًا. مصادر لتعلم HTML لغة HTML هي الصيغة التي تكتب فيها صفحات الويب، ويفترض هذا المقال أن لديك بعض الأساسيات حول HTML، لكن إن كنت تريد البدء من الصفر فأنصحك أن تراجع: توثيق HTML في موسوعة حسوب. قسم HTML في أكاديمية حسوب. تذكرة سريعة في حال لم تلمس HTML من مدة، سأخبرك بملخص بسيط عنها. ملفات HTML هي ملفات نصية لها اللاحقة html، وتكون النصوص فيها محادثة بالوسوم tags، وكل وسم يكون ضمن قوسي زاوية <>، وتخبر هذه الوسوم المتصفحات كيف يجب أن تعرض الصفحة. يمكن أن يكون هنالك نص بين وسم البداية ووسم النهاية، وهذا ما يؤلف عنصرًا element. فمثلًا، الشيفرة الآتية تعرض لك Hello, world! في المتصفح، وتكون كلمة Hello بخط عريض: <strong>Hello</strong>, world! وستبدو في المتصفح كما في الشكل الآتي: مثال Hello, world! معروضة في متصفح وسم البداية <strong> يخبر المتصفح أن النص سيكون بخط عريض، ووسم النهاية </strong> يخبر المتصفح أين هي نهاية النص العريض. هنالك وسوم متنوعة في HTML، ولبعض تلك الوسوم خاصيات تُذكَر ضمن قوسَي الزاوية <>، مثلًا الوسم <a> يعني أن النص هو رابط، وتكون قيمة هذا الرابط محددة بالخاصية href. مثال: Al's free <a href="https://inventwithpython.com">Python books</a>. ستبدو صفحة الويب كما في الشكل الآتي: رابط معروض في متصفح. تمتلك بعض العناصر الخاصية id التي تستخدم لتعريف عناصر الصفحة بشكل فريد، ويمكنك أن تطلب من برامجك البحث عن عنصر ما باستخدام معرفه id، لهذا تكون معرفة قيمة id لأحد العناصر من أهم الأمور التي سنستعمل فيها أدوات المطور أثناء كتابة برامج استخلاص البيانات من صفحات الويب. عرض مصدر صفحة HTML إذا أردت إلقاء نظرة على مصدر صفحة HTML لإحدى الصفحات التي تزورها، اضغط على الزر الأيمن للفأرة واختر View page source كما في الشكل التالي. هذا النص هو ما يحصل عليه متصفحك وهو يعرف كيف يعرض الصفحة اعتمادًا على ذاك النص. عرض مصدر صفحة ويب أنصح -وبشدة- أن تجرب عرض مصدر صفحات HTML لبعض مواقعك المفضلة، حتى لو لم تفهم تمامًا كل ما تراها أمامك، فلست بحاجة إلى احتراف HTML لتعرف كيف تكتب برامج لاستخراج البيانات؛ فليس المطلوب منك برمجة موقعك الشخصي وإنما أن يكون لديك ما يكفي لتحصل البيانات المطلوبة. فتح أدوات المطور إضافةً إلى عرض المصدر، يمكنك أن تلقي نظرة على صفحات HTML باستخدام أدوات المطور في متصفحك. يمكنك أن تضغط الزر F12 في كروم لإظهارها، أو الضغط على F12 مرة أخرى لإخفائها. يمكنك أيضًا فتحها من القائمة الجانبية ثم Developer Tools، أو الضغط على ⌘-⌥-I في ماك. أدوات المطور في متصفح كروم أما في فايرفكس، فيمكنك فتح أدوات المطور بالضغط على Ctrl+Shift+C في ويندوز ولينكس، أو ⌘-⌥-C في ماك، وأدوات المطور هنا تشبه كروم كثيرًا. بعد تفعيل أدوات المطور في متصفحك، يمكنك الضغط بالزر الأيمن للفأرة على أي عنصر واختيار Inspect Element في القائمة المنبثقة لترى شيفرة HTML المسؤولة عن ذاك الجزء من الصفحة. ستستفيد من ذلك كثيرًا عندما تبدأ تفسير صفحات HTML لاستخراج البيانات منها. استخدام أدوات المطور للعثور على عناصر HTML بعد أن نزل برنامج صفحة الويب باستخدام الوحدة requests فسيكون لدينا صفحة الويب كاملةً كسلسلة نصية واحدة، وعلينا أن نجد طريقة لمعرفة ما هي عناصر HTML التي تحتوي على المعلومات التي نريد استخراجها من الصفحة. ستساعدنا هنا أدوات المطور في ذلك. لنقل مثلًا أننا نريد كتابة برنامج لجلب بيانات الطقس من موقع weather.gov. لكن قبل أن نبدأ بكتابة الشيفرات فعلينا القيام ببعض التحريات أولًا. إذا زرنا الموقع وبحثنا عن الرمز البريدي 94105 فستحصل على صفحة تظهر لك الطقس في تلك المنطقة. ماذا إن كانت مهتمًا باستخراج بيانات الطقس لهذا العنوان البريدي؟ اضغط بالزر الأيمن على مكان معلومات الطقس في تلك الصفحة واختر Inspect Element من القائمة، وستظهر لك نافذة أدوات المطور، التي تريك شيفرات HTML المسؤولة عن ذاك الجزء من الصفحة كما يظهر في الشكل التالي. انتبه إلى أن الموقع قد يتغير دوريًا وقد تظهر لك عناصر مختلفة وعليك اتباع نفس الخطوات لتفحص العناصر الجديدة. تفحص عناصر صفحة الطقس باستخدام أدوات المطور يمكننا أن نرى ما هي شيفرة HTML المسؤولة عن عرض الطقس في الصفحة السابقة، وهي: <div class="col-sm-10 forecast-text">Sunny, with a high near 64. West wind 11 to 16 mph, with gusts as high as 21 mph.</div> هذا ما نبحث عنه تمامًا، يبدو أن معلومات الطقس موجودة داخل عنصر <div> له صنف CSS باسم forecast-text. اضغط بالزر الأيمن على العنصر في أدوات المطور واختر من القائمة Copy ▸ CSS Selector، وستُنسَخ لك سلسلة نصية مثل الآتية إلى الحافظة: 'div.row-odd:nth-child(1) > div:nth-child(2)' ويمكنك أن تستخدم تلك السلسلة النصية مع التابع ذ في BS4 أو find_element_by_css_selector() في Selenium كما هو مشروح في هذا المقال. الآن وبعد أن عرفت ما الذي تبحث عنه تحديدًا، يمكن لوحدة Beautiful Soup أن تساعدك في العثور عليه. تفسير HTML مع وحدة BS4 تستخدم الوحدة Beautiful Soup في استخراج المعلومات من صفحة HTML، واسم الوحدة في بايثون هو bs4 للإشارة إلى الإصدار الرابع منها، ويمكننا تثبيتها عبر الأمر pip install --user beautifulsoup4 (راجع الملحق أ لتعليمات حول تثبيت الوحدات). صحيح أننا استخدمنا beautifulsoup4 لتثبيت الوحدة، لكن حين استيرادها في بايثون سنستعمل import bs4. ستكون أمثلتنا عن BS4 عن تفسير ملف HTML (أي تحليلها والتعرف على أجزائها) مخزن محليًا على حاسوبنا. افتح لسانًا جديدًا في محرر Mu وأدخل ما يلي فيه واحفظه باسم example.html: <!-- This is the example.html example file. --> <html><head><title>The Website Title</title></head> <body> <p>Download my <strong>Python</strong> book from <a href="https:// inventwithpython.com">my website</a>.</p> <p class="slogan">Learn Python the easy way!</p> <p>By <span id="author">Al Sweigart</span></p> </body></html> نعم، حتى ملفات HTML البسيطة فيها مختلف العناصر والخاصيات، وستكون الأمور أعقد بكثير في المواقع الكبيرة، لكن مكتبة BS4 هنا لمساعدتنا وتسهيل الأمر علينا. إنشاء كائن BeautifulSoup من سلسلة HTML نصية يمكننا استدعاء الدالة bs4.BeautifulSoup() مع تمرير سلسلة نصية تحتوي على شيفرة HTML التي ستفسر. تعيد الدالة bs4.BeautifulSoup() كائن BeautifulSoup. جرب ما يلي في الصدفة التفاعلية على حاسوبك مع وجود إنترنت: >>> import requests, bs4 >>> res = requests.get('https://academy.hsoub.com') >>> res.raise_for_status() >>> academySoup = bs4.BeautifulSoup(res.text, 'html.parser') >>> type(academySoup) <class 'bs4.BeautifulSoup'> هذه الشيفرة تستعمل requests.get() لتنزيل صفحة الويب من موقع أكاديمية حسوب ثم تمرر قيمة السمة text للرد إلى الدالة bs4.BeautifulSoup() التي تعيد كائن BeautifulSoup نخزنه في المتغير academySoup. يمكننا أيضًا تحميل ملف HTML من القرص لدينا بتمرير كائن File إلى الدالة bs4.BeautifulSoup() مع وسيط ثانٍ يخبر وحدة BS4 ما المفسر الذي عليها استعماله لتفسير الملف. أدخل ما يلي في الصدفة التفاعلية مع التأكد أنك في نفس المجلد الذي يحتوي على ملف example.html: >>> exampleFile = open('example.html') >>> exampleSoup = bs4.BeautifulSoup(exampleFile, 'html.parser') >>> type(exampleSoup) <class 'bs4.BeautifulSoup'> المفسر 'html.parser' المستخدم هنا يأتي مع بايثون، لكن يمكنك استخدام المفسر 'lxml' الأسرع إذا ثبتت الوحدة الخارجية lxml. اتبع التعليمات الموجودة في الملحق أ لتثبيت الوحدة باستخدام الأمر pip install --user lxml. إذا لم نضمِّن الوسيط الثاني الذي يحدد المفسر فسيظهر التحذير: UserWarning: No parser was explicitly specified. بعد أن يكون لدينا كائن BeautifulSoup سنتمكن من استخدام توابعه لتحديد أجزاء معينة من مستند HTML. العثور على عنصر باستخدام التابع select() يمكنك الحصول على عنصر من عناصر صفحة الويب في الكائن BeatuifulSoup باستدعاء التابع select() وتمرير محدد CSS (أي CSS Selector) للعنصر الذي تبحث عنه. المحددات تشبه التعابير النمطية في وظيفتها: تحدد نمطًا يمكن البحث عنه في صفحات الويب. نقاش محددات CSS خارج سياق هذه السلسلة، لكن هذه مقدمة مختصرة عنها في الجدول التالي، وأنصحك بالاطلاع على توثيق المحددات في موسوعة حسوب. المحدد الذي يمرر إلى التابع ()select سيطابق soup.select('div') جميع عناصر <div> soup.select('#author') جميع العناصر التي لها الخاصية id وقيمتها author soup.select('.notice') جميع العناصر التي لها خاصية class ولها صنف CSS باسم notice soup.select('div span') جميع عناصر <span> الموجود داخل عناصر <div> soup.select('div > span') جميع العناصر <span> الموجودة مباشرةً داخل عناصر <div> بدون وجود أي عنصر بينهما soup.select('input[name]') جميع عناصر <input> التي لها الخاصية name بغض النظر عن قيمتها soup.select('input[type="button"]') جميع عناصر <input> التي لها الخاصية type وتكون مساوية إلى button أمثلة عن محددات CSS يمكن دمج مختلف أنماط المحددات مع بعضها لمطابقة أنماط أكثر تعقيدًا، فمثلًا soup.select('p #author') ستطابق أي عنصر له خاصية id قيمتها author لكن يجب أن يكون هذا العنصر موجودًا داخل عنصر <p>. بدلًا من محاولة كتابة المحددات بنفسك، يمكنك الضغط بالزر الأيمن على أي عنصر في صفحة الويب واختيار Inspect Element، وحينما تفتح أدوات المطور اضغط بالزر الأيمن على عنصر HTML واختر Copy ▸ CSS Selector لنسخ محدد CSS إلى الحافظة لتستطيع لصقه في الشيفرة لديك. التابع select() سيعيد قائمةً بعناصر Tag، وهذا ما تفعله وحدة BS4 لتمثيل عنصر HTML. هذه القائمة ستحتوي على كائن Tag لكل مطابقة للمحدد في كائن BeautifulSoup. يمكن تمرير كائن Tag إلى الدالة str() لعرض وسوم HTML التي تمثلها. تمتلك قيم Tag أيضًا السمة attrs التي تظهر جميع خاصيات HTML للعنصر ممثلةً كقاموس. لنجرب على ملف example.html بإدخال ما يلي في الصدفة التفاعلية: >>> import bs4 >>> exampleFile = open('example.html') >>> exampleSoup = bs4.BeautifulSoup(exampleFile.read(), 'html.parser') >>> elems = exampleSoup.select('#author') >>> type(elems) # elems هي قائمة من كائنات Tag <class 'list'> >>> len(elems) 1 >>> type(elems[0]) <class 'bs4.element.Tag'> >>> str(elems[0]) # تحويل الكائن إلى سلسلة نصية. '<span id="author">Al Sweigart</span>' >>> elems[0].getText() 'Al Sweigart' >>> elems[0].attrs {'id': 'author'} ستستخرج الشيفرة السابقة العنصر الذي له id="author" في مستند HTML. سنستخدم select('#author') للحصول على قائمة بجميع العناصر التي لها id="author"، وسنخزن قائمة كائنات Tag في المتغير elems، وسيخبرنا التعبير len(elems) أن لدينا كائن Tag وحيد في القائمة، أي جرت عملية المطابقة لعنصر HTML وحيد. استدعاء التابع getText() على العناصر سيعيد النص الموجود في العنصر، أي السلسلة النصية الموجودة بين وسم البدء والإغلاق. أما تمرير الكائن إلى الدالة str() سيعيد سلسلة نصيةً فيها وسمَي البداية والنهاية مع نص العنصر؛ أما السمة attrs فستعطينا قاموسًا فيه خاصيات العنصر كاملةً. يمكنك أيضًا استخراج جميع عناصر <p> من كائن BeautifulSoup كما في المثال الآتي: >>> pElems = exampleSoup.select('p') >>> str(pElems[0]) '<p>Download my <strong>Python</strong> book from <a href="https:// inventwithpython.com">my website</a>.</p>' >>> pElems[0].getText() 'Download my Python book from my website.' >>> str(pElems[1]) '<p class="slogan">Learn Python the easy way!</p>' >>> pElems[1].getText() 'Learn Python the easy way!' >>> str(pElems[2]) '<p>By <span id="author">Al Sweigart</span></p>' >>> pElems[2].getText() 'By Al Sweigart' ستعطينا select() ثلاث مطابقات، والتي ستخزن في المتغير pElems. سنجرب استخدام str() على الكائنات pElems[0] و pElems[1] و pElems[2] لعرض العناصر كسلاسل نصية، وسنجرب أيضًا التابع getText() لإظهار نص تلك العناصر فقط. الحصول على معلومات من خاصيات العنصر يسهل علينا التابع get() الخاص بكائنات Tag الوصول إلى خاصيات العناصر، ونمرر لهذا التابع سلسلةً نصيةً باسم الخاصية attribute وسيعيد لنا قيمتها. لنجرب المثال الآتي على الملف example.html: >>> import bs4 >>> soup = bs4.BeautifulSoup(open('example.html'), 'html.parser') >>> spanElem = soup.select('span')[0] >>> str(spanElem) '<span id="author">Al Sweigart</span>' >>> spanElem.get('id') 'author' >>> spanElem.get('some_nonexistent_addr') == None True >>> spanElem.attrs {'id': 'author'} يمكننا استخدام select() للعثور على أي عناصر <span> ثم تخزين أول عنصر مطابق في المتغير spanElem، الذي سنمرر للتابع get() القيمة 'id' للحصول على قيمة المعرف الخاصة به، وهي 'author' في مثالنا. مشروع: فتح جميع نتائج البحث في كل مرة أبحث فيها في جوجل، لا أريد أن أرى نتيجة بحث واحدة فقط ثم أنتقل إلى النتيجة التي بعدها، بل أضغط بسرعة على الزر الأوسط للفأرة على كل الروابط (أو الضغط عليها مع زر Ctrl) لفتحها في لسان جديد وأقرؤها معًا لاحقًا. فعلت هذا الأمر كثيرًا في جوجل لدرجة أنني مللت من البحث ثم الضغط على كل الروابط واحدًا واحدًا. ماذا لو كتبت برنامجًا أستطيع عبره كتابة عبارة البحث وسيفتح لي متصفحًا فيه كل نتائج البحث الأولى مفتوحةً كل واحد منها في لسان في المتصفح؟ لنكتب سكربتًا يفعل ذلك مع صفحة نتائج بحث فهرس حزم بايثون. يمكن تعديل هذا البرنامج ليعمل على الكثير من المواقع الأخرى، لكن انتبه إلى أن محركات البحث غوغل و DockDockGo عادةً ما تصعب عملية استخراج نتائج البحث من مواقعها. هذا ما يفعله البرنامج: الحصول على عبارة البحث من سطر الأوامر الحصول على صفحة نتائج البحث فتح لسان متصفح جديد لكل نتيجة هذا يعني أن على برنامجك أن يفعل ما يلي: قراءة وسائط سطر الأوامر من sys.argv. الحصول على صفحة نتائج البحث عبر الوحدة requests. العثور على جميع روابط نتائج البحث. استدعاء الدالة webbrowser.open() لفتحها في المتصفح. لنبدأ البرمجة بفتح ملف جديد باسم searchpypi.py في محررنا. الخطوة 1: الحصول على وسائط سطر الأوامر وطلب صفحة نتائج البحث قبل أن نبدأ بالبرمجة، علينا أن نعرف ما هو رابط URL لصفحة نتائج البحث. انظر إلى شريط العنوان في المتصفح بعد إتمامك لعملية البحث، وسترى أن الرابط يشبه https://pypi.org/search/?q=SEARCH_TERM_HERE. يمكننا استخدام الوحدة requests لتنزيل هذه الصفحة، ثم استخدام BS4 للبحث عن الروابط في مستند HTML. يمكننا في النهاية أن نستعمل وحدة webbrowser لفتح تلك الروابط في ألسنة جديدة. يحب أن تكون الشيفرة كما يلي: #! python3 # searchpypi.py - فتح عدة نتائج بحث. import requests, sys, webbrowser, bs4 print('Searching...') # عرض النص ريثما تحمل الصفحة res = requests.get('https://pypi.org/search/?q=' + ' '.join(sys.argv[1:])) res.raise_for_status() # TODO: Retrieve top search result links. # TODO: فتح لسان لكل نتيجة. سيحدد المستخدم كلمات البحث عبر تمريرها إلى سطر الأوامر أثناء تشغيل البرنامج، وستخزن في sys.argv كما رأينا في المقالات السابقة لهذه السلسلة. الخطوة 2: العثور على كل النتائج ستحتاج الآن إلى وحدة BS4 لاستخراج نتائج البحث من مستند HTML المنزل. لكن كيف يمكننا معرفة المحدد المناسب لحالتنا؟ ليس من المنطقي مثلًا البحث عن جميع عناصر <a> لوجود روابط كثيرة لا تهمنا؛ والحل هنا أن نفتح أدوات المطور في المتصفح وننظر إلى المحدد المناسب الذي سينتقي لنا الروابط التي نريدها فقط. بعد أن نبحث فسنجد بعض العناصر التي تشبه التالي: <a class="package-snippet" href="/project/pyautogui/"> ولا يهمنا إن كانت تلك العناصر معقدة، وإنما نحتاج إلى النمط الذي علينا استخدامه للبحث عن الروابط. لنعدل شيفرتنا إلى: #! python3 # searchpypi.py - Opens several google results. import requests, sys, webbrowser, bs4 --snip-- # Retrieve top search result links. soup = bs4.BeautifulSoup(res.text, 'html.parser') # فتح لسان لكل نتيجة. linkElems = soup.select('.package-snippet') إذا ألقيت نظرةً إلى عناصر <a> فستجد أن جميع نتائج البحث لها الخاصية class="package-snippet" وإذا بحثنا في كامل مصدر الصفحة فسنتأكد أن الصنف package-snippet ليس مستخدمًا إلا لروابط نتائج البحث. لا يهمنا ما هو الصنف package-snippet ولا ما يفعل، وإنما يهمنا كيف سنستفيد منه لتحديد عناصر <a> التي نبحث عنها. لننشِئ كائن BeautifulSoup من صفحة HTML التي نزلناها ونستخدم المحدد '.package-snippet' لتحديد جميع عناصر <a> التي لها صنف CSS المحدد؛ ضع في ذهنك أن موقع PyPI قد يحدث واجهته الرسومية وقد تحتاج إلى استخدام محدد CSS مختلف مستقبلًا، إن حدث ذلك فمرر المحدد الجديد إلى التابع soup.select() وسيعمل البرنامج على ما يرام. الخطوة 3: فتح نتائج البحث في المتصفح لنخبر برنامجنا الآن أن يفتح لنا النتائج الأولى في متصفح الويب. أضف ما يلي إلى برنامجك: #! python3 # searchpypi.py - فتح عدة نتائج بحث. import requests, sys, webbrowser, bs4 --snip-- # فتح لسان لكل نتيجة. linkElems = soup.select('.package-snippet') numOpen = min(5, len(linkElems)) for i in range(numOpen): urlToOpen = 'https://pypi.org' + linkElems[i].get('href') print('Opening', urlToOpen) webbrowser.open(urlToOpen) سيفتح البرنامج افتراضيًا أول خمسة روابط في المتصفح باستخدام الوحدة webbrowser، لكن قد يكون ناتج بحث المستخدم أقل من خمس نتائج، فحينها ننظر أيهما أقل، 5 أم عدد عناصر القائمة المعادة من استدعاء التابع soup.select(). الدالة المضمنة في بايثون min() تعيد العدد الأصغر من الوسائط الممررة إليها (والدالة max() تفعل العكس). يمكنك أن تستخدم الدالة min() لمعرفة إذا كان عدد الروابط أقل من 5 وتخزين الناتج في المتغير numOpen، والذي ستستفيد منه في حلقة for عبر range(numOpen). سنستخدم الدالة webbrowser.open() في كل تكرار لحلقة for لفتح الرابط في المتصفح. لاحظ أن قيمة الخاصية href في عناصر <a> لا تحتوي على https://pypi.org لذا علينا إضافتها بأنفسنا إلى قيمة الخاصية href قبل فتح الرابط. يمكنك الآن فتح أول 5 نتائج بحث عن «boring stuff» في محرك بحث PyPI بكتابة الأمر searchpypi boring stuff في سطر الأوامر. أفكار لبرامج مشابهة من الجميل أن يكون لدينا برنامج يفتح لنا عدة ألسنة في المتصفح لأي عملية روتينية مكررة، مثل: فتح جميع صفحات المنتجات في متجر أمازون بعد البحث عن منتج معين. فتح كل روابط المراجعات لمنتج ما. فتح الصور الناتجة بعد إجراء عملية بحث سريعة على أحد مواقع الصور مثل Flickr. مشروع: تنزيل كل رسمات XKCD عادةً ما تحدث المواقع والمدونات الصفحة الرئيسية لها بعرض آخر منشور فيها، مع وجود زر «السابق» الذي يأخذك إلى المنشور السابق، ثم تجد في المنشور السابق زرًا يأخذك للمنشور الذي قبله، وهلم جرًا، مما ينشِئ سلسلةً من الصفحات التي تأخذك من الصفحة الرئيسية إلى صفحة أول منشور في الموقع. إذا أردت نسخةً من محتوى موقعٍ ما لتقرأها وأنت غير متصل بالإنترنت، فيمكنك أن تفتح بنفسك كل صفحة وتحفظها، لكن هذا الأمر ممل جدًا ومن المناسب كتابة برنامج لأتمتة هذه المهمة. موقع XKCD هو موقع كوميكس له نفس البنية المذكورة. وصفحته الرئيسية فيها زر Prev للعودة إلى الكوكميس السابقة، لكن عملية تنزيل كل الكوميكس واحدةً واحدةً تأخذ وقتًا كثيرًا، لكننا نستطيع كتابة سكربت لأتمتة ذلك في ثوانٍ معدودة. هذا ما سيفعله برنامجنا: فتح صفحة XKCD الرئيسية حفظ صورة الكوميكس في تلك الصفحة الانتقال إلى صفحة الكوميكس السابق تكرار العملية حتى الوصول إلى أول صورة كوميكس. هذا يعني أن الشيفرة البرمجية ستفعل ما يلي: تنزيل الصفحات باستخدام الوحدة requests. العثور على رابط URL لصورة الكوميكس باستخدام وحدة BS4. تنزيل وحفظ صورة الكوميكس إلى نظام الملفات باستخدام iter_content(). العثور على رابط صورة الكوميكس السابقة، وتكرار العملية كلها. افتح ملفًا جديدًا في محررك وسمِّه باسم downloadXkcd.py. الخطوة 1: تصميم البرنامج إذا فتحت أدوات المطور في المتصفح ونظرت إلى عناصر الصفحة، فسترى ما يلي: رابط URL لملف صورة الكوميكس في الخاصية href في العنصر <img>. العنصر <img> موجود داخل العنصر <div id="comic">. الزر Perv له الخاصية rel وقيمتها prev. صفحة أول صورة كوميكس يشير فيها الزر Prev إلى الرابط https://xkcd.com/# مما يشير إلى عدم وجود صفحات سابقة. عدّل الشيفرة لتبدو كما يلي: #! python3 # downloadXkcd.py - تنزيل كل صورة كوميكس في XKCD. import requests, os, bs4 url = 'https://xkcd.com' # starting url os.makedirs('xkcd', exist_ok=True) # store comics in ./xkcd while not url.endswith('#'): # TODO: Download the page. # TODO: Find the URL of the comic image. # TODO: Download the image. # TODO: حفظ الصورة إلى ./xkcd. # TODO: الحصول على رابط الصفحة السابقة. print('Done.') سيكون لدينا متغير اسمه url يبدأ بالقيمة 'https://xkcd.com' وتحدث قيمته دوريًا ضمن حلقة for لتصبح الرابط الموجود في الزر Prev للصفحة الحالية. وسننزل صورة الكوميكس في كل تكرار من تكرارات حلقة for الموجودة في الرابط url، وسننتهي من حلقة التكرار حينما تنتهي قيمة url بالعلامة '#'. سننزل ملفات الصور إلى مجلد موجود في مجلد العمل الحالي باسم xkcd، وسنتأكد من أن المجلد موجود باستدعاء os.makedirs() مع تمرير وسيط الكلمات المفتاحية exist_ok=True الذي يمنع الدالة من رمي استثناء إن كان المجلد موجودًا مسبقًا. بقية الشيفرة هو تعليقات سنملؤها في الأقسام القادمة. الخطوة 2: تنزيل صفحة الويب لنكتب الشيفرة الآتية التي ستنزل الصفحة: #! python3 # downloadXkcd.py - تنزيل كل صورة كوميكس في XKCD. import requests, os, bs4 url = 'https://xkcd.com' # starting url os.makedirs('xkcd', exist_ok=True) # store comics in ./xkcd while not url.endswith('#'): # Download the page. print('Downloading page %s...' % url) res = requests.get(url) res.raise_for_status() soup = bs4.BeautifulSoup(res.text, 'html.parser') # TODO: Find the URL of the comic image. # TODO: Download the image. # TODO: حفظ الصورة إلى ./xkcd. # TODO: الحصول على رابط الصفحة السابقة. print('Done.') لنطبع بدايةً قيمة url ليعرف المستخدم ما هو رابط URL الذي يحاول البرنامج تنزيله، ثم سنستخدم الدالة requests.get() في الوحدة requests لتنزيل الصفحة. ثم سنستدعي التابع raise_for_status() كالعادة لرمي استثناء إن حدثت مشكلة ما في التنزيل؛ ثم إن سارت الأمور على ما يرام فسننشِئ كائن BeautifulSoup من نص الصفحة المنزلة. الخطوة 3: البحث عن صورة الكوميكس وتنزيلها لنعدل شيفرة برنامجنا: #! python3 # downloadXkcd.py - تنزيل كل صورة كوميكس في XKCD. import requests, os, bs4 --snip-- # Find the URL of the comic image. comicElem = soup.select('#comic img') if comicElem == []: print('Could not find comic image.') else: comicUrl = 'https:' + comicElem[0].get('src') # Download the image. print('Downloading image %s...' % (comicUrl)) res = requests.get(comicUrl) res.raise_for_status() # TODO: حفظ الصورة إلى ./xkcd. # TODO: الحصول على رابط الصفحة السابقة. print('Done.') بعد تفحص صفحة XKCD عبر أدوات المطور، سنعرف أن عنصر <img> لصورة الكوميكس موجود داخل عنصر <div> له الخاصية id التي قيمتها هي comic، وبالتالي سيكون المحدد '#comic img' صحيحًا لتحديد العنصر <img> عبر كائن BeautifulSoup. تمتلك بعض صفحات موقع XKCD على محتوى خاص لا يمثل صورة بسيطة، لكن لا بأس فيمكننا تخطي تلك الصفحات، فإن لم يطابِق المحدد الخاص بنا أي عنصر فسيعيد استدعاء soup.select('#comic img') قائمة فارغة، وحينها سيظهر برنامجنا رسالة خطأ ويتجاوز الصفحة دون تنزيل الصورة. عدا ذلك، فسيعيد المحدد السابق قائمةً فيها عنصر <img> وحيد، ويمكننا الحصول على قيمة الخاصية src لعنصر <img>ونمررها إلى requests.get() لتنزيل صورة الكوميكس. الخطوة 4: حفظ الصورة والعثور على الصفحة السابقة لنعدل الشيفرة لتصبح كما يلي: #! python3 # downloadXkcd.py - تنزيل كل صورة كوميكس في XKCD. import requests, os, bs4 --snip-- # حفظ الصورة إلى ./xkcd. imageFile = open(os.path.join('xkcd', os.path.basename(comicUrl)), 'wb') for chunk in res.iter_content(100000): imageFile.write(chunk) imageFile.close() # الحصول على رابط الصفحة السابقة. prevLink = soup.select('a[rel="prev"]')[0] url = 'https://xkcd.com' + prevLink.get('href') print('Done.') أصبح لدينا ملف صورة الكوميكس مخزنًا في المتغير res، وعلينا كتابة بيانات هذه الصورة إلى ملف في نظام الملفات المحلي لدينا. سنحدد اسم الملف المحلي للصورة ونمرره إلى الدالة open()، لاحظ أن المتغير comicUrl يحتوي على قيمة تشبه القيمة الآتية: 'https://imgs.xkcd.com/comics/heartbleed_explanation.png' ويبدو أنك لاحظت وجود مسار الملف فيها. يمكنك استخدام الدالة os.path.basename() مع comicUrl لإعادة آخر جزء من رابط URL السابق، أي 'heartbleed_explanation.png'، ويمكنك حينها استخدام هذا الاسم حين حفظ الصورة إلى نظام الملفات المحلي. يمكنك أن تضيف هذا الاسم إلى اسم مجلد xkcd باستخدام الدالة os.path.join() كما تعلمنا سابقًا لكي يستخدم برنامجك الفاصل الصحيح (الخط المائل الخلفي \ في ويندوز، والخط المائل / في لينكس وماك). أصبح لدينا الآن اسم ملف صحيح، ويمكننا استدعاء الدالة open() لفتح ملف جديد بوضع الكتابة الثنائية 'wb'. إذا كنت تذكر حينما حفظنا الملفات التي نزلناها عبر الوحدة requests في بداية المقال كنا نمر بحلقة تكرار على القيمة المعادة من التابع iter_content()، وستكتب الشيفرة الموجودة في حلقة for قطعًا من بيانات الصورة (كحد أقصى 100,000 بايت كل مرة) إلى الملف، ثم تغلق الملف. أصبحت الصورة محفوظة لديك محليًا الآن! علينا بعدئذٍ استخدام المحدد 'a[rel="prev"]' لتحديد العنصر <a> الذي له العلاقة rel مضبوطةً إلى prev. يمكنك استخدام قيمة الخاصية href لعنصر <a> المحدد للحصول على رابط صورة الكوميكس السابقة، والتي ستخزن في المتغير url، ثم ستدور حلقة while مرة أخرى وتعيد تنفيذ عملية التنزيل من جديد على الصفحة الجديدة. سيبدو ناتج تنفيذ البرنامج كما يلي: Downloading page https://xkcd.com... Downloading image https://imgs.xkcd.com/comics/phone_alarm.png... Downloading page https://xkcd.com/1358/... Downloading image https://imgs.xkcd.com/comics/nro.png... Downloading page https://xkcd.com/1357/... Downloading image https://imgs.xkcd.com/comics/free_speech.png... Downloading page https://xkcd.com/1356/... Downloading image https://imgs.xkcd.com/comics/orbital_mechanics.png... Downloading page https://xkcd.com/1355/... Downloading image https://imgs.xkcd.com/comics/airplane_message.png... Downloading page https://xkcd.com/1354/... Downloading image https://imgs.xkcd.com/comics/heartbleed_explanation.png... --snip– هذا المشروع هو مثال ممتاز لبرامج تتبع الروابط تلقائًا لاستخراج كمية معلومات كبيرة من الويب. يمكنك تعلم المزيد من المعلومات حول ميزات Beautiful Soup الأخرى من توثيقها الرسمي. أفكار لبرامج مشابهة تنزيل صفحات الويب وتتبع الروابط فيها هو أساس أي برنامج زحف crawler. يمكنك أن تبرمج برامج تفعل ما يلي: تنسخ موقعًا كاملًا احتياطيًا. تنسخ جميع التعليقات من أحد المنتديات. تعرض قائمة بجميع المنتجات التي عليها تخفيضات في أحد المتاجر. الوحدتان requests و bs4 رائعتين جدًا، لكن عليك أن تعرف ما هو رابط URL المناسب لتمريره إلى requests.get()، لكن في بعض الأحيان قد لا يكون ذلك سهلًا؛ أو ربما عليك تسجيل الدخول إلى أحد المواقع قبل بدء عملية الاستخراج، لهذا السبب سنستكشف سويةً الوحدة selenium لأداء مهام معقدة. التحكم في المتصفح عبر الوحدة selenium تسمح الوحدة selenium لبايثون بالتحكم برمجيًا بالمتصفح بضغط الروابط وتعبئة الاستمارات، مثلها كمثل أي تفاعل من مستخدم بشري. يمكننا أن نتفاعل مع صفحات الويب بطرائق أكثر تقدمًا في الوحدة selenium مقارنة مع requests و bs4، لكنها قد تكون أبطأ وأصعب بالتشغيل في الخلفية لأننا نفتح متصفح ويب كامل مقارنة بتنزيل بعض الصفحات من الويب. لكن إن كان علينا التعامل مع صفحات الويب بطريقة تعتمد على شيفرات JavaScript التي تحدث الصفحة، فعلينا استخدام selenium بدلًا من requests. أضف إلى ذلك أن المواقع الشهيرة مثل أمازون تستخدم برمجيات للتعرف على الزيارات الآتية من سكربتات التي تحاول استخراج البيانات من صفحاتها أو إنشاء عدّة حسابات مجانية، ومن المرجح أن تحجب هذه المواقع برامجك بعد فترة؛ وهنا تأتي الوحدة selenium التي تعمل مثل متصفحات الويب العادية. أحد أشهر العلامات التي تتعرف فيها المواقع أن الزيارات آتية من برنامج (أو سكربت script) هي عبارة user-agent، التي تُعرِّف متصفح الويب المستخدم وتضمَّن في كل طلبيات HTTP. فمثلًا تكون عبارة user-agent للوحدة requests شيء يشبه 'python-requests/2.21.0'. يمكنك زيارة موقع مثل whatsmyua.info لتعرف ما هي user-agent الخاصة بك. أما الوحدة selenium فمن المرجح أن تظن المواقع أنها مستخدم بشري، لأنها تستخدم user-agent شبيه بالمتصفحات العادية مثل: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0 ولأن نمط التصفح فيها يشبه المتصفحات الأخرى، فستُنزَّل الصور والإعلانات وملفات تعريف الارتباط والمتتبعات كما في المتصفحات العادية. لكن يمكن للمواقع كشف selenium وتحاول شركات شراء بطاقات الدخول والمتاجر الإلكترونية حجب أي سكربتات تستعمل متصفح selenium. تشغيل متصفح تتحكم فيه selenium سنرى في الأمثلة القادمة كيفية التحكم في متصفح فيرفكس من selenium، إذا لم يكن مثبتًا لديك فيمكنك تنزيله من getfirefox.com، ويمكنك تثبيت selenium،من سطر الأوامر بتشغيل pip install --user selenium، وأذكرك بالذهاب إلى الملحق أ لمزيد من المعلومات. قد يكون استيراد مكونات selenium مختلفًا عليك، فبدلًا من import selenium سنستعمل from selenium import webdriver (سبب فعل selenium لذلك خارج نطاق هذه السلسلة لا تقلق حوله). يمكنك بعد ذلك تشغيل فايرفوكس مع selenium بكتابة ما يلي في الصدفة التفاعلية: >>> from selenium import webdriver >>> browser = webdriver.Firefox() >>> type(browser) <class 'selenium.webdriver.firefox.webdriver.WebDriver'> >>> browser.get('https://inventwithpython.com') ستلاحظ أن متصفح فايرفوكس قد بدأ بالعمل حين استدعاء webdriver.Firefox(). استدعاء type() على webdriver.Firefox() سيظهر أنها من نوع البيانات WebDrive، واستدعاء التالي: browser.get('https://inventwithpython.com') سيوجه المتصفح إلى https://inventwithpython.com/. بعد استدعاء webdriver.Firefox() و get() ستفتح الصفحة في متصفح فايرفوكس إذا واجهت مشكلة من قبيل 'geckodriver' executable needs to be in PATH فهذا يعني أن عليك تنزيل محرك فايرفوكس يدويًا قبل استخدام selenium للتحكم به. يمكنك التحكم بمتصفحات أخرى غير فيرفكس إذا ثبتت محرك الويب لهم webdriver. لمتصفح فايرفوكس، اذهب إلى github.com وثبت محرك geckodriver لنظام تشغيلك. (كلمة «Gecko» هي اسم محرك المتصفح engine في فيرفكس). سيحتوي الملف المضغوط المنزل على geckodriver.exe في ويندوز أو geckodriver في ماك ولينكس، وعليك وضعه في مسار PATH الخاص في نظامك، راجع الإجابة الآتية stackoverflow.com. أما لمتصفح كروم فاذهب إلى chromium.org ونزل الملف المضغوط المناسب لنظامك، وضع الملف chromedriver.exe أو chromedriver في مسار PATH كما ذكرنا أعلاه. يمكن فعل نفس الأمر لبقية المتصفحات الرئيسية، ابحث سريعًا في الويب عن اسم المتصفح متبوعًا بالكلمة webdriver وستجدها. قد تواجه مشاكل في تشغيل المتصفحات الجديدة في selenium، بسبب عدم التوافقية بينهما، لكن يمكنك إجراء حل التفافي بتثبيت نسخة قديمة من المتصفح أو من وحدة selenium. يمكنك معرفة تاريخ الإصدارات من pypi.org. للأسف قد تحدث مشاكل توافقية بين selenium وبعض إصدارات المتصفحات، وأنصحك حينها بالبحث في الويب عن الحلول المقترحة، وراجع الملحق أ لمعلومات حول تثبيت إصدار محدد من selenium (مثل تثبيت pip install --user -U selenium==3.14.1). العثور على عناصر في الصفحة توفر كائنات WebDriver عددًا من التوابع للعثور على عناصر صفحة، ويمكن تقسيمها إلى قسمين من التوابع find_element_ و find_elements_*. توابع find_element_* تعيد كائن WebElement وحيد يمثل أول عنصر في الصفحة يطابق الطلبية التي حددتها، بينما تعيد توابع _find_elements قائمة من كائنات WebElement_* لكل عنصر مطابق في الصفحة. يظهر الجدول الموالي أمثلة على التوابع find_element_ و find_elements_ المستدعاة على كائن WebDriver مخزن في المتغير browser. اسم التابع كائن/قائمة كائنات WebElement browser.find_element_by_class_name(name) browser.find_elements_by_class_name(name) العناصر التي تستخدم صنف CSS باسم name browser.find_element_by_css_selector(selector) browser.find_elements_by_css_selector(selector) العناصر التي تطابق محدد CSS معين browser.find_element_by_id(id) browser.find_elements_by_id(id) العناصر التي تطابق id معين browser.find_element_by_link_text(text) browser.find_elements_by_link_text(text) عناصر <a> التي يطابق محتواها القيمة text كاملةً browser.find_element_by_partial_link_text(text) browser.find_elements_by_partial_link_text(text) عناصر <a> التي يطابق محتواها القيمة text جزئيًا browser.find_element_by_name(name) browser.find_elements_by_name(name) العناصر التي لها الخاصية name وتطابق قيمة معينة browser.find_element_by_tag_name(name) browser.find_elements_by_tag_name(name) العناصر التي تطابق وسمًا معينًا، وهي غير حساسة لحالة الأحرف (أي <a> يمكن أن يطابق عبر 'a' أو 'A') توابع WebDriver للعثور على العناصر في selenium باستثناء التوابع *_by_tag_name() تكون جميع الوسائط الممررة إلى الدوال حساسةً لحالة الأحرف، وإذا لم تكن العناصر موجودة في الصفحة فستطلق selenium الاستثناء NoSuchElement، وعليك استخدام عبارات try و except إذا لم ترغب بتوقف برنامج عن العمل عند ذلك. بعد أن تحصل على كائن WebElement فيمكنك أن تتعرف على المزيد حوله بقراءة سماته أو استدعاء توابع المذكورة في الجدول التالي: السمة أو التابع الوصف tag_name اسم الوسم، مثل 'a' لعنصر <a> get_attribute(name) قيمة خاصية name للعنصر text النص الموجود داخل العنصر، مثل 'hello' في <span>hello</span> clear() مسح النص المكتوب في الحقول النصية is_displayed() إعادة True إذا كان العنصر ظاهرًا، وإلا فيعيد False is_enabled() إعادة True إذا كان حقل الإدخال مفعلًا، وإلا فيعيد False is_selected() لأزرار الانتقاء radio ومربعات التأشير checkbox ستعيد True إذا كان العنصر مختار، وإلا فتعيد False location قاموس فيه المفاتيح 'x' و 'y' لمكان العنصر في الصفحة سمات وتوابع الكائن WebElement مثلًا، افتح ملفًا جديدًا واكتب البرنامج الآتي: from selenium import webdriver browser = webdriver.Firefox() browser.get('https://inventwithpython.com') try: elem = browser.find_element_by_class_name(' cover-thumb') print('Found <%s> element with that class name!' % (elem.tag_name)) except: print('Was not able to find an element with that name.') فتحنا هنا متصفح فيرفكس ووجهناه إلى رابط URL معين، وحاولنا العثور على العناصر التي لها الصنف bookcover، وإذا عثرنا على أحد العناصر فسنطبع اسم الوسم عبر السمة tag_name؛ أما لو لم يعثر على أي عنصر فسنطبع رسالة مختلفة. سيعيد البرنامج الناتج الآتي: Found <img> element with that class name! أي عثرنا على عنصر <img> له اسم الصنف 'bookcover'. الضغط على عناصر الصفحة تمتلك كائنات WebElement المعادة من التوابع find_element_* و find_elements_* التابع click() الذي يحاكي ضغطات الفأرة على العنصر، ويمكن استخدام هذا التابع للضغط على رابط أو تحديد زر انتقاء أو الضغط على زر الإرسال أو أي حدث آخر يُطلَق عند الضغط على أحد العناصر. جرب المثال الآتي في الطرفية التفاعلية: >>> from selenium import webdriver >>> browser = webdriver.Firefox() >>> browser.get('https://inventwithpython.com') >>> linkElem = browser.find_element_by_link_text('Read Online for Free') >>> type(linkElem) <class 'selenium.webdriver.remote.webelement.FirefoxWebElement'> >>> linkElem.click() # follows the "Read Online for Free" link سيفتح متصفح فيرفكس الصفحة ويحصل على العنصر <a> الذي يحتوي على النص Read it Online ويحاكي الضغط على الرابط كما لو ضغطته بنفسك. تعبئة وإرسال الاستمارات يمكن تعبئة الحقول النصية مثل <input> أو <textarea> بالبحث عنها ثم استدعاء التابع send_keys(). جرب ما يلي في الطرفية التفاعلية: >>> from selenium import webdriver >>> browser = webdriver.Firefox() >>> browser.get('https://login.metafilter.com') >>> userElem = browser.find_element_by_id('user_name) >>> userElem.send_keys('your_real_username_here') >>> passwordElem = browser.find_element_by_id('user_pass') >>> passwordElem.send_keys('your_real_password_here') >>> passwordElem.submit() لطالما لم تغيّر صفحة MetaFilter المعرف id لحقول اسم المستخدم وكلمة المرور بعد نشر هذا الكتاب، فيمكنك استخدام الشيفرة السابقة لملء تلك الحقول النصية (تذكر أنك تستطيع استخدام أدوات المطور للتأكد من المعرف id للحقول في أي وقت). استدعاء التابع submit() على أي عنصر في الاستمارة له نفس تأثير الضغط على زر الإرسال. تحذير: ابتعد عن تخزين كلمات المرور الخاصة بك في الشيفرة المصدرية لبرامجك قدر الإمكان، فمن السهل تسريب كلمات المرور إذا تركناها دون تشفير. يمكن لبرنامجك أن يطلب كلمة المرور من المستخدم أثناء التشغيل كما ناقشنا في مقال سابق. إرسال المفاتيح الخاصة تمتلك selenuim وحدةً للمفاتيح الخاصة التي لا يمكن كتابتها كسلسلة نصية، مثل زر Tab أو الأسهم. تلك القيم مخزنة كسمات في الوحدة selenium.webdriver.common.keys، ولأن اسم الوحدة طويل جدًا فمن الأسهل كتابة from selenium.webdriver.common.keys import Keys في بداية البرنامج، وحينها تستطيع استخدام Keys في أي مكان تريد أن تكتب فيه selenium.webdriver.common.keys. يعرض الجدول 12-5 أشهر القيم المستخدمة. الجدول 12-5: أشهر القيم المستخدمة في الوحدة selenium.webdriver.common.keys. السمة الشرح Keys.DOWN و Keys.UP و Keys.LEFT و Keys.RIGHT الأسهم في لوحة المفاتيح Keys.ENTER و Keys.RETURN زر Enter Keys.HOME و Keys.END و Keys.PAGE_DOWN و Keys.PAGE_UP أزرار Home و End و PageDown و PageUp على التوالي Keys.ESCAPE و Keys.BACK_SPACE و Keys.DELETE أزرار Escape و Backspace و Delete على التوالي Keys.F1 و Keys.F2 و . . . و Keys.F12 الأزرار F1 حتى F12 في الصف العلوي من لوحة المفاتيح Keys.TAB زر Tab فمثلًا لو لم يكن المؤشر موجودًا داخل حقل نصي، فالضغط على زر Home و End سيؤدي إلى التمرير إلى بداية ونهاية الصفحة على التوالي. جرب ما يلي على الصدفة التفاعلية ولاحظ كيف يؤدي استدعاء send_keys() إلى تمرير الصفحة: >>> from selenium import webdriver >>> from selenium.webdriver.common.keys import Keys >>> browser = webdriver.Firefox() >>> browser.get('https://nostarch.com') >>> htmlElem = browser.find_element_by_tag_name('html') >>> htmlElem.send_keys(Keys.END) # scrolls to bottom >>> htmlElem.send_keys(Keys.HOME) # scrolls to top العنصر <html> موجود في كل ملفات HTML، ويكون كامل محتوى مستند HTML داخله؛ وتحديد هذا العنصر مفيد لو أردنا إرسال المفاتيح إلى صفحة الويب عمومًا، وهذا بدوره مفيد إذا كانت الصفحة تحمِّل محتوى جديد عند التمرير إلى أسفل الصفحة. الضغط على أزرار المتصفح يمكن أن تحاكي الوحدة selenium الضغط على أزرار المتصفح المختلفة: التابع browser.back() يضغط على زر الرجوع التابع browser.forward() يضغط على زر إلى الأمام التابع browser.refresh() يضغط على زر التحديث التابع browser.quit() يضغط على زر إغلاق النافذة المزيد من المعلومات حول Selenium يمكن للوحدة selenium فعل الكثير مما لم نذكره هنا، فيمكنك أن تعدل محتوى ملفات تعريف الارتباط، وتأخذ لقطة شاشة، وتشغل سكربت JavaScript مخصص …إلخ. لمزيد من المعلومات حول تلك الخصائص فأنصحك أن تزور التوثيق الرسمي. الخلاصة المهام المملة ليست متعلقة بالملفات المحلية على حاسوبك. إذا كنت قادرًا على تنزيل صفحات الويب برمجيًا فستستطيع أتمتة أي مهمة تردك! تسهل الوحدة requests تنزيل صفحات الويب، وبعد تعلم أساسيات HTML فيمكنك أن تستخدم الوحدة BeautifulSoup لتفسير الصفحات التي تنزلها. لكن إن كانت تريد أتمتة أي مهمة متعلقة بالويب أيًا كانت، فيمكنك التحكم برمجيًا مباشرةً بمتصفح الويب عبر الوحدة selenium، التي تسمح لك بفتح المواقع وملء الاستمارات كما لو كان برنامجك كائنًا بشريًا يتصفح المواقع. مشاريع تدريبية لكي تتدرب، اكتب برامج لتنفيذ المهام الآتية. إرسال البريد الإلكتروني من سطر الأوامر اكتب برنامج يقبل عنوان بريد إلكتروني وسلسلة نصية في سطر الأوامر، ثم يستخدم selenium للدخول إلى حسابك البريدي وإرسال بريد إلكتروني إلى العنوان المحدد (أنصحك بإنشاء حساب تجريبي مختلف عن حسابك الأساسي). اكتب برامج أخرى لإضافة منشورات إلى فيسبوك أو تويتر (إكس، سمه ما شئت!). منزِّل الصور اكتب برنامج يفتح أحد مواقع مشاركة الصور مثل Flickr ويبحث عن تصنيف معين من الصور وينزل جميع نتائج البحث في الصفحة الأولى. يمكنك أن تكتب برنامج يعمل على أي موقع مشاركة صور ولديه خاصية البحث. لعبة 2048 لعبة 2048 هي لعبة بسيطة تسمح لك بجمع مربعات بجعلها تنزلق إلى أحد الاتجاهات باستخدام أزرار الأسهم. يمكنك أن تحصل على نتيجة عالية إذا جعلت الانزلاق بهذا الترتيب مرارًا وتكرارًا: الأعلى ثم اليمين ثم الأسفل ثم اليسار. اكتب برنامج بسيط يفتح اللعبة gabrielecirulli.github.io وينفذ النمط السابق للعب اللعبة تلقائيًا. التحقق من الروابط اكتب برنامج يأخذ رابط URL من صفحة ويب، ويحاول أن ينزل كل صفحة مرتبطة فيها، ويُعلِّم كل الصفحات التي تعيد 404 وتؤشر عليها أنها روابط مكسورة. ترجمة -بتصرف- للمقال Web Scraping من كتاب Automate the Boring Stuff with Python. اقرأ أيضًا المقال السابق: تنقيح أخطاء Debugging شيفرتك البرمجية باستخدام لغة بايثون برمجة عملاء ويب باستخدام بايثون أهم 10 مكتبات بايثون تستخدم في المشاريع الصغيرة مشاريع بايثون عملية تناسب المبتدئين النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون
-
يمكننا تخزين المعلومات في المتغيرات في برنامجنا وستبقى موجودة طالما استمر تشغيل البرنامج، لكنا ماذا لو أردنا الحفاظ على البيانات بعد انتهاء تنفيذ البرنامج؟ سنحتاج إلى حفظها إلى ملف؛ وسنتعلم في هذا المقال كيفية استخدام بايثون لإنشاء وقراءة وكتابة الملفات في حاسوبنا. الملفات ومساراتها يملك كل ملف خاصيتان أساسيتان: اسم الملف ومساره. يحدد مسار الملف أين سيظهر في حاسوبك، فمثلًا هنالك ملف في حاسوبي الذي يعمل بنظام ويندوز موجود اسمه project.docx في المسار C:\Users\Al\Documents. الجزء الذي يلي اسم الملف ويأتي بعد النقطة يسمى بامتداد الملف file extension ويخبرنا ما هو نوع الملف، فمثلًا الملف project.docx هو مستند وورد؛ بينما تشير Users و Al و Documents إلى مجلدات. يمكن أن تحتوي المجلدات على ملفات ومجلدات أخرى، فمثلًا الملف project.docx موجود في المجلد Documents الذي بدوره موجود في المجلد Al الموجود في المجلد Users. يوضح الشكل الآتي هذه البنية. الشكل 1: ملف موجود في مجلد الجزءC:\ من المسار يسمى بالمجلد الجذر root، أي يحتوي على جميع المجلدات الأخرى. وهو المجلد C:\ في نظام ويندوز أو يسمى القرص C:؛ أما في ماك أو لينكس فيكون المجلد الجذر هو /. سنستعمل في هذه السلسلة نمط مسارات ويندوز لأنها أكثر شيوعًا، لكن إن كنت تستعمل ماك أو لينكس فاستعمل بنية المسارات المناسبة لنظامك. تظهر أجهزة التخزين الأخرى مثل أقراص DVD أو وسائط تخزين USB بطرائق مختلف حسب نظام التشغيل، ففي ويندوز ستظهر على شكل قرص جديد له حرف مختلف مثل D:/ أو E:/. بينما في ماك فستظهر كمجلد جديد في المجلد /Volumes، وفي لينكس ستظهر كمجلدات جديدة في المجلد /mnt (أو /media حسب التوزيعة عندك). من المهم أن تلاحظ أن أسماء الملفات والمجلدات غير حساسة لحالة الأحرف في ويندوز وماك، لكنها حساسة لحالة الأحرف في لينكس. ملاحظة: من المؤكد أن بنية المجلدات وأسماء الملفات في حاسوبك ونظام تشغيلك تختلف عمّا هو عندي، لذا لن تستطيع اتباع أمثلة هذه السلسلة حرفيًا. لكن جرب المتابعة على الملفات والمجلدات الموجودة عندك. الخط المائل الخلفي \ في ويندوز والخط المائل الأمامي / في ماك ولينكس تستعمل مسارات الملفات في أنظمة ويندوز الخط المائل الخلفي \ الذي يفصل بين أسماء المجلدات؛ أما في ماك ولينكس فيستعمل الخط المائل الأمامي / فاصلًا بين المجلدات؛ ولو أردت أن تعمل برامجك التي تكتبها على جميع الأنظمة (وهذا أمر مهم أنصحك به) فعليك أن تتعامل مع كلا الحالتين. لحسن الحظ هنالك دالة في الوحدة pathlib باسم Path()، التي نمرر إليها سلاسل نصية بأسماء المجلدات والملف المطلوب، وستعيد الدالة Path() سلسلةً نصيةً لمسار الملف يستعمل الفاصل الصحيح بين المجلدات وفقًا لنظام التشغيل: >>> from pathlib import Path >>> Path('spam', 'olive', 'eggs') WindowsPath('spam/olive/eggs') >>> str(Path('spam', 'olive', 'eggs')) 'spam\\olive\\eggs' لاحظ أن من الشائع حين استيراد pathlib أن نكتب from pathlib import Path وإلا فسنحتاج إلى كتابة pathlib.Path في كل مرة نريد استعمال Path فيها. سأشغل أمثلة هذا المقال على نظام ويندوز، لذا ستعيد الدالة Path('spam', 'olive', 'eggs') الكائن WindowsPath للمسار النهائي WindowsPath('spam/olive/eggs')؛ وصحيحٌ أن ويندوز يستعمل الخط المائل الخلفي في المسارات، لكن تمثيل الكائن WindowsPath في الطرفية التفاعلية يستعمل الخط المائل الأمامي، هذا لأن مطوري البرمجيات مفتوحة المصدر يفضلون نظام لينكس ويستعملون العادات الخاصة به أثناء التطوير. إذا أردنا الحصول على سلسلة نصية من المسار، فيمكننا تمرير الكائن WindowsPath إلى الدالة str() التي ستعيد في مثالنا السلسلة النصية 'spam\\olive\\eggs'، لاحظ أن الخطوط المائلة الخلفية مضاعفة لأن كل خط مائل خلفي يحتاج إلى خطٍ مائل خلفي آخر لتهريبه. إذا استخدمنا الدالة السابقة في نظام لينكس فستعيد كائن PosixPath، الذي حين تمريره إلى الدالة str() فسيعيد السلسلة النصية 'spam/olive/eggs' (كلمة POSIX تشير إلى مجموعة من المعايير الحاكمة للأنظمة الشبيهة بيونكس Unix-like مثل لينكس، إذا لم تجرب لينكس من قبل فأنصحك وبشدة أن تجربه). يمكن تمرير كائنات Path (سواءً كانت WindowsPath أو PosixPath اعتمادًا على نظام تشغيلك) إلى دوال أخرى متعلقة بالتعامل مع الملفات والتي سنشرحها خلال هذا المقال. المثال الآتي يولد مجموعة من مسارات الملفات في أحد المجلدات: >>> from pathlib import Path >>> myFiles = ['accounts.txt', 'details.csv', 'invite.docx'] >>> for filename in myFiles: print(Path(r'C:\Users\Al', filename)) C:\Users\Al\accounts.txt C:\Users\Al\details.csv C:\Users\Al\invite.docx يفصل الخط المائل الخلفي بين المجلدات في ويندوز، لذا لا يمكنك استخدامه في أسماء الملفات، لكنك تستطيع استخدام الخطوط المائلة الخلفية \ في ماك ولينكس، فبينما يشير المسار Path(r'spam\eggs') إلى مجلدين مختلفين (أو الملف eggs في المجلد spam) في ويندوز، لكنه يشير إلى مجلد أو ملف باسم spam\eggs في ماك ولينكس. لهذا السبب من المستحسن استخدام الخطوط المائلة الأمامية / في شيفرات بايثون دومًا، وسنفعل المثل في أمثلة المقال، وستضمن لنا الوحدة pathlib أن المسارات التي نستخدمها تعمل على جميع أنظمة التشغيل. لاحظ أن الوحدة pathlib جديدة في بايثون 3.4 وأتت لتستبدل دوال os.path القديمة؛ وتدعمها دوال المكتبة القياسية في بايثون بدءًا من الإصدار 3.6. إذا كنت تعمل مع سكربتات مكتوبة بإصدار بايثون 2 فأنصحك أن تستعمل الوحدة pathlib2 التي توفر إمكانية pathlib في بايثون 2.7. يشرح المقال 1 خطوات تثبيت pathlib2 باستخدام pip. سأوضح أي اختلافات واستخدامات للوحدة os حين الحاجة، فقد تستفيد منها حين قراءة السكربتات القديمة أو التي كتبها غيرك. استخدام العامل / لجمع المسارات نستخدم العامل + عادةً لجمع عددين كما في التعبير 2 + 2، الذي ينتج القيمة العددية 4، لكن يمكننا أيضًا استخدام العامل + لجمع سلسلتين نصيتين كما في التعبير 'Hello' + 'World' الذي ينتج السلسلة النصية 'HelloWorld'. وبشكل مشابه يستعمل العامل / للقسمة لكن يمكنه أيضًا أن يجمع بين كائنات Path والسلاسل النصية، وهو يفيد في التعامل مع كائنات Path التي أنشأناها سابقًا عبر الدالة Path(): >>> from pathlib import Path >>> Path('spam') / 'olive' / 'eggs' WindowsPath('spam/olive/eggs') >>> Path('spam') / Path('olive/eggs') WindowsPath('spam/olive/eggs') >>> Path('spam') / Path('olive', 'eggs') WindowsPath('spam/olive/eggs') يسهل استخدام العامل / مع كائنات Path عملية جمع المسارات مع بعضها كما لو كانت سلاسل نصية بسيطة، واستخدامه أكثر أمانًا من إجراء عملية جمع للسلاسل النصية يدويًا أو عبر التابع join() كما في المثال الآتي: >>> homeFolder = r'C:\Users\Al' >>> subFolder = 'spam' >>> homeFolder + '\\' + subFolder 'C:\\Users\\Al\\spam' >>> '\\'.join([homeFolder, subFolder]) 'C:\\Users\\Al\\spam' الشيفرة السابقة ليست آمنة لأن الخطوط المائلة الخلفية لا تعمل إلا على ويندوز كما ناقشنا في الأقسام السابقة. يمكنك أن تضيف عبارةً شرطيةً if للتحقق من sys.platform (الذي يحتوي على سلسلة نصية تصف نظام التشغيل المستعمل) لتحديد ما هو نوع الخط المائل الذي نريد استخدامه. لكن استخدام هذه الشيفرة في كل مكان تريد التعامل مع مسارات الملفات فيه هو أمر متعب وغير متناسق ومن المرجح أن يسبب علل برمجية. تحل الوحدة pathlib هذه المشاكل بإعادة استخدام معامل القسمة / ليجمع بين المسارات دون مشاكل بغض النظر عن نظام التشغيل المستعمل. يوضح المثال الآتي آلية استخدامه: >>> homeFolder = Path('C:/Users/Al') >>> subFolder = Path('spam') >>> homeFolder / subFolder WindowsPath('C:/Users/Al/spam') >>> str(homeFolder / subFolder) 'C:\\Users\\Al\\spam' أمر واحد مهم يجب أن نبقيه في ذهننا أثناء استخدام العامل / لجمع المسارات هو أن إحدى أول قيمتين من المسارات التي يجب جمعها يجب أن تكون كائن Path، وإلا فستظهر لك بايثون رسالة خطأ: >>> 'spam' / 'olive' / 'eggs' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for /: 'str' and 'str' تقيّم بايثون التعبير البرمجي الذي يستعمل العامل / من اليسار إلى اليمين إلى كائن Path، لهذا يجب أن يكون أول أو ثاني قيمة من اليسار من النوع Path لكي تستطيع إنتاج كائن Path من التعبير البرمجي. هذه هي آلية العمل التي تتبعها بايثون للحصول على كائن Path النهائي: الشكل 2: آلية تفسير المسارات مع العامل / إذا ظهرت رسالة الخطأ TypeError: unsupported operand type(s) for /: 'str' and 'str' فهذا يعني أن علينا وضع الكائن Path في الطرف الأيسر من التعبير البرمجي. يستبدل العامل / الدالةَ os.path.join() التي يمكنك معرفة المزيد عنها من التوثيق الرسمي https://docs.python.org/3/library/os.path.html#os.path.join. مجلد العمل الحالي يملك كل برنامج تشغله على حاسوب ما يسمى «مجلد العمل الحالي» current working directory أو اختصارًا cwd؛ تفترض بايثون أن أي ملفات أو مسارات لا تبدأ بالمجلد الجذر هي موجودة في مجلد العمل الحالي. ملاحظة: صحيح أننا نقول «مجلد» ترجمةً لكلمة directory التي تعني «موجِّه»، لكنها شائعة بين المستخدمين العرب أكثر؛ ولا أحد يقول current working folder. أغلبية الاصطلاحات البرمجية تستعمل directory بدلًا من folder، لذا ستجد هذه الكلمة مستعملةً في أسماء الدوال المشروحة تاليًا. يمكننا الحصول على سلسلة نصية تمثل مجلد العمل الحالي باستخدام Path.cwd()، ويمكن تغييرها باستخدام os.chdir(): >>> from pathlib import Path >>> import os >>> Path.cwd() WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37')' >>> os.chdir('C:\\Windows\\System32') >>> Path.cwd() WindowsPath('C:/Windows/System32') لاحظ أن مجلد العمل الحالي هو C:\Users\Al\AppData\Local\Programs\Python\Python37 لذا إذا استعملنا اسم الملف project.docx فإنه سيشير إلى المسار C:\Users\Al\AppData\Local\Programs\Python\Python37\project.docx. حينما بدلنا مجلد العمل الحالي إلى C:\Windows\System32 فسيفسر project.docx إلى المسار C:\Windows\System32\project.docx. ستظهر بايثون رسالة خطأ حين محاولة تغيير مجلد العمل الحالي إلى مجلد غير موجود: >>> os.chdir('C:/ThisFolderDoesNotExist') Traceback (most recent call last): File "<stdin>", line 1, in <module> FileNotFoundError: [WinError 2] The system cannot find the file specified: 'C:/ThisFolderDoesNotExist' لا توجد دالة في pathlib لتغيير مجلد العمل الحالي، لأن تغييره أثناء تشغيل البرنامج قد يسبب علل برمجية نحن في غنى عنها. الدالة os.getcwd() هي الطريقة القديمة للحصول على سلسلة نصية تمثل مجلد العمل الحالي. مجلد المنزل يمتلك جميع المستخدمون مجلدًا خاصًا بهم يسمى مجلد المنزل home directory، ويمكننا الحصول على كائن Path لمجلد المنزل باستدعاء Path.home(): >>> Path.home() WindowsPath('C:/Users/Al') توجد مجلدات المنزل عادةً في مكان محدد يختلف حسب نظام تشغيلك: في ويندوز تكون في C:\Users. في ماك تكون في /Users. في لينكس تكون في /home. من شبه المؤكد أن سكربتات بايثون التي تكتبها ستمتلك أذونات القراءة والكتابة في مجلد المنزل، لذا من المستحسن أن تضع الملفات التي ستعالجها عبر بايثون فيه. المسارات النسبية والمسارات المطلقة هنالك طريقتان لتحديد مسار ملف: مسار مطلق absolute path، الذي يبدأ من المجلد الجذر. مسار نسبي relative path، الذي يبدأ من مجلد العمل الحالي للبرنامج. هنالك النقطة . والنقطتان .. حين التعامل مع المسارات، وهي ليست مجلدات حقيقة ولكنها أسماء خاصة، إذا تمثل النقطة . المجلد الحالي، بينما النقطتان .. تمثل المجلد الأب. يوضح الشكل 3 بعض الملفات والمجلدات ويكون مجلد العمل الحالي فيه هو C:\olive، وفيه مسارات الملفات والمجلدات كلها المطلقة والنسبية. الشكل 3: المسارات المطلقة والنسبية لاحظ أن .\ في بداية المسارات النسبية اختيارية، إذ يشير .\spam.txt و spam.txt إلى نفس الملف. إنشاء مجلدات جديدة باستخدام الدالة os.makedires() يمكن لبرامجك إنشاء مجلدات جديدة باستخدام الدالة os.makedires(): >>> import os >>> os.makedirs('C:\\delicious\\walnut\\waffles') سينتشئ المثال السابق المجلد C:\delicious وبداخله المجلد walnut وبداخله المجلد waffles. فالدالة os.makedires() ستنشِئ أي مجلدات لازمة غير موجودة مسبقًا. الشكل 4: ناتج تنفيذ os.makedirs('C:\delicious\walnut\waffles') لإنشاء مجلد من كائن Path فيمكننا استدعاء التابع mkdir()، فمثلًا سأنشِئ المجلد spam في مجلد المنزل في حاسوبي: >>> from pathlib import Path >>> Path(r'C:\Users\Al\spam').mkdir() لاحظ أن التابع mkdir() يستطيع إنشاء مجلد واحد فقط، ولن ينشِئ مجلدات فرعية كما في الدالة os.makedirs(). التعامل مع المسارات النسبية والمطلقة توفر الوحدة pathlib توابع للتحقق إن كان أحد المسارات نسبيًا أو مطلقًا، وتستطيع إعادة المسار المطلق من مسارٍ نسبي. سيعيد استدعاء التابع is_absolute() على كائن Path القيمة True إذا كان يمثل مسارًا مطلقًا أو False إذا كان يمثل مسارًا نسبيًا. تذكر أن تستعمل مسارات موجودة في حاسوبك في الأمثلة القادمة: >>> Path.cwd() WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37') >>> Path.cwd().is_absolute() True >>> Path('spam/olive/eggs').is_absolute() False للحصول على مسار مطلق من مسار نسبي يمكننا وضع Path.cwd() / قبل كائن Path، فحينما نقول «مسار نسبي» فهذا يعني أن المسار منسوب إلى مجلد العمل الحالي: >>> Path('my/relative/path') WindowsPath('my/relative/path') >>> Path.cwd() / Path('my/relative/path') WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37/my/relative/ path') أما إذا كان المسار النسبي منسوب إلى مسار مختلف عن مجلد العمل الحالي، فعلينا تبديل Path.cwd() إلى ذاك المسار، ففي المثال الآتي سنحصل على المسار النسبي نسبةً إلى مجلد المنزل الخاص بنا بدلًا من مجلد العمل الحالي: >>> Path('my/relative/path') WindowsPath('my/relative/path') >>> Path.home() / Path('my/relative/path') WindowsPath('C:/Users/Al/my/relative/path') تمتلك الوحدة os.path عددًا من الدوال المفيدة المتعلقة بالمسارات النسبية والمطلقة: ستعيد os.path.abspath(path) سلسلةً نصية فيها المسار المطلق للوسيط الممرر إليها، وهذه أسهل طريقة لتحويل مسار نسبي إلى مسار مطلق. ستعيد os.path.isabs(path) القيمة True إن كان الوسيط الممرر مسارًا مطلقًا و False إذا كان مسارًا نسبيًا. ستعيد os.path.relpath(path, start) سلسلةً نصيةً للمسار النسبي للمسار path بدءًا من المسار start. إذا لم نوفر المعامل start فسيستعمل مجلد العمل الحالي بدلًا منه. لنجرب الدوال السابقة: >>> os.path.abspath('.') 'C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37' >>> os.path.abspath('.\\Scripts') 'C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37\\Scripts' >>> os.path.isabs('.') False >>> os.path.isabs(os.path.abspath('.')) True ولمّا كان المسار C:\Users\Al\AppData\Local\Programs\Python\Python37 هو مجلد العمل الحالي حين استدعاء الدالة os.path.abspath()، فسيكون المجلد . (نقطة واحدة) هو المسار المطلق لمجلد العمل الحالي 'C:\\Users\\Al\\AppData\\Local\\Programs\\Python\\Python37'. لنجرب الدالة os.path.relpath() في الصدفة التفاعلية: >>> os.path.relpath('C:\\Windows', 'C:\\') 'Windows' >>> os.path.relpath('C:\\Windows', 'C:\\spam\\eggs') '..\\..\\Windows' إذا امتلك المسار النسبي نفس الأب لمجلد العمل الحالي كما في 'C:\\Windows' و 'C:\\spam\\eggs' فيمكن استخدام النقطتين .. للوصول إلى المجلد الأب. الحصول على أقسام المسار يمكننا استخلاص مختلف أقسام المسار عبر خاصيات الكائن Path، وهذا يفيدنا في حال أردنا إنشاء مسار جديد اعتمادًا على مسار ملف موجود مسبقًا. الشكل التالي يوضح هذه الخاصيات: الشكل 5: أجزاء المسار (ويندوز في الأعلى، ولينكس أو ماك في الأسفل) تتألف مسارات الملفات من الأقسام الآتية: المرساة anchor وهي المجلد الجذر root directory في نظام الملفات. المحرك drive في ويندوز وهو حرف واحد يشير إلى القرص المستخدم. الأب parent وهو مسار المجلد الذي يحتوي على الملف. اسم الملف name وهو يتألف من الاسم الأساسي stem والامتداد أو اللاحقة suffix. لاحظ أن كائنات Path تمتلك الخاصية drive في ويندوز، لكنها غير موجودة في ماك أو لينكس. لاحظ أيضًا أن الخاصية drive لا تتضمن أول خط مائل خلفي. لنجرب هذه الخاصيات على أحد المسارات: >>> p = Path('C:/Users/Al/spam.txt') >>> p.anchor 'C:\\' >>> p.parent # لن يعيد سلسلة نصية بل كائن Path WindowsPath('C:/Users/Al') >>> p.name 'spam.txt' >>> p.stem 'spam' >>> p.suffix '.txt' >>> p.drive 'C:' ستعيد هذه الخاصيات سلاسل نصية باستثناء الخاصية parent التي ستعيد كائن Path آخر. تمنحنا الخاصية parents (التي تختلف عن الخاصية parent السابقة) وصولًا إلى كائنات Path للمجلدات الأب مع فهرس رقمي: >>> Path.cwd() WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python37') >>> Path.cwd().parents[0] WindowsPath('C:/Users/Al/AppData/Local/Programs/Python') >>> Path.cwd().parents[1] WindowsPath('C:/Users/Al/AppData/Local/Programs') >>> Path.cwd().parents[2] WindowsPath('C:/Users/Al/AppData/Local') >>> Path.cwd().parents[3] WindowsPath('C:/Users/Al/AppData') >>> Path.cwd().parents[4] WindowsPath('C:/Users/Al') >>> Path.cwd().parents[5] WindowsPath('C:/Users') >>> Path.cwd().parents[6] WindowsPath('C:/') تمتلك الوحدة os.path القديمة دوال مشابهة لما سبق للحصول على مختلف أقسام المسارات كسلسلة نصية. ستعيد الدالة os.path.dirname(path) سلسلةً نصيةً فيها كل ما يسبق آخر خط مائل في المعامل path، بينما ستعيد os.path.basename(path) سلسلةً نصيةً فيها كل ما يلي آخر خط مائل في المعامل path. الشكل 6 يوضح مخرجات الدالتين السابقتين: الشكل 6: اسم المجلد dirname واسم الملف basename في الوحدة os.path لنجربها عمليًا في الطرفية التفاعلية: >>> calcFilePath = 'C:\\Windows\\System32\\calc.exe' >>> os.path.basename(calcFilePath) 'calc.exe' >>> os.path.dirname(calcFilePath) 'C:\\Windows\\System32' إذا احتجت إلى اسم المجلد واسم الملف معًا، فيمكنك استدعاء الدالة os.path.split() للحصول على صف فيه سلسلتين نصيتين كما يلي: >>> calcFilePath = 'C:\\Windows\\System32\\calc.exe' >>> os.path.split(calcFilePath) ('C:\\Windows\\System32', 'calc.exe') لاحظ أنك تستطيع إنشاء نفس الصف السابق باستدعاء الدالتين os.path.dirname() و os.path.basename() بنفسك: >>> (os.path.dirname(calcFilePath), os.path.basename(calcFilePath)) ('C:\\Windows\\System32', 'calc.exe') لكن استخدام os.path.split() سيوفر عليك بعض الوقت أحيانًا. لاحظ أن الدالة os.path.split() لا تأخذ مسار أحد الملفات وتعيد قائمةً من السلاسل النصية تمثل كل مجلد، وإنما علينا استخدام الدالة split() الخاصة بالسلاسل النصية وتقسيم المسار عند كل فاصل os.sep (لاحظ أن الفاصل sep موجود في os وليس os.path). يتمثل المتغير os.sep الفاصل بين المجلدات في نظام التشغيل المستخدم، وهو '\\' في ويندوز و '/' في ماك ولينكس: >>> calcFilePath.split(os.sep) ['C:', 'Windows', 'System32', 'calc.exe'] سعيد المثال السابق جميع أقسام المسار كقائمة من السلاسل النصية. سيكون أول عنصر في القائمة المعادة في أنظمة ماك ولينكس هو سلسلة نصية فارغة: >>> '/usr/bin'.split(os. sep) ['', 'usr', 'bin'] الحصول على حجم ملف ومحتويات مجلد بعد أن تعلمنا كيف نتعامل مع مسارات الملفات والمجلدات، يمكننا أن نبدأ بجمع المعلومات حولها. توفر الوحدة os.path ما يلزم لمعرفة الحجم التخزيني لملفٍ ما بالبايت، أو للملفات والمجلدات الموجودة في مجلد معين. استدعاء os.path.getsize(path) سيعيد الحجم التخزيني للملف الموجود في المسار path بالبايت. استدعاء os.listdir(path) سيعيد قائمة list فيها أسماء كل الملفات الموجودة في المسار path، لاحظ أننا استعملنا هنا الوحدة os وليس os.path. لنجرب هذه الدوال في الطرفية التفاعلية: >>> os.path.getsize('C:\\Windows\\System32\\calc.exe') 27648 >>> os.listdir('C:\\Windows\\System32') ['0409', '12520437.cpx', '12520850.cpx', '5U877.ax', 'aaclient.dll', --snip-- 'xwtpdui.dll', 'xwtpw32.dll', 'zh-CN', 'zh-HK', 'zh-TW', 'zipfldr.dll'] كما هو واضح، حجم الملف calc.exe في حاسوبي هو 27,648 بايت، ولدي الكثير من الملفات في المسار C:\Windows\system32، وإذا أردت الحصول على الحجم التخزيني لكل الملفات في هذا المجلد فسأستخدم os.path.getsize() و os.listdir() معًا. >>> totalSize = 0 >>> for filename in os.listdir('C:\\Windows\\System32'): totalSize = totalSize + os.path.getsize(os.path.join('C:\\Windows\\System32', filename)) >>> print(totalSize) 2559970473 سأمر بحلقة التكرار على كل ملف موجود في المجلد C:\Windows\System32 وسأزيد قيمة المتغير totalSize بمقدار الحجم التخزيني لكل ملف، لاحظ أنه حين استدعاء os.path.getsize() فنستخدم os.path.join() لإضافة اسم المجلد إلى اسم الملف الحالي. ستضاف القيمة العددية المعادة من os.path.getsize() إلى قيمة totalSize، وبعد إنهاء المرور على جميع الملفات فسنطبع قيمة totalSize لمعرفة حجم المجلد C:\Windows\System32 التخزيني. الحصول على قائمة من الملفات التي تطابق نمطًا معينًا باستخدام glob() إذا أردت العمل على ملفات محددة فمن الأسهل استخدام التابع glob() بدلًا من listdir()، إذ تملك كائنات Path التابع glob() للحصول على قائمة الملفات الموجودة داخل مجلد وفقًا لنمط يسمى Glob pattern، والتي تعرف أيضًا بالمحارف البديلة wildcard characters وهي نسخة مبسطة من التعابير النمطية التي يشيع استخدامها في سطر الأوامر (وتسمى هناك بالتوسعات expansion). يعيد التابع glob() كائنًا مولدًا generator object (وهو خارج عن سياق هذا الكتاب)، الذي يمكننا تمريره إلى الدالة list() لتسهيل التعامل معه: >>> p = Path('C:/Users/Al/Desktop') >>> p.glob('*') <generator object Path.glob at 0x000002A6E389DED0> >>> list(p.glob('*')) [WindowsPath('C:/Users/Al/Desktop/1.png'), WindowsPath('C:/Users/Al/ Desktop/22-ap.pdf'), WindowsPath('C:/Users/Al/Desktop/cat.jpg'), --snip-- WindowsPath('C:/Users/Al/Desktop/zzz.txt')] رمز النجمة * يعني "مجموعة من أي نوع من المحارف"، وبالتالي سيعيد p.glob('*') مولدًا فيه جميع الملفات الموجودة في المسار المخزن في p. وكما في التعابير النمطية، يمكننا كتابة أنماط معقدة بعض الشيء: >>> list(p.glob('*.txt') # قائمة بكل الملفات النصية [WindowsPath('C:/Users/Al/Desktop/foo.txt'), --snip-- WindowsPath('C:/Users/Al/Desktop/zzz.txt')] سيعيد النمط '*.txt' كل الملفات التي تبدأ بأي مجموعة من المحارف طالما أنها تنتهي بالسلسلة النصية '.txt'، وهو عادةً امتداد الملفات النصية. أما رمز إشارة الاستفهام ? فيعني أي محرف واحد: >>> list(p.glob('project?.docx') [WindowsPath('C:/Users/Al/Desktop/project1.docx'), WindowsPath('C:/Users/Al/ Desktop/project2.docx'), --snip-- WindowsPath('C:/Users/Al/Desktop/project9.docx')] التعبير 'project?.docx' سيعيد 'project1.docx' أو 'project5.docx'، لكنه لن يطابق 'project10.docx' لأن رمز علامة الاستفهام ? سيطابق محرفًا واحدًا فقط، لذا لن يستطيع مطابقة محرفين '10'. يمكنك أن تستعمل رمز النجمة وعلامة الاستفهام معًا لإنشاء تعابير مخصصة مثل: >>> list(p.glob('*.?x?') [WindowsPath('C:/Users/Al/Desktop/calc.exe'), WindowsPath('C:/Users/Al/ Desktop/foo.txt'), --snip-- WindowsPath('C:/Users/Al/Desktop/zzz.txt')] التعبير '*.?x?' سيعيد جميع الملفات التي لها أي اسم لكنها تنتهي بلاحقة تتألف من 3 محارف، والمحرف الوسط بينها هو 'x'. يسهل علينا التابع glob() تحديد الملفات التي نريدها باختيار الملفات التي يطابق اسمها نمطًا معينًا. سنستخدم هنا الحلقة for للمرور على المولد generator المولد من التابع glob(): >>> p = Path('C:/Users/Al/Desktop') >>> for textFilePathObj in p.glob('*.txt'): ... print(textFilePathObj) # طباعة كائن Path كسلسلة نصية ... # معالجة الملف النصي ... C:\Users\Al\Desktop\foo.txt C:\Users\Al\Desktop\spam.txt C:\Users\Al\Desktop\zzz.txt إذا أردت إجراء نفس العملية على جميع الملفات الموجودة في المجلد، فيمكنك أن تستعمل حينها os.listdir(p) أو p.glob('*'). التأكد من المسارات ستفشل أغلبية دوال بايثون التي تتعامل مع الملفات إذا أعطيناها مسارًا غير موجود، لكن لحسن الحظ هنالك توابع لكائنات Path للتحقق أن المسار المعطى موجود فعلًا، وهل هو ملف أم مجلد. فعلى فرض أن المتغير p يشير إلى كائن Path، فبالتالي سيعيد استدعاء: p.exists() القيمة True إذا كان المسار موجودًا، أو False إن لم يكن موجودًا. p.is_file() القيمة True إن كان المسار موجودًا ويشير إلى ملف، أو False خلاف ذلك. p.is_dir() القيمة True إذا كان المسار موجودًا ويشير إلى مجلد، أو False خلاف ذلك. دعني أجرب هذه التوابع على حاسوبي الشخصي: >>> winDir = Path('C:/Windows') >>> notExistsDir = Path('C:/This/Folder/Does/Not/Exist') >>> calcFile = Path('C:/Windows /System32/calc.exe') >>> winDir.exists() True >>> winDir.is_dir() True >>> notExistsDir.exists() False >>> calcFile.is_file() True >>> calcFile.is_dir() False إذا كنت تستعمل ويندوز فيمكنك أن تتحقق إن كان قرص التخزين المؤقت («الفلاشة») موصولًا إلى الحاسوب عبر التابع exists()، فمثلًا لو أردت التحقق أن القرص المسمى D:`` موجود على حاسوبي: >>> dDrive = Path('D:/') >>> dDrive.exists() False يبدو أنني نسيت وصل القرص إلى الحاسوب. الوحدة القديمة os.path تستطيع إنجاز نفس المهمة باستخدام os.path.exists(path) و os.path.isfile(path) و os.path.isdir(path)، التي تعمل ملف مكافأتها في كائنات Path. وبدءًا من الإصدار بايثون 3.6 أصبحت تقبل هذه التوابع كائنات Path إضافةً إلى سلاسل نصية تحتوي على مسارات الملفات. عملية قراءة الملفات والكتابة إليها بعد أن تصبح مرتاحًا بالتعامل مع المجلدات والمسارات النسبية، فستتمكن من تحديد موقع الملفات التي تريد قراءتها أو الكتابة إليها. الدوال التي سنشرحها في الأقسام الآتية تعمل على الملفات النصية البسيطة، التي هي ملفات تحتوي على محارف نصية دون أن تحتوي على معلومات التنسيقات مثل الخطوط أو الألوان أو خلاف ذلك، ومن الأمثلة على الملفات النصية البسيطة هي ملفات txt أو py التي تحتوي على شيفرات بايثون. يمكن فتح هذه الملفات باستخدام المفكرة Notepad في ويندوز، أو TextEdit في ماك، أو Kate أو Gedit في لينكس. وتستطيع أن تفتح هذه الملفات في برامجك وتعاملها كسلاسل نصية عادية. الملفات الثنائية هي نوع آخر من الملفات، مثل الملفات التي تنتجها برامج إنشاء العروض التقديمية أو ملفات PDF أو الصور أو الملفات التنفيدية …إلخ. وإذا فتحتها بالمفكرة مثلًا فستجد أنها مجموعة من الرموز غير المفهومة: الشكل 7: برنامج calc.exe مفتوح في المفكرة ولأن كل نوع من الملفات الثنائية يجري التعامل معه بطريقة مختلفة، فلن ندخل بتفاصيل تعديل الملفات الثانية مباشرةً في هذا الكتاب؛ وهنالك وحدات تسهل التعامل معها مثل الوحدة shelve التي ستتعامل معها لاحقًا في هذا المقال. التابع read_text() في الوحدة pathlib تعيد سلسلةً نصية فيها كل محتويات الملف النصي، بينما يكتب التابع write_text() ما يمرر إليه إلى ملف نصي جديد (أو يعيد الكتابة فوق ملف موجود مسبقًا): >>> from pathlib import Path >>> p = Path('spam.txt') >>> p.write_text('Hello, world!') 13 >>> p.read_text() 'Hello, world!' سننشئ ملفًا باسم spam.txt فيه المحتويات 'Hello, world!'، لاحظ أن التابع write_text() قد أعاد الرقم 13 الذي يشير إلى عدد المحارف التي كتبت إلى الملف (ونتجاهل تخزين هذا الرقم عادةً)، ويقرأ التابع read_text() محتويات الملف الجديد ويعيدها على شكل سلسلة نصية. تذكر أن توابع الكائن Path توفر الأمور الأساسية في التعامل مع الملفات؛ والطريقة الأشيع لقراءة الملفات تكون عبر الدالة open() والكائن File. هنالك خطوات ثلاث لقراءة أو كتابة الملفات في بايثون: استدعاء الدالة open() لإعادة الكائن File. استدعاء التابع read() أو write() على الكائن File. إغلاق الملف باستدعاء التابع close() على الكائن File. سنشرح هذه الخطوات في الأقسام الآتية. فتح الملفات عبر الدالة open() لفتح ملف باستخدام الدالة open() فنمرر سلسلة نصية تحتوي على مسار الملف الذي نريد فتحه؛ والذي يكون إما مسارًا مطلقًا absolute أو نسبيًا relative. ستعيد الدالة open() كائنًا من النوع File. لنجربها بإنشاء ملف نصي بسيط اسمه hello.txt باستخدام المفكرة أو أي محرر نصوص، وكتابة Hello, World! داخلها وحفظه في مجلد المنزل، ثم كتابة ما يلي في الطرفية التفاعلية: >>> helloFile = open(Path.home() / 'hello.txt') تقبل الدالة open() السلاسل النصية أيضًا، فإذا كنت تستخدم ويندوز فاكتب: >>> helloFile = open('C:\\Users\\your_home_folder\\hello.txt') أما إذا كان نظامك ماك: >>> helloFile = open('/Users/your_home_folder/hello.txt') تذكر أن تبدل الكلمة yourhomefolder باسم المستخدم في حاسوبك، فلو كان hsoub مثلًا فستدخل 'C:\\Users\\hsoub\\hello.txt' في ويندوز؛ لاحظ أن الدالة open() أصبحت تقبل كائنات Path بدءًا من إصدار بايثون 3.6، وكان عليك استخدام السلاسل النصية فقط فيما سبق. ستفتح هذه الأوامر الملف في وضع "قراءة الملفات النصية" أو اختصارًا "وضع القراءة"، وحينما يفتح الملف في وضع القراءة فتسمح لنا بايثون بقراءة الملفات من الملف فقط، ولا يمكنك أن تكتب عليه أو تعدله بأي شكل. لكن إذا أردت أن تحدد أنك تريد فتح الملف بوضع القراءة بوضوح فمرر القيمة 'r' كثاني وسيط إلى الدالة open()، أي أن open('/Users/Al/hello.txt', 'r') و open('/Users/Al/hello.txt') متكافئتان تمامًا. سيعيد استدعاء الدالة open() كائن File، ويمثل كائن File ملفًا على حاسوبك، وهو نوع مختلف من القيم في بايثون مثله كمثل القوائم أو القواميس التي تعرفت عليها مسبقًا. خزنّا في المثال السابق كائن File في المتغير helloFile، ويمكنك أن تستسخدمه لأي عمليات قراءة أو كتابة مستقبلًا باستدعاء التوابع المناسبة على الكائن File المخزن في المتغير helloFile. قراءة محتويات الملفات أصبح لدينا الآن كائن File، ويمكننا أن نبدأ بقراءة كامل محتويات الملف كسلسلة نصية، وذلك عبر التابع read()، لنكمل مثالنا السابق الذي فيه المتغير helloFile بكتابة ما يلي: >>> helloContent = helloFile.read() >>> helloContent 'Hello, world!' لو تخيلت أن جميع محتويات الملف هي سلسلة نصية كبيرة، فإن التابع read() يعيد تلك السلسلة النصية. بدلًا من ذلك، يمكنك استخدام التابع readlines() للحصول على قائمة list فيها سلاسل نصية من الملف، وكل سلسلة نصية تمثل سطرًا فيه، فمثلًا لو كان لدينا ملف اسمه sonnet29.txt في نفس المجلد الذي فيه الملف hello.txt وكتبنا فيه النص الآتي: When, in disgrace with fortune and men's eyes, I all alone beweep my outcast state, And trouble deaf heaven with my bootless cries, And look upon myself and curse my fate, تأكد أنك قد فصلت بين الأسطر الأربعة السابقة كلٌ في سطر مختلف، ثم أدخل ما يلي في الطرفية التفاعلية: >>> sonnetFile = open(Path.home() / 'sonnet29.txt') >>> sonnetFile.readlines() [When, in disgrace with fortune and men's eyes,\n', ' I all alone beweep my outcast state,\n', And trouble deaf heaven with my bootless cries,\n', And look upon myself and curse my fate,'] لاحظ أن كل عنصر من عناصر القائمة (باستثناء آخر واحد) هو سلسلة نصية تنتهي بمحرف السطر الجديد \n. يكون في العادة من الأسهل التعامل مع قائمة من السلاسل النصية بدل سلسلة نصية واحدة كبيرة. الكتابة إلى الملفات تسمح لنا بايثون بكتابة محتوى إلى الملفات بشكل يشبه "كتابة" الدالة print() للسلاسل النصية إلى الشاشة. لا يمكنك أن تكتب إلى ملف قد فتحته بوضع القراءة، لذا عليك أن تفتحه بوضع "الكتابة على الملفات النصية" أو "الإضافة إلى الملفات النصية" واختصارًا وضع الكتابة أو وضع الإضافة. وضع الكتابة سيعيد الكتابة فوق ملف موجود ويبدأ من الصفر، كما لو أعدنا إسناد قيمة جديدة إلى متغير. يمكنك فتحت الملف بوضع الكتابة بتمرير 'w' كثاني وسيط إلى الدالة open(). أما وضع الإضافة فسيضيف النص إلى نهاية ملف موجود مسبقًا، كما لو أضفنا عنصرًا إلى قائمة موجودة في متغير بدلًا من إعادة الكتابة. مرر 'a' كثاني وسيط إلى الدالة open() لفتح الملف في وضع الإضافة. إذا لم يكن الملف الممرر إلى الدالة open() موجودًا فسينشأ ملف جديد في وضع الكتابة والإسناد. لا تنسَ أن تستدعي الدالة close() قبل إعادة فتح الملف مجددًا. لنجرب هذه المفاهيم معًا بكتابة: >>> oliveFile = open('olive.txt', 'w') >>> oliveFile.write('Hello, world!\n') 13 >>> oliveFile.close() >>> oliveFile = open('olive.txt', 'a') >>> oliveFile.write('Olive is not a vegetable.') 25 >>> oliveFile.close() >>> oliveFile = open('olive.txt') >>> content = oliveFile.read() >>> oliveFile.close() >>> print(content) Hello, world! Olive is not a vegetable. في البداية فتحنا الملف becon.txt بوضع الكتابة، ولعدم وجود الملف becon.txt بعد فإن بايثون تنشئه لنا، واستدعاء التابع write() وتمرير السلسلة النصية 'Hello, world! /n' سيؤدي إلى كتابتها إلى الملف وإعادة عدد المحارف المكتوبة بما فيها محرف السطر الجديد. ثم أغلقنا في النهاية الملف. لإضافة نص إلى المحتويات الموجودة لملف بدلًا من استبداله، فسنفتح الملف في وضع الإضافة، وأضفنا السلسلة النصية 'Olive is not a vegetable.' إلى الملف وأغلقناه. في النهاية نريد أن نطبع محتويات الملف فاستخدمنا الدالة open() لفتح الملف في الوضع الافتراضي وهو وضع القراءة، وخزنّا محتويات الملف في المتغير content ثم أغلقنا الملف وطبعنا محتوياته. لاحظ أن التابع write() لا يضيف محرف السطر الجديد إلى نهاية السلسلة النصية مثلما تفعل الدالة print() لذا عليك أن تضيفه بنفسك. تذكر أنك تستطيع تمرير كائن Path إلى الدالة open() بدلًا من سلسلة نصية بسيطة بدءًا من إصدار بايثون 3.6. حفظ المتغيرات باستخدام الوحدة shelve يمكنك أن تحفظ المتغيرات الموجودة في برامجك إلى ملفات ثنائية باستخدام الوحدة shelve، وبالتالي يمكنك أن تستعيد البيانات إلى المتغيرات من ملف مخزن على حاسوبك. تسمح لك الوحدة shelve بحفظ واستعادة البيانات إلى برنامجك، فلو ضبطتَ مثلًا بعض المتغيرات في برنامجك، يمكنك أن تحفظ تلك البيانات على الرف (shelf، ومن هنا أتى اسم الوحدة) ثم تستعيد تلك القيم حينما تشغل برنامجك مجددًا. >>> import shelve >>> shelfFile = shelve.open('mydata') >>> cats = ['Zophie', 'Pooka', 'Simon'] >>> shelfFile['cats'] = cats >>> shelfFile.close() لقراءة وكتابة البيانات باستخدام الوحدة shelve عليك أن تستوردها أولًا، ثم تستدعي shelve.open() وتمرر إليها اسم الملف ثم تخزن القيم. لاحظ أنك تستطيع التعامل مع القيم كما لو أنها قاموس. بعد أن تنتهي لا تنسَ استدعاء close(). أنشأنا في المثال السابق القائمة cats وكتبنا shelfFile['cats'] = cats لتخزين القائمة في shelfFile كقيمة مرتبطة مع المفتاح 'cat' (كما في مفاتيح القواميس). ثم استدعينا close() على shelfFile، لاحظ أنه بدءًا من إصدار بايثون 3.7 سيكون عليك تمرير أسماء الملفات إلى open() كسلاسل نصية، ولا يمكنك تمرير كائن Path. بعد تشغيل الشيفرة السابقة في ويندوز، ستجد ثلاثة ملفات جديدة في المجلد وهي mydata.bak و mydata.dat و mydata.dir؛ أما على ماك فسينشَأ ملف واحد باسم mydata.db. تحتوي هذه الملفات الثنائية على البيانات التي خزنتها «على الرف»؛ ولا تهمك صيغة هذه الملفات الثانية، فكل ما تحتاج إلى معرفته هو ما تفعله الوحدة shelve وليس كيف تفعل ذلك. وهذه الوحدة تريح رأسك من القلق حول كيفية تخزين البرنامج للبيانات. يمكن لبرامجك استخدام الوحدة shelve لإعادة فتح الملف والحصول على البيانات، ولا حاجة إلى تحديد إن كنت تريد قراءة البيانات أم كتابتها، ففتح الملف يسمح بكلي العمليتين. >>> shelfFile = shelve.open('mydata') >>> type(shelfFile) <class 'shelve.DbfilenameShelf'> >>> shelfFile['cats'] ['Zophie', 'Pooka', 'Simon'] >>> shelfFile.close() فتحنا في المثال السابق الملف الذي وضعنا فيه البيانات، وتأكدنا أن التخزين سليم إذ أعاد shelfFile['cats'] نفس القائمة التي خزناها سابقًا، وفي النهاية أغلقنا الملف close(). هنالك تابعان اسمهما keys() و values() تشبه تلك الموجودة في القواميس التي تعيد قيمةً شبيهة بالقوائم list-like للمفاتيح والقيم الموجودة في الرف. ولأن هذه التوابع تعيد قيمًا شبيهة بالقوائم وليست قوائم حقيقية فيجب عليك تمريرها إلى الدالة list() للحصول على قائمة حقيقية تتعامل معها. >>> shelfFile = shelve.open('mydata') >>> list(shelfFile.keys()) ['cats'] >>> list(shelfFile.values()) [['Zophie', 'Pooka', 'Simon']] >>> shelfFile.close() الخلاصة أن الملفات النصية البسيطة مفيدة لتخزين البيانات النصية الأساسية، أما لو أردت حفظ بيانات من برنامج بايثون الذي كتبته، فيمكنك أن تستفيد من الوحدة shelve. حفظ المتغيرات مع الدالة pprint.pformat() إذا كنت تذكر في قسم «تجميل الطباعة» أن الدالة pprint.pprint() تطبع محتويات قائمة أو قاموس بتنسيق مخصص، بينما الدالة pprint.pformat() تنسق النص وتعيده بدلًا من طباعته مباشرةً. وصحيحٌ أن النص المعاد من هذه الدالة سيكون منسقًا تنسيقًا جميلًا لتسهيل قراءته، لكنه في الواقع منسق كما لو أنه شفيرة بايثون. لنقل مثلًا أن لديك قاموسًا مخزنًا في متغير وأردت حفظ هذا المتغير ومحتوياته للاستخدام مستقبلًا، فيمكنك أن تستفيد من الدالة pprint.pformat() لإعادة سلسلة نصية تكتبها إلى ملف .py ويمكنك أن تستورد هذا الملف في أي مرة تريد استخدام المتغير المخزن فيه. >>> import pprint >>> cats = [{'name': 'Zophie', 'desc': 'chubby'}, {'name': 'Pooka', 'desc': 'fluffy'}] >>> pprint.pformat(cats) "[{'desc': 'chubby', 'name': 'Zophie'}, {'desc': 'fluffy', 'name': 'Pooka'}]" >>> fileObj = open('myCats.py', 'w') >>> fileObj.write('cats = ' + pprint.pformat(cats) + '\n') 83 >>> fileObj.close() استوردنا في هذا المثال الوحدة pprint لكي نستطيع استخدام الدالة pprint.pformat()، ولدينا متغير cats فيه قائمة من القواميس؛ ولكي نحتفظ بالقائمة الموجودة في المتغير cats حتى بعد أن نغلق الصدفة التفاعلية فيمكننا استخدام الدالة pprint.pformat() لإعادته كسلسلة نصية، ثم بعد حصولنا على السلسلة النصية يمكننا كتابتها إلى ملف وليكن اسمه myCats.py. تذكر أن الوحدات التي تستوردها العبارة import هي سكربتات بايثون عادية؛ وعندما نحفظ السلسلة النصية المأخوذة من pprint.pformat() إلى ملف .py فيمكن اعتبار هذا الملف على أنه وحدة يمكن استيرادها مثل أي وحدات بايثون الأخرى. ولأن سكربتات بايثون هي ملفات نصية بسيط امتدادها .py فيمكن لبرامجك أن تولد برامج بايثون أخرى، ويمكنك استيراد تلك البرامج داخل برامجك: >>> import myCats >>> myCats.cats [{'name': 'Zophie', 'desc': 'chubby'}, {'name': 'Pooka', 'desc': 'fluffy'}] >>> myCats.cats[0] {'name': 'Zophie', 'desc': 'chubby'} >>> myCats.cats[0]['name'] 'Zophie' الفائدة من إنشاء ملف .py بسيط بدلًا من حفظ المتغيرات مع الوحدة shelve هو أن الناتج ملف نصي يمكن قراءته وتعديله من أي شخص بمحرر نصي بسيط. لكن لأغلبية حالات الاستخدام يكون من المناسب حفظ البيانات باستخدام الوحدة shelve، إذا لا يمكن كتابة القيم إلى ملفات نصية بسيطة إلا إذا كانت قيمًا بسيطة مثل الأعداد والسلاسل النصية والقوائم والقواميس، بينما لا تستطيع تخزين الكائنات مثل File أو غيره كنص بسيط. مشروع: توليد ملفات اختبارات عشوائية لنفترض أنك أستاذ مادة الجغرافية ولديك 35 طالبًا في صفحك، وتريد إجراء اختبار لعواصم الولايات الأمريكية؛ وأنت تعرف طلابك حق المعرفة وتدرك أن بعضهم سيحاول أن يغش، لذا تريد أن تغيّر ترتيب الأسئلة في كل اختبار لكي تكون فريدة مما يجعل من الصعب جدًا نقل الإجابات من طالب آخر. عمل هذه النماذج يدويًا يأخذ وقتًا وجهدًا وسيكون أمرًا مملًا، لكن ستساعدك مهاراتك في بايثون هنا. هذه هي وظيفة البرنامج: إنشاء 35 اختبار مختلف إنشاء 50 سؤال اختيار من إجابات متعددة لكل اختبار، بترتيب عشوائي توفير الإجابة الصحيحة وثلاث إجابات خطأ لكل سؤال مرتبة ترتيبًا عشوائيًا كتابة الاختبارات إلى 35 ملف نصي كتابة مفاتيح الإجابات الصحيحة إلى 35 ملف نصي هذا يعني أن الشيفرة عليها أن: تخزن أسماء الولايات في أمريكا وأسماء عواصمها في قاموس تستدعي open() و write() و close() لكل ملف اختبار وإجابات تستخدم random.shuffle() لترتيب الأسئلة والإجابات ترتيبًا عشوائيًا الخطوة 1: تخزين بيانات الاختبار في قاموس أول خطوة هي إنشاء بينة السكربت الأساسية وكتابة بيانات الاختبار. أنشِئ ملفًا باسم randomQuizGenerator.py وضع فيه المحتوى الآتي: #! python3 # randomQuizGenerator.py - إنشاء اختبارات مع أسئلة وإجابات عشوائية # مع تخزين مفتاح الحل. ➊ import random # بيانات الاختبار: أسماء الولايات الأمريكية وعواصمها. ➋ capitals = {'Alabama': 'Montgomery', 'Alaska': 'Juneau', 'Arizona': 'Phoenix', 'Arkansas': 'Little Rock', 'California': 'Sacramento', 'Colorado': 'Denver', 'Connecticut': 'Hartford', 'Delaware': 'Dover', 'Florida': 'Tallahassee', 'Georgia': 'Atlanta', 'Hawaii': 'Honolulu', 'Idaho': 'Boise', 'Illinois': 'Springfield', 'Indiana': 'Indianapolis', 'Iowa': 'Des Moines', 'Kansas': 'Topeka', 'Kentucky': 'Frankfort', 'Louisiana': 'Baton Rouge', 'Maine': 'Augusta', 'Maryland': 'Annapolis', 'Massachusetts': 'Boston', 'Michigan': 'Lansing', 'Minnesota': 'Saint Paul', 'Mississippi': 'Jackson', 'Missouri': 'Jefferson City', 'Montana': 'Helena', 'Nebraska': 'Lincoln', 'Nevada': 'Carson City', 'New Hampshire': 'Concord', 'New Jersey': 'Trenton', 'New Mexico': 'Santa Fe', 'New York': 'Albany', 'North Carolina': 'Raleigh', 'North Dakota': 'Bismarck', 'Ohio': 'Columbus', 'Oklahoma': 'Oklahoma City', 'Oregon': 'Salem', 'Pennsylvania': 'Harrisburg', 'Rhode Island': 'Providence', 'South Carolina': 'Columbia', 'South Dakota': 'Pierre', 'Tennessee': 'Nashville', 'Texas': 'Austin', 'Utah': 'Salt Lake City', 'Vermont': 'Montpelier', 'Virginia': 'Richmond', 'Washington': 'Olympia', 'West Virginia': 'Charleston', 'Wisconsin': 'Madison', 'Wyoming': 'Cheyenne'} # توليد 35 اختبار عشوائي. ➌ for quizNum in range(35): # TODO: إنشاء ملفات الاختبار والإجابات. # TODO: كتابة ترويسة الاختبار. # TODO: تغيير ترتيب الولايات. # TODO: المرور على 50 سؤال وتوليد إجابات عشوائية. لما كان هذا البرنامج يرتب الأسئلة والإجابات عشوائيًا، فعلينا استيراد الوحدة random ➊ لكي نستطيع استخدام الدوال الخاصة بها. يحتوي المتغير capitals ➋ على قاموس فيه أسماء الولايات الأمريكية كمفتاح وعاصمتها كقيمة. ولأننا نريد كتابة الشيفرة التي تولد ملفات الاختبار والإجابات (أضفناها على أنها TODO حاليًا) فسندخل إلى حلقة التكرار for بعدد 35 مرة ➌، ويمكننا تغيير الرقم وفق متطلبات البرنامج. الخطوة 2: إنشاء ملف الاختبار وتغيير ترتيب الأسئلة عشوائيا حان الوقت لبدء العمل على مهام TODO. ستكرر الشيفرة الموجودة داخل حلقة for بعدد 35 مرة، مرة لكل اختبار، فعلينا أن نفكر بكيفية إنشاء اختبار واحدة وسيكرر الأمر على البقية. بدايةً سنحتاج إلى إنشاء ملف الاختبار، ويجب أن يكون له اسم فريد، ويجب أن يحتوي على ترويسة قياسية فيها معلومات الاختبار ومكان ليضع الطالب اسمه وصفه وتاريخ اليوم. ثم ستكون هنالك قائمة الولايات بترتيب عشوائي، التي يمكن استخدامها لاحقًا لإنشاء الأسئلة والإجابات لكل اختبار. أضف الأسطر الآتية إلى ملف randomQuizGenerator.py: #! python3 # randomQuizGenerator.py - إنشاء اختبارات مع أسئلة وإجابات عشوائية # مع تخزين مفتاح الحل. --snip-- # توليد 35 اختبار عشوائي. for quizNum in range(35): # إنشاء ملف الاختبارات والإجابات ➊ quizFile = open(f'capitalsquiz{quizNum + 1}.txt', 'w') ➋ answerKeyFile = open(f'capitalsquiz_answers{quizNum + 1}.txt', 'w') # كتابة الترويسة ➌ quizFile.write('Name:\n\nDate:\n\nPeriod:\n\n') quizFile.write((' ' * 20) + f'State Capitals Quiz (Form{quizNum + 1})') quizFile.write('\n\n') # تغيير ترتيب الولايات states = list(capitals.keys()) ➍ random.shuffle(states) # TODO: المرور على 50 سؤال وتوليد إجابات عشوائية. ستكون أسماء ملفات الاختبارات على الشكل capitalsquiz<N>.txt حيث <N> هو رقم فريد لكل اختبار يأتي من quizNum الذي هو عدّاد حلقة for. سيخزن ملف مفاتيح الإجابات للاختبار capitalsquiz<N>.txt في ملف باسم capitalsquiz_answers<N>.txt. في كل دورة في حلقة for ستبدل قيمة {quizNum + 1} في f'capitalsquiz{quizNum + 1}.txt' و f'capitalsquiz_answers{quizNum + 1}.txt' برقم فريد، وستكون أسماء الملفات لأول مرور باسم capitalsquiz1.txt و capitalsquiz_answers1.txt. ستنشأ هذه الملفات حين استدعاء الدالة open() في السطرين ➊ و ➋ حين تمرير الوسيط الثاني 'w' لفتح الملفات في وضع الكتابة. العبارات write() في القسم ➌ تكتب ترويسة الاختبار التي يجب على الطالب أن يملأها؛ ثم سننشِئ قائمة عشوائية فيها أسماء الولايات الأمريكية باستخدام الدالة random.shuffle() في السطر ➍، التي تغير ترتيب القيم في أي قائمة تمرر إليها. الخطوة 3: إنشاء خيارات الإجابة علينا الآن توليد خيارات إجابات كل سؤال، والتي ستكون اختيار من متعدد من A إلى D، أي أننا سنحتاج إلى إنشاء حلقة for أخرى لتوليد محتوى لكل سؤال من الأسئلة الخميس. ثم ستكون هنالك حلقة for ثالثة داخلها لتوليد الإجابات المحتملة لكل سؤال. #! python3 # randomQuizGenerator.py - إنشاء اختبارات مع أسئلة وإجابات عشوائية # مع تخزين مفتاح الحل. --snip-- # المرور على الولايات الخمسين وتوليد سؤال لكل منها. for questionNum in range(50): # الحصول على الإجابات الصحيحة والخطأ. ➊ correctAnswer = capitals[states[questionNum]] ➋ wrongAnswers = list(capitals.values()) ➌ del wrongAnswers[wrongAnswers.index(correctAnswer)] ➍ wrongAnswers = random.sample(wrongAnswers, 3) ➎ answerOptions = wrongAnswers + [correctAnswer] ➏ random.shuffle(answerOptions) # TODO: كتابة السؤال والإجابات إلى ملف الاختبار # TODO: كتابة مفتاح الحل إلى ملف الحلول. من السهل الحصول على الجواب الصحيحة، فهو القيمة المخزنة في القاموس capitals ➊. ستمر حلقة التكرار على جميع الولايات الموجودة ضمن قائمة states المرتبة عشوائيًا، من states[0] حتى states[49]، ويجد كل ولاية في capitals ثم يخزن الجواب الصحيح في المتغير correctAnswer. عملية إنشاء قائمة بالإجابات الخطأ أصعب بقليل، عليك أولًا أن تنسخ جميع القيم في قاموس capitals ➋، ثم تحذف الجواب الصحيح ➌، ثم تأخذ ثلاث قيم عشوائية من القائمة ➍؛ وتسهل علينا الدالة random.sample() ذلك، وتأخذ وسيطين: الأول هو القائمة التي تريد الاختيار منها، والثاني هو عدد القيم التي تريدها. ستكون القائمة النهائية هي الإضافة الصحيحة إضافةً إلى إجابات خطأ ثلاثة ➎، ثم علينا جعل الإجابات عشوائية ➏ كيلا تكون الإجابة D هي الإجابة الصحيحة دومًا. الخطوة 4: كتابة المحتوى إلى ملفات الاختبارات والإجابات الصحيحة كل ما بقي فعله هو كتابة السؤال إلى ملف الاختبار، وكتابة الإجابة إلى ملف الإجابات الصحيحة. #! python3 # randomQuizGenerator.py - إنشاء اختبارات مع أسئلة وإجابات عشوائية # مع تخزين مفتاح الحل. --snip-- # المرور على الولايات الخمسين وتوليد سؤال لكل منها. for questionNum in range(50): --snip-- # كتابة السؤال والإجابات إلى ملف الاختبار. quizFile.write(f'{questionNum + 1}. What is the capital of {states[questionNum]}?\n') ➊ for i in range(4): ➋ quizFile.write(f" {'ABCD'[i]}. { answerOptions[i]}\n") quizFile.write('\n') # كتابة مفتاح الحل إلى ملف الحلول. ➌ answerKeyFile.write(f"{questionNum + 1}. {'ABCD'[answerOptions.index(correctAnswer)]}") quizFile.close() answerKeyFile.close() تمر حلقة for على الأعداد 0 إلى 3 وتكتب الجواب في القائمة answerOptions ➊، أما التعبير 'ABCD'[i] في ➋ أن السلسلة النصية 'ABCD' ستعامل كمصفوفة وستكون قيمها هي 'A' ثم 'B' ثم 'C' ثم 'D' وفقًا لدورة حلقة التكرار. في السطر الأخير ➌ سيعثر التعبير answerOptions.index(correctAnswer) على فهرس الإجابة الصحيحة في قائمة الإجابات المعشوائية، ثم ستكون نتيجة التعبير البرمجي 'ABCD'[answerOptions.index(correctAnswer)] هي حرف الجواب الصحيح الذي سيكتب في ملف الإجابات. بعد أن تشغل البرنامج فسيبدو الملف capitalsquiz1.txt كما يلي، مع الانتباه إلى أن الناتج سيكون مختلفًا عما عندك لأننا نأخذ القيم عشوائيًا: Name: Date: Period: State Capitals Quiz (Form 1) 1. What is the capital of West Virginia? A. Hartford B. Santa Fe C. Harrisburg D. Charleston 2. What is the capital of Colorado? A. Raleigh B. Harrisburg C. Denver D. Lincoln --snip-- وسيبدو ملف capitalsquiz_answers1.txt كما يلي: 1. D 2. C 3. A 4. C --snip-- مشروع: تحديث لمشروع الحافظة لنعد كتابة مشروع الحافظة من المقال السابع من هذه السلسلة حول كيفية معالجة النصوص، لنستعمل الوحدة shelve، إذ سيتمكن المستخدم من حفظ سلاسل نصية جديدة وتحميلها إلى الحافظة دون تعديل الشيفرة المصدرية للتطبيق. سنسمي مشروعنا mcb.pyw، فاختصار mcb هو multi-clipboard أي الحافظة المتعددة، والامتداد .pyw يعني أن بايثون لن تظهر نافذة الطرفية حين تشغيل البرنامج (راجع المقال الأول من السلسلة لمزيد من التفاصيل). سيحفظ البرنامج المحتويات النصية للحافظة تحت كلمة مفتاحية معينة، فمثلًا لو شغلت py mcb.pyw save spam فستحفظ المحتويات الحالية للحافظة مع الكلمة المفتاحية spam، ويمكن إعادة تحميل النص إلى الحافظة مجددًا بتشغيل py mcb.pyw spam، وإذا نسي المستخدم ما الكلمات المفتاحية التي استخدمها فيمكنه تشغيل py mcb.pyw list لنسخ قائمة فيها جميع الكلمات المفتاحية إلى الحافظة. هذه هي آلية عمل المشروع: التحقق من الوسيط الممرر عبر سطر الأوامر إذا كان save فستحفظ محتويات الحافظة إلى الكلمة المفتاحية المرسلة إذا كان list فستنسخ جميع الكلمات المفتاحية إلى الحافظة خلاف ذلك، سينسخ النص المرتبط بالكلمة المفتاحية إلى الحافظة. هذا يعني أن على البرنامج: قراءة وسائط سطر الأوامر من sys.argv. القراءة والكتابة إلى الحافظة. حفظ وتحميل ملف shelf. إذا كنت تستخدم ويندوز، فيمكنك ببساطة تشغيل السكربت من نافذة Run بإنشاء ملف mcb.bat فيه المحتوى الآتي: @pyw.exe C:\Python34\mcb.pyw %* الخطوة 1: البنية الأساسية وضبط عملية الحفظ والتحميل لنبدأ بكتابة الهيكل الأساسي للسكربت مع بعض التعليقات وضبط أساسي لعملية الحفظ والتحميل: #! python3 # mcb.pyw - حفظ وتحميل نصوص إلى الحافظة. ➊ # Usage: py.exe mcb.pyw save <keyword> - حفظ الحاوية إلى keyword. # py.exe mcb.pyw <keyword> -تحميل محتويات keyword إلى الحاوية. # py.exe mcb.pyw list - تحميل كل الكلمات المفتاحية إلى الحاوية. ➋ import shelve, pyperclip, sys ➌ mcbShelf = shelve.open('mcb') # TODO: حفظ محتويات الحاوية. # TODO: إظهار كل الكلمات المفتاحية والمحتوى. من الشائع أن نضع معلومات الاستخدام في تعليقات في أول الملف ➊، ففي حال نسيت طريقة تشغيل البرنامج فيمكنك أن تلقي نظرة سريعة على هذه التعليقات لتتذكر طريقة الاستخدام، ثم استوردنا الوحدات اللازمة ➋ فعملية النسخ واللصق إلى الحاوية تحتاج إلى الوحدة pyperclip، وقراءة وسائط سطر الأوامر تحتاج إلى الوحدة sys، وستفيدنا الوحدة shelve: فكل مرة يحفظ فيها المستخدم محتويات الحافظة فسنكتبها إلى ملف، وحينما يريد تحميل نص إلى الحافظة فسنفتح الملف ونقرأه منه، وسنسمي هذا الملف باسم mcb. الخطوة 2: حفظ محتويات الحافظة مع كلمة مفتاحية يسلك البرنامج سلوكًا مختلفًا اعتمادًا على ما يريده المستخدم: حفظ محتويات الحاوية مع كلمة مفتاحية، أو تحميل نص إلى الحاوية، أو عرض جميع الكلمات المفتاحية. لنعالج الآن أول حالة: #! python3 # mcb.pyw - حفظ وتحميل نصوص إلى الحافظة. --snip-- # حفظ محتوى الحافظة. ➊ if len(sys.argv) == 3 and sys.argv[1].lower() == 'save': ➋ mcbShelf[sys.argv[2]] = pyperclip.paste() elif len(sys.argv) == 2: ➌ # TODO: تحميل المحتوى وعرض الكلمات المفتاحية. mcbShelf.close() إذا كان أول وسيط من وسائط سطر الأوامر (الذي يكون دومًا بالفهرس 1 من قائمة sys.argv) هو 'save' ➊ فإن الوسيط الثاني هو الكلمة المفتاحية التي يجب حفظ محتويات الحافظة إليها، والتي ستستخدم كمفتاح مع mvbShelf، وستكون قيمة هذا المفتاح هي محتويات الحافظة الحالية ➋. أما إذا كان هنالك وسيط واحد ممرر من سطر الأوامر فهذا يعني أنه إما 'list' أو كلمة مفتاحية نريد تحميل المحتوى النصي المرتبط بها إلى الحافظة. اترك تعليقًا الآن وسنكتب الشيفرة لاحقًا ➌. الخطوة 3: تحميل الكلمات المفتاحية أو محتوى إحدى الكلمات لنكتب الآن الشيفرة المناسبة للحالتين الباقيتين: تحميل النص المرتبط بإحدى الكلمات المفتاحية إلى الحافظة، أو نسخ قائمة بجميع الكلمات المفتاحية إلى الحافظة: #! python3 # mcb.pyw - حفظ وتحميل نصوص إلى الحافظة. --snip-- # حفظ محتوى الحافظة. if len(sys.argv) == 3 and sys.argv[1].lower() == 'save': mcbShelf[sys.argv[2]] = pyperclip.paste() elif len(sys.argv) == 2: # تحميل المحتوى وعرض الكلمات المفتاحية. ➊ if sys.argv[1].lower() == 'list': ➋ pyperclip.copy(str(list(mcbShelf.keys()))) elif sys.argv[1] in mcbShelf: ➌ pyperclip.copy(mcbShelf[sys.argv[1]]) mcbShelf.close() إذا كان هنالك وسيط واحد في سطر الأوامر، فلنتأكد إن كان 'list' ➊، فإذا كان كذلك فستنسخ سلسلة نصية تمثل قائمةً من الكلمات المفتاحية إلى الحافظة ➋، ويمكن للمستخدم أن يلصقها في أي محرر نصي أمامه ليقرأها. فيما عدا ذلك فسنفترض أن الوسيط الممرر من سطر الأوامر هو كلمة مفتاحية، وإذا كانت الكلمة المفتاحية موجودة في mcbShelf فسنحمل القيمة إلى الحافظة ➌. هذا كل ما يلزم لتطوير البرنامج! قد تختلف خطوات التشغيل اعتمادًا على نظام التشغيل الذي تستعمله، راجع المقال 1 لتفاصيل. من غير المنطقي أن تغير الشيفرة المصدرية لبرنامجك كلما احتجت إلى تحديث البيانات، لأن المستخدم العادي لا يرتاح لتعديل الشيفرات البرمجية؛ وكل مرة تعدل فيها البرنامج فأنت تخاطر بحدوث مشاكل جديدة وعلل لم تنتبه إليها. لذا من المهم فصل البيانات اللازمة لتشغيل التطبيق عن التطبيق نفسه، مما يجعل برامجك سهلة الاستخدام من الآخرين وتقلل احتمال حدوث مشاكل فيها. الخلاصة تنظم الملفات في مجلدات (التي تسمى في بعض أنظمة التشغيل directory أي دليل)، ويصف مسارُ موقعَ الملف، ولكل برنامج يعمل في حاسوبك ما يسمى بمجلد العمل الحالي، مما يسمح بتحديد مسارات نسبية تبدأ من المجلد الحالي بدلًا من كتابة المسار الكامل للملف الذي يسمى أيضًا بالمسار المطلق. توفر الوحدتان pathlib و os.path عددًا من الدوال لإجراء عمليات على مسارات الملفات. يمكن أن تتفاعل برامجك مباشرةً مع محتويات الملفات النصية، فالدالة open() تفتح الملفات للقراءة، وتستطيع أن تحصل على محتواها كسلسلة نصية واحدة كبيرة عبر التابع read() أو كقائمة من السلاسل النصية عبر التابع readlines(). يمكن أن تستخدم الدالة open() لفتح الملفات في وضع الكتابة أو الإضافة لإنشاء ملفات نصية جديدة أو الإضافة على ملفات موجودة مسبقًا، على التوالي. تعلمنا في المقالات السابقة من هذه السلسلة كيفية استخدام الحافظة لتخزين وتحميل كمية كبيرة من النصوص إلى برامجنا بدل من كتابتها يدويًا، لكننا الآن قادرون على جعل برامجنا تقرأ الملفات من ذاكرة التخزين أو الكتابة إليها، وهذا تحسين كبير عما سبق، وتكون وسيطة التخزين أكثر استقرارًا من الحافظة. سنتعلم في المقال القادم كيفية التعامل مع الملفات نفسها، عبر نسخها وحذفها وإعادة تسميتها ونقلها …إلخ. مشاريع تدريبية لكي تتدرب، اكتب برامج لتنفيذ المهام الآتية. توسعة تطبيق الحافظة وسِّع تطبيق الحافظة الذي أنشأناه في هذا المقال وأضف إليه الأمر delete الذي يحذف كلمة مفتاحية ومحتوياتها من «الرف». ثم أضف الأمر delete دون أي وسائط إضافية الذي سيؤدي إلى حذف جميع الكلمات المفتاحية. لعبة Mad Libs أنشِئ لعبة Mad Libs التي تقرأ ملفًا نصية وتسمح للمستخدم بإضافة النص الذي يريده في أي مكان تظهر فيه الكلمات المفتاحية ADJECTIVE و NOUN و ADVERB و VERB في الملف النصي. فمثلًا سيبدو الملف النصي كما يلي: The ADJECTIVE panda walked to the NOUN and then VERB. A nearby NOUN was unaffected by these events. سيقرأ البرنامج هذا الملف، ويبحث عن تلك الكلمات، ويطلب من المستخدم أن يدخل بديلًا عنها: Enter an adjective: silly Enter a noun: chandelier Enter a verb: screamed Enter a noun: pickup truck وستكون نتيجة البرنامج هي: The silly panda walked to the chandelier and then screamed. A nearby pickup truck was unaffected by these events. ويجب أن تطبع هذه النتيجة على الشاشة وتحفظ في ملف نصي جديد باسم مناسب. البحث عبر التعابير النمطية اكتب برنامجًا يفتح جميع ملفات txt النصية في مجلد، ويبحث فيها عن أي سطر يحتوي على مطابقة لتعبير نمطي ضبطه المستخدم. يجب أن تعرض النتائج على الشاشة. ترجمة -بتصرف- للمقال Reading And Writing Files من كتاب Automate the Boring Stuff with Python. اقرأ أيضًا المقال السابق: التحقق من المدخلات عبر بايثون python القوائم Lists في لغة بايثون تهيئة بيئة العمل في بايثون Python
-
شيفرات التحقق من المدخلات تتأكد أن ما يدخله المستخدم، مثل النصوص الآتية من الدالة ()input، هي مكتوبة كتابةً صحيحةً؛ فمثلًا حينما نطلب من المستخدمين إدخال أعمارهم، فلا يفترض أن يقبل برنامجك أجوبةً غير منطقية على السؤال، مثل الأرقام السالبة (التي هي خارج المجال المنطقي للأعمار) أو الكلمات (نوع البيانات خطأ). يمنع التحقق من المدخلات حدوث العلل والمشاكل الأمنية، فلو كانت لدينا دالة باسم widthdrawFromAccount() التي تأخذ وسيطًا هو المبلغ الذي يجب اقتطاعه من حساب المستخدم، فيجب علينا الحرص على أن يكون المبلغ قيمةً إيجابية، فلو لم نتحقق من ذلك ومررنا قيمةً سالبة إلى الدالة widthdrawFromAccount() وطرحت هذه الدالة القيمة السالبة من حسابنا فسنحصل على أموال بدل سحبها! من الشائع أن نتحقق من مدخلات المستخدم ونستمر بسؤاله عن مدخلات صحيحة حتى يدخل نصًا صالحًا كما في المثال الآتي: while True: print('Enter your age:') age = input() try: age = int(age) except: print('Please use numeric digits.') continue if age < 1: print('Please enter a positive number.') continue Break print(f'Your age is {age}.') إذا شغلنا هذا البرنامج فسيكون الناتج كما يلي: Enter your age: five Please use numeric digits. Enter your age: -2 Please enter a positive number. Enter your age: 30 Your age is 30. إذا شغلت الشيفرة السابقة، فستُسأل عن عمرك حتى تدخل رقمًا صالحًا، وهذا يضمن أن قيمة المتغير age ستكون صالحة حينما ينتهي تنفيذ حلقة while، ولن تسبب قيمة هذا المتغير بخطأ لاحقًا في البرنامج. لكن كتابة شيفرات للتحقق لكل استدعاء للدالة input() في برنامجك هو أمر ممل وصعب، ومن المرجح أنك ستغفل عن بعض الحالت مما يؤدي إلى مرور مدخلات خطأ إلى برنامجك. لذا سنتعلم كيف نستعمل الوحدة PyInputPlus في هذا المقال للتحقق من المدخلات. الوحدة PyInputPlus تحتوي الوحدة PyInputPlus على عدد من الدوال الشبيه بالدالة input() لعدد من أنواع البيانات: الأرقام، والتواريخ، وعناوين البريد الإلكتروني، والمزيد. إذا أدخل المستخدم قيمةً غير صالحة، كتاريخ فيه خطأ في الصياغة أو رقم خارج المجال المحدد، فستعيد الوحدة PyInputPlus طلب المدخلات من المستخدم كما في الشيفرة السابقة. وتمتلك أيضًا بعض الميزات الأخرى المفيدة مثل وضع حد لعدد المرات التي سيعاد سؤال المستخدم فيها، ووضع زمن انتظار يجب أن يدخل المستخدم فيه المدخلات. الوحدة PyInputPlus ليست جزءًا من المكتبة القياسية في بايثون، لذا يجب عليك تثبيتها بشكل منفصل عبر استخدام مدير الحزم pip؛ وذلك بتشغيل الأمر pip install --user pyinputplus من سطر الأوامر. يشرح المقال الأول من السلسلة بالتفصيل خطوات تثبيت الوحدات الخارجية. للتحقق من سلامة تثبيت الوحدة PyInputPlus يمكننا تجربة استيرادها في الطرفية التفاعلية: >>> import pyinputplus إذا لم تظهر أي أخطاء حين استيراد الوحدة، فهذا يعني أنها مثبتة تثبيتًا صحيحًا. تمتلك الوحدة PyInputPlus عددًا من الدوال للتعامل مع مختلف أنواع المدخلات: inputStr() تشبه الدالة input() لكنها تمتلك ميزات وحدة PyInputPlus العامة، كما أن بإمكاننا تمرير دالة تحقق مخصصة. inputNum() تتحقق أن المستخدم يدخل رقمًا، وتعيد عددًا صحيحًا int أو عشريًا float، اعتمادًا إذا كان الرقم فيه فاصلة عشرية أم لا. inputChoice() تتحقق أن المستخدم اختار أحد الخيارات الموفرة له. inputMenu() شبيهة بالدالة inputChoice() لكنها توفر قائمة مع خيارات لها أرقام أو أحرف. inputDatetime() تتحقق أن المستخدم أدخل تاريخًا ووقتًا. inputYesNo() تتحقق أن المستخدم أدخل yes أو no. inputBool()* تشبه الدالة inputYesNo() لكنها تقبل القيمة True أو False وتعيد قيمة منطقية. inputEmail() تتحقق أن المستخدم أدخل بريدًا إلكترونيًا صالحًا. inputFilepath() تتأكد أن المستخدم أدخل مسارًا صالحًا لأحد الملفات، ويمكن أن تتحقق اختيارًا أن هنالك ملف بهذا الاسم. inputPassword() كما في الدالة المبنية في بايثون input() لكنها تظهر نجومًا * بدل إظهار مدخلات المستخدم لكي لا تظهر المدخلات الحساسة مثل كلمات المرور على الشاشة. ستعيد هذه الدوال طلب إدخال مدخلات صالحة من المستخدم في حال أدخل قيمةً خطأ: >>> import pyinputplus as pyip >>> response = pyip.inputNum() five 'five' is not a number. 42 >>> response 42 كتابة as pyip في عبارة import توفر علينا وقتًا في كتابة pyinputplus في كل مرة نريد استدعاء دالة من الوحدة PyInputPlus، وسنكتب بدلًا منها pyip. إذا نظرت إلى المثال السابق فستجد أن الدوال تعيد القيمة int أو float على عكس input() التي تعيد سلاسل نصية مثل '42'. وكما كنا نمرر سلسلة نصية إلى input() لاستعمالها كمحث prompt، فيمكننا تمرير سلسلة نصية إلى دوال PyInputPlus عبر استخدام الوسائط ذات الكلمات المفتاحية Keyword arguments، واستخدام الوسيط prompt لعرض المحث: >>> response = input('Enter a number: ') Enter a number: 42 >>> response '42' >>> import pyinputplus as pyip >>> response = pyip.inputInt(prompt='Enter a number: ') Enter a number: cat 'cat' is not an integer. Enter a number: 42 >>> response 42 يمكنك استخدام الدالة help() في بايثون لتعرف المزيد من المعلومات حول أي دالة من تلك الدوال، فمثلًا كتابة help(pyip.inputChoice) سيعرض معلومات حول الدالة inputChoice(). يمكنك أن تقرأ التوثيق كاملًا عبر pyinputplus.readthedocs. على خلاف الدالة المبنية في بايثون input()، تمتلك دوال الوحدة PyInputPlus عددًا من الميزات الإضافية للتحقق من المدخلات، كما سنشرح في القسم التالي. وسائط ذات الكلمات المفتاحية min و max و greaterThan و lessThan تمتلك الدوال inputNum() و inputInt() و inputFloat() التي تقبل الأرقام الصحيحة int والعشرية float الوسائط ذات الكلمات المفتاحية min و max و greaterThan و lessThan لتحديد مجال من القيم الصالحة: >>> import pyinputplus as pyip >>> response = pyip.inputNum('Enter num: ', min=4) Enter num:3 Input must be at minimum 4. Enter num:4 >>> response 4 >>> response = pyip.inputNum('Enter num: ', greaterThan=4) Enter num: 4 Input must be greater than 4. Enter num: 5 >>> response 5 >>> response = pyip.inputNum('>', min=4, lessThan=6) Enter num: 6 Input must be less than 6. Enter num: 3 Input must be at minimum 4. Enter num: 4 >>> response 4 هذه الوسائط ذات الكلمات المفتاحية هي اختيارية، لكن إذا ضبطناها فلا يمكن أن تكون مدخلات المستخدم أقل من min أو أكبر من max (لكن يمكن أن تكون المدخلات مساويةً لها)؛ ويجب أن تكون المدخلات أيضًا أكبر من قيمة greaterThan وأقل من قيمة lessThan (وأيضًا يمكن أن تكون المدخلات مساويةً لها). الوسيط ذو الكلمة المفتاحية blank افتراضيًا لا يُسمَح بالقيم الفارغة ما لم يضبط الوسيط blank إلى True: >>> import pyinputplus as pyip >>> response = pyip.inputNum('Enter num: ') Enter num:(blank input entered here) Blank values are not allowed. Enter num: 42 >>> response 42 >>> response = pyip.inputNum(blank=True) (blank input entered here) >>> response '' استخدام blank=True إذا أردت أن تجعل مدخلات المستخدم اختيارية. الوسائط ذات الكلمات المفتاحية limit و timeout و default ستستمر دوال الوحدة PyInputPlus سؤال المستخدم عن مدخلات صالحة للأبد (ما دام البرنامج يعمل بالطبع) افتراضيًا. لكن إذا أردنا أن تتوقف الدالة عن سؤال المستخدم بعد عدد من المحاولات أو بعد وقتٍ محدد، فيمكننا استخدام الوسيط limit و timeout. يمكننا تمرير قيمة إلى الوسيط ذي الكلمة المفتاحية limit لتحديد عدد المرات التي ستحاول الدالة فيها الحصول على مدخل صحيح قبل أن تتوقف عن المحاولة، وتمرير رقم إلى الوسيط ذي الكلمة المفتاحية timeout سيحدد عدد الثواني التي ستنتظرها الدالة لمدخلات المستخدم قبل أن تتوقف عن المحاولة. إذا فشل المستخدم بإدخال قيمة صالحة فسيؤدي ذلك إلى رمي الاستثناء RetryLimitException أو TimeoutException على التوالي. فمثلًا جرب إدخال ما يلي في الطرفية التفاعلية: >>> import pyinputplus as pyip >>> response = pyip.inputNum(limit=2) blah 'blah' is not a number. Enter num: number 'number' is not a number. Traceback (most recent call last): --snip-- pyinputplus.RetryLimitException >>> response = pyip.inputNum(timeout=10) 42 (entered after 10 seconds of waiting) Traceback (most recent call last): --snip-- pyinputplus.TimeoutException حين استخدام الوسائل السابقة يمكننا أيضًا استخدام الوسيط ذي الكلمة المفتاحية default، مما يجعل الدالة تعيد قيمةً افتراضيةً بدلًا من رمي استثناء: >>> response = pyip.inputNum(limit=2, default='N/A') hello 'hello' is not a number. world 'world' is not a number. >>> response 'N/A' فبدلًا من رمي الاستثناء RetryLimitException فستعيد الدالة inputNum() السلسلة النصية 'N/A'. الوسيط ذو الكلمة المفتاحية allowRegexes و blockRegexes يمكنك استخدام التعابير النمطية للتحقق إن كانت المدخلات مسموحٌ بها أم لا. فالوسيطان allowRegexes و blockRegexes يأخذها قائمةً من التعابير النمطية لتحديد إن كانت دالة PyInputPlus ستسمح بالمدخلات على أنها مقبولة أو ترفضها. جرب مثلًا الشيفرة الآتية في الطرفية التفاعلية لكي تقبل الدالة inputNum() الأرقام الرومانية بالإضافة إلى الأرقام العادية: >>> import pyinputplus as pyip >>> response = pyip.inputNum(allowRegexes=[r'(I|V|X|L|C|D|M)+', r'zero']) XLII >>> response 'XLII' >>> response = pyip.inputNum(allowRegexes=[r'(i|v|x|l|c|d|m)+', r'zero']) xlii >>> response 'xlii' ما سيؤثر عليه هذا التعبير النمطي هو الأحرف التي ستقبلها الدالة inputNum() من المستخدم، فالدالة حاليًا تقبل الأرقام الرومانية ذات الترتيب الخطأ مثل 'XVX' أو 'MILLI' لأن التعبير النمطي r'(I|V|X|L|C|D|M)+' يقبل هذه القيم. يمكنك أيضًا تحديد قائمة بالتعابير النمطية التي سترفضها دالة PyInputPlus باستخدام الوسيط blockRegexes. جرب المثال الآتي في الطرفية التفاعلية لترى أن الدالة inputNum() لن تقبل الأرقام الزوجية: >>> import pyinputplus as pyip >>> response = pyip.inputNum(blockRegexes=[r'[02468]$']) 42 This response is invalid. 44 This response is invalid. 43 >>> response 43 إذا حددت قيمتين للوسيطين allowRegexes و blockRegexes، فإن قائمة السماح ستأخذ أولوية على قائمة الرفض؛ جرب المثال الآتي الذي يسمح بالكلمتين 'caterpillar' و 'category' لكنه يرفض أي مدخلات فيها الكلمة 'cat'`: >>> import pyinputplus as pyip >>> response = pyip.inputStr(allowRegexes=[r'caterpillar', 'category'], blockRegexes=[r'cat']) cat This response is invalid. catastrophe This response is invalid. category >>> response 'category' توابع الوحدة PyInputPlus يمكنها أن توفر علينا كتابة شيفرات كثيرة مملة، تذكر أن تعود إلى التوثيق الرسمي للوحدة PyInputPlus لمزيد من المعلومات حول الوسائط التي يمكن تمريرها إلى دوالها. تمرير دالة تحقق خاصة إلى inputCustom() يمكننا كتابة دالة خاصة بنا لتجري عملية التحقق، ونمررها إلى الدالة inputCustom(). لنقل مثلًا أننا نريد من المستخدم أن يدخل سلسلةً من الأرقام التي يجب أن يكون مجموعها 10؛ فلن نجد دالةً باسم pyinputplus.inputAddsUpToTen() لكننا نستطيع إنشاء دالة خاصة بنا التي: تقبل معاملًا واحدًا هو السلسلة النصية التي أدخلها المستخدم. ترمي استثناءً حين وقوع مشكلة في التحقق. تعيد None (أو لا تحتوي على عبارة return) إذا أردنا أن تعيد الدالة inputCustom() مدخلات المستخدم كما هي. تعيد قيمة ليست None إذا أردنا أن تعيد الدالة inputCustom() سلسلةً نصيةً مختلفةً عمّا أدخله المستخدم. نمررها كأول وسيط إلى الدالة inputCustom(). سننشِئ في هذا المثال الدالة addsUpToTen() الخاصة بنا ومررناها إلى الدالة inputCustom(). لاحظ أن استدعاء الدالة يكون على الشكل inputCustom(addsUpToTen) وليس inputCustom(addsUpToTen()) لأننا نريد تمرير الدالة addsUpToTen() نفسها إلى الدالة inputCustom()، وليس استدعاء الدالة addsUpToTen() وتمرير القيمة المعادة منها. >>> import pyinputplus as pyip >>> def addsUpToTen(numbers): ... numbersList = list(numbers) ... for i, digit in enumerate(numbersList): ... numbersList[i] = int(digit) ... if sum(numbersList) != 10: ... raise Exception('The digits must add up to 10, not %s.' % (sum(numbersList))) ... return int(numbers) # إعادة عدد صحيح ... >>> response = pyip.inputCustom(addsUpToTen) # لا توجد أقواس بعد اسم الدالة 123 The digits must add up to 10, not 6. 1235 The digits must add up to 10, not 11. 1234 >>> response # inputStr() أعادت رقمًا وليس سلسلةً نصية 1234 >>> response = pyip.inputCustom(addsUpToTen) hello invalid literal for int() with base 10: 'h' 55 >>> response الدالة inputCustom() تقبل أيضًا بقية ميزات PyInputPlus مثل blank و limit و timeout و default و allowRegexes و blockRegexes. سنستفيد جدًا من كتابة دالة التحقق الخاصة بنا إذا كان من الصعب أو المستحيل كتابة تعبير نمطي للتحقق من صحة مدخلات المستخدم، كمثالنا عن إدخال أرقام مجموعها 10. مشروع: هل تريد معرفة حكمة اليوم؟ لنستخدم PyInputPlus لإنشاء مشروع بسيط يفعل ما يلي: يسأل المستخدم إن كان يريد معرفة حكمة اليوم؟ إذا أدخل المستخدم no فسينتهي البرنامج بسلام إذا أدخل المستخدم yes فسيذهب إلى الخطوة 1. أجزم أنك لا تريد أن تعرف الحكمة من مثالنا ? . لا نعرف إن كان سيدخل المستخدم أي سلسلة نصية خلاف "yes" و "no" لذا علينا إجراء عملية تحقق من صحة المدخلات، وسيكون جميلًا أن نسمح للمستخدم بإدخال "y" أو "n" بدلًا من كتابة كاملة الكلمة. تستطيع الدالة inputYesNo() فعل ذلك، وستعيد لنا السلسلة النصية 'yes' أو 'no' بغض النظر عن طريقة الإيجاب أو الرفض التي استعملها المستخدم: Want to know how to keep a 'wise' man busy for hours? sure 'sure' is not a valid yes/no response. Want to know how to keep a 'wise' man busy for hours? yes Want to know how to keep a 'wise' man busy for hours? y Want to know how to keep a 'wise' man busy for hours? Yes Want to know how to keep a 'wise' man busy for hours? YES Want to know how to keep a 'wise' man busy for hours? YES!!!!!! 'YES!!!!!!' is not a valid yes/no response. Want to know how to keep a 'wise' man busy for hours? TELL ME HOW TO KEEP A WISE MAN BUSY FOR HOURS. 'TELL ME HOW TO KEEP A WISE MAN BUSY FOR HOURS.' is not a valid yes/no response. Want to know how to keep a 'wise' man busy for hours? no Thank you. Have a nice day. افتح محرر النصوص وسمِّ ملفك باسم idiot.py وأدخل ما يلي: import pyinputplus as pyip سنستورد الوحدة PyInputPlus، لكننا نريد اختصار اسمها في برنامجنا إلى pyip لأنها أقصر من كتابة pyinputplus في كل مرة. while True: prompt = 'Want to know how to keep a 'wise' man busy for hours?\n' response = pyip.inputYesNo(prompt) ثم سندخل في حلقة تكرار لا نهائية لا تنتهي إلا بالخروج من البرنامج أو الوصول إلى عبارة break. وسنستخدم الدالة pyip.inputYesNo() في هذه الحلقة لقبول مدخلات المستخدم والتحقق أنها yes أو no. if response == 'no': break الدالة pyip.inputYesNo() مصممة لتعيد السلسلة النصية yes أو no؛ فإذا أعادت no فسنخرج من الحلقة اللانهائية ونكمل تنفيذ السطر الأخير من البرنامج الذي يشكر المستخدم على صبره: print('Thank you. Have a nice day.') وإلا فسيستمر تنفيذ حلقة التكرار. يمكنك إنشاء نسخة من الدالة inputYesNo() في اللغات غير الإنكليزية بتمرير قيم للوسيطين ذوي الكلمات المفتاحية yesVal و noVal. فانظر إلى المثال الآتي باللغة الإسبانية: prompt = '¿Quieres saber cómo mantener ocupado a un idiota durante horas?\n' response = pyip.inputYesNo(prompt, yesVal='sí', noVal='no') if response == 'sí': يمكن للمستخدم الآن إدخال sí أو s (سواءً كانت بأحرف كبيرة أو صغيرة) بدلًا من yes أو y للإيجاب بالموافقة. مشروع: اختبار جدول الضرب يمكننا استخدام ميزات الوحدة PyInputPlus لإنشاء اختبار لجدول الضرب له وقت معين. إذ نستطيع أن نضبط قيم للوسائط ذات الكلمات المفتاحية allowRegexes و blockRegexes و timeout و limit للدالة pyip.inputStr() ونترك أمر التحقق من صحة مدخلات المستخدم على الوحدة PyInputPlus. تذكر دومًا أنه كلما كتبت شيفرة أقل ستصبح برامجك أسرع في التطوير والتنفيذ. لننشِئ برنامجًا يعرض 10 أسئلة في جدول الضرب على المستخدم، ولا يجوز أن يدخل المستخدم سوى الأرقام الصحيحة. احفظ الشيفرات الآتية في ملف باسم multiplicationQuiz.py. سنستورد في البداية الوحدات pyinputplus و random و time. وسنتتبع عدد الأسئلة التي يسألها برنامجنا وعدد الإجابات الصحيحة التي يوفرها المستخدم عبر المتغيرين numberOfQuestions و correctAnswers. سنسأل المستخدم داخل حلقة for لعشر مرات. import pyinputplus as pyip import random, time numberOfQuestions = 10 correctAnswers = 0 for questionNumber in range(numberOfQuestions): سنختار داخل حلقة for أرقامًا ذات خانة واحدة لضربها مع بضعها، وسننشِئ المحث prompt الذي سنسأل المستخدم فيه عن قيمة ناتج الضرب #Q: N × N = الذي تكون فيه Q هي رقم السؤال (من 1 إلى 10) و N هما الرقمان اللذان سنضربهما ببعضهما: # اختيار رقمين عشوائيين num1 = random.randint(0, 9) num2 = random.randint(0, 9) prompt = '#%s: %s x %s = ' % (questionNumber, num1, num2) ستتولى الدالة pyip.inputStr() أغلبية خصائص البرنامج، فسنمرر لها المعامل allowRegexes الذي هو قائمة فيها عنصر واحد وهو السلسلة النصية '^%s$'، وسيبدل فيها %s إلى قيمة الجواب الصحيح، وسنستخدم ^ و $ للتحقق أن جواب المستخدم يبدأ وينتهي بالرقم الصحيح، وستحذف المكتبة PyInputPlus أي فراغات بيضاء في بداية ونهاية جواب المستخدم في حال أضاف مسافةً فارغةً خطأً. القيمة التي سنمررها إلى المعامل blocklistRegexes هي قائمة فيها عنصر هو صف قيمته ('.*', 'Incorrect!'). أول سلسلة نصية في الصف هي التعبير النمطي الذي يطابق أي شيء، وبالتالي لو أدخل المستخدم أي جواب لا يطابق الجواب الصحيح فسيرفضه البرنامج وستظهر السلسلة النصية 'Incorrect!' وسنطلب من المستخدم إدخال قيمة مجددًا. لاحظ أيضًا تمرير القيمة 8 إلى المعامل timeout و 3 إلى المعامل limit لكي نحرص أن المستخدم يمتلك 8 ثواني لإدخال إجابة و 3 محاولات فقط لإدخال الرقم الصحيح. try: pyip.inputStr(prompt, allowRegexes=['^%s$' % (num1 * num2)], blockRegexes=[('.*', 'Incorrect!')], timeout=8, limit=3) إذا لم يدخل المستخدم الإجابة خلال مهلة 8 ثواني، فسترمي pyip.inputStr() الاستثناء TimeoutException. إذا أدخل المستخدم 3 إجابات خطأ فسيرمى الاستثناء RetryLimitException. لاحظ أنهما جزء من الوحدة PyInputPlus لذا يجب أن نسبقهما بالبادئة pyip.. except pyip.TimeoutException: print('Out of time!') except pyip.RetryLimitException: print('Out of tries!') هل تذكر أن كتلة else تأتي بعد كتل if أو elif؟ يمكنها أيضًا أن تأتي بعد آخر كتلة except؛ وستنفذ الشيفرة الموجودة في كتلة else في حال عدم رمي أي استثناء في كتلة try، أي في حالتنا حين إدخال المستخدم الإجابة الصحيحة. استخدام else في هذا السياق هو أمر اختياري، والسبب الرئيسي لاستخدامها بدلًا من كتابة بقية التعابير البرمجية في كتلة try هو تسهيل مقروئية النص. else: # ستنفذ هذه الكتلة في حال عدم رمي أي استثناء print('Correct!') correctAnswers += 1 ومهما كانت الرسالة التي ستظهر للمستخدم من الرسائل الثلاث "Out of time!" أو "Out of tries!" أو "Correct!" فسيمهل المستخدم لمدة 1 ثانية في نهاية حلقة for ليقرأها. بعد سؤال المستخدم 10 مرات عبر حلقة for فسنعرض له كم إجابةً صحيحةً قد أجاب: time.sleep(1) # انتظر برهة ليستطيع المستخدم القراءة print('Score: %s / %s' % (correctAnswers, numberOfQuestions)) الوحدة PyInputPlus مرنة بما يكفي لاستخدامها في مختلف أنواع التطبيقات التي تقبل مدخلات المستخدم من سطر الأوامر كما رأينا في هذا المقال. الخلاصة من السهل أن ننسى كتابة شيفرات التحقق من مدخلات المستخدم، لكن دونها سنجد ظهور مختلف العلل البرمجية في برامجنا بسبب اختلاف القيم التي نتوقع أن يدخلها المستخدم عن القيم التي القيم التي يمكنه إدخالها؛ لذا يجب أن تكون برامجنا مرنةً بما يكفي للتعامل مع هذه الحالات الاستثنائية. يمكننا استخدام التعابير النمطية لإنشاء الشيفرات اللازمة للتحقق من المدخلات، أو استخدام مكتبة خارجية جاهزة مثل PyInputPlus باستيرادها عبر import pyinputplus as pyip، وستستطيع استخدام اسم مختصر لها أيضًا. تمتلك الوحدة PyInputPlus دوال مختلفة لأنواع متنوعة من المدخلات، بما في ذلك السلاسل النصية والأرقام والتواريخ، وأسئلة الإيجاب بالموافقة أو الرفض، وأسئلة True أو False، وعناوين البريد الإلكتروني، والملفات. وفي حين أن الدالة input() المضمنة في بايثون تعيد سلسلةً نصيةً دومًا، لكن هذه الدوال تعيد القيمة مع نوع البيانات المناسب لها. تسمح لنا الدالة inputChoice() باختصار أحد الخيارات الموجودة مسبقًا، بينما توفر inputMenu() قائمة مع خيارات لها أرقام أو أحرف لتسهيل الاختيار. لكل تلك الدوال ميزات قياسية: فهي تزيل الفراغات من بداية ونهاية المدخلات، وتضبط مهلةً وعددًا لمحاولات الإدخال عبر المعاملين timeout و limit، ونستطيع تمرير قائمة فيها سلاسل نصية تمثل تعابير نمطية إلى المعاملين allowRegexes أو blockRegexes لتضمين أو استبعاد تعابير نمطية محددة من إجابات المستخدم. وعلا ذلك ستوفر عليك وقتك الذي ستقضيه في كتابة حلقات while لإعادة طلب المدخلات من المستخدم. إذا لم تكن إحدى دوال الوحدة PyInputPlus مناسبةً لاحتياجاتك، لكنك ما تزال تريد الاستفادة من الطيف الواسع من ميزاتها، فيمكنك استدعاء الدالة inputCustom() وتمرير دالة تحقق خاصة بك لاستخدامها. لا تنسَ العودة إلى توثيق الوحدة PyInputPlus لأي ميزات أو أمور ترغب في أن تستوضحها ولم تكن واضحةً لك في هذا المقال. لا حاجة أن تعيد اختراع العجلة كل مرة، من المهم أن تتعلم كيفية استخدام الوحدات والمكتبات التي كتبها غيرك بدلًا من إضافة الوقت في إعادة برمجة نفس الميزات. بعد أن أصبحت لدينا المعرفة اللازمة في معالجة النصوص والتحقق من مدخلات المستخدم، لنتعلم كيفية القراءة والكتابة من نظام الملفات في حاسوبك. مشاريع تدريبية لكي تتدرب، اكتب برامج لتنفيذ المهام الآتية. صانع الصندويشات اكتب برنامجًا يسأل المستخدم عما يفضل وضعه في الصندويشة التي طلبها. استعمل الوحدة PyInputPlus للتأكد من صحة مدخلات المستخدم كما يلي: استخدم inputMenu() لنوع الخبز: دقيق كامل wheat، أو خبز أبيض white، أو خبز مختمر sourdough. استخدم inputMenu() لنوع البروتين: دجاج chicken، أو ديك turkey، أو بقر beef، أو توفو (جبن نباتي من حليب الصويا) tofu. استخدم inputYesNo() لتسأله إذا كان يريد جبنة cheese أم لا. إذا كان يريد جبنة فاستخدم inputMenu() لتسأله عن نوعها: شيدر cheddar أو موزاريلا mozzarella أو جبنة سويسرية swiss. استخدم inputYesNo() لسؤاله إن كان يريد مايونيز mayo وخردل mustard وخس lettuce وبندورة tomato. استخدم inputInt() لتسأله كم صندويشة يريد، احرص أن يكون العدد أكبر من 1. حدد من عندك أسعارًا للخيارات السابقة، واعرض للمستخدم السعر النهائي في نهاية البرنامج. أعد كتابة اختبار جدول الضرب لترى كم تسهل عليك الوحدة PyInputPlus عملية التحقق من المدخلات، حاول إعادة إنشاء اختبار جدول الضرب دون استخدامها؛ أي اكتب برنامجًا يسأل المستخدم 10 أسئلة حول جدول الضرب من 0 × 0 حتى 9 × 9. ستحتاج إلى برمجة الميزات الآتية: إذا أدخل المستخدم الإجابة الصحيحة فسيعرض له البرنامج القيمة "Correct!" لثانية ثم ينتقل إلى السؤال الذي يليه. لدى المستخدم ثلاث محاولات قبل أن ينتقل البرنامج إلى السؤال التالي. إذا مرت 8 ثواني بعد عرض السؤال لأول مرة ولم يدخل المستخدم إجابة صحيحةً فسينتقل البرنامج إلى السؤال التالي دون احتساب إجابة السؤال. قارن بين الشيفرة التي كتبتها وبين الشيفرة في قسم «مشروع: اختبار جدول الضرب». ترجمة -بتصرف- للفصل Input Validation من كتاب Automate the Boring Stuff with Python. اقرأ أيضًا المقال السابق: التعابير النمطية في لغة بايثون python التعابير النمطية في البرمجة القوائم Lists في لغة بايثون تهيئة بيئة العمل في بايثون Python
-
من المرجح أنك تعرف كيف تبحث عن النصوص بالضغط على Ctrl+f وإدخال الكلمات التي تريد البحث عنها، في حين أن التعابير النمطية Regular expressions تنقل الأمور إلى مرحلة أعلى: فهي تسمح لك بتحديد «نمط» النص الذي تبحث عنه، فلو لم تكن تعرف رقم هاتف الشركة في الورقة أمامك، لكنك تعيش في الولايات المتحدة أو كندا، فستعلم أن أرقام الهاتف تتألف من 3 أرقام ثم شرطة -، ثم 4 أرقام أخرى (وقد يبدأ برمز المنطقة وهو 3 أرقام في البداية). أي أنك ستعرف أن ما يلي هو رقم هاتف 415-555-1234 لكن 4,155,551,234 ليس رقمًا هاتفيًا. يمكننا التعرف على مختلف أنماط النصوص بسهولة: فعناوين البريد الإلكتروني تحتوي الرمز @ في منتصفها، وعناوين URL للمواقع تحتوي على نقط وخطوط مائلة، والعناوين الأخبارية تستعمل نسق العنوان Title case، والتاغات في وسائل التواصل الاجتماعي تبدأ برمز # ولا تحتوي على فراغات …إلخ. التعابير النمطية مفيدة جدًا، لكن قلّة من غير المبرمجين يعرفونها على الرغم من أن أغلبية المحررات النصية الحديثة ومعالجات النصوص مثل مايكرروسوفت وورد وليبرأوفيس رايتر يمتلكون خاصيات بحث واستبدال تستعمل التعابير النمطية. والتعابير النمطية تسرع معالجة النصوص لدرجة كبيرة، فلا يستفيد منها مستخدمو برمجيات التحرير فقط، بل المبرمجون أيضًا، ويقول الكاتب الشهير كوري دكتورو أن علينا تدريس التعابير النمطية قبل تدريس البرمجة: سنبدأ هذا المقال بكتابة برنامج لمطابقة نمط من النصوص دون استخدام التعابير النمطية، ثم سنرى كيف يمكننا كتابة شيفرة بسيطة باستخدام التعابير النمطية؛ وسنبدأ بالتعابير البسيطة ثم ننتقل إلى الميزات المتقدمة أكثر، مثل استبدال النصوص وفئات الحروف، ثم سنكتب في نهاية المقال مشروعًا بسيطًا يستخرج أرقام الهواتف وعناوين البريد الإلكتروني من مستند نصي. مطابقة الأنماط دون استخدام التعابير النمطية لنقل أننا نريد البحث عن رقم هاتف يتبع نظام الكتابة الأمريكي في سلسلة نصية، أي يحتوي 3 أرقام ثم شرطة ثم 3 أرقام ثم شرطة ثم 4 أرقام، مثل 415-555-4242. لنكتب دالةً باسم isPhoneNumber() للتحقق إن كانت تمثل السلسلة النصية رقم هاتف، وتعيد القيمة True أو False. احفظ الشيفرة الآتية في ملف باسم isPhoneNumber.py: def isPhoneNumber(text): ➊ if len(text) != 12: return False for i in range(0, 3): ➋ if not text[i].isdecimal(): return False ➌ if text[3] != '-': return False for i in range(4, 7): ➍ if not text[i].isdecimal(): return False ➎ if text[7] != '-': return False for i in range(8, 12): ➏ if not text[i].isdecimal(): return False ➐ return True print('Is 415-555-4242 a phone number?') print(isPhoneNumber('415-555-4242')) print('Is ABC a phone number?') print(isPhoneNumber(ABC)) حين تجربة المثال السابق فسينتج ما يلي: Is 415-555-4242 a phone number? True Is ABC a phone number? False تحتوي الدالة isPhoneNumber() على شيفرة تجري عدة عمليات تحقق للتأكد أن السلسلة النصية في المعامل text هي رقم هاتف صالح، وإذا فشلت أي تحقق من هذه التحققات فستعيد الدالة القيمة False. تبدأ الشيفرة بالتحقق أن السلسلة النصية بطول 12 محرفًا تمامًا ➊، ثم تتحقق أن رقم المنطقة (أي أول 3 محارف في text) تحتوي على محارف رقمية فقط ➋، بقية الدالة تتحقق أن السلسلة النصية تتبع نمط أرقام الهواتف: يجب أن تكون أول شرطة في الرقم بعد رمز المنطقة ➌، ثم 3 أرقام ➍، ثم شرطة ➎، ثم 4 أرقام ➏، وإن أنهينا كل عمليات التحقق بنجاح فستعيد الدالة True ➐. استدعاء الدالة isPhoneNumber() مع الوسيط '415-555-4242' سيعيد True، بينما استدعاؤها مع 'ABC' سيعيد False، إذ ستفشل أول عملية تحقق لأن السلسلة النصية 'ABC' ليست بطول 12 محرفًا. إذا أردت العثور على رقم هاتف في سلسلة نصية طويل، فستحتاج إلى كتابة المزيد من الشيفرات لمطابقة نمط رقم الهاتف. بدِّل استدعاء الدالة print() في المثال السابق إلى ما يلي: message = 'Call me at 415-555-1011 tomorrow. 415-555-9999 is my office.' for i in range(len(message)): ➊ chunk = message[i:i+12] ➋ if isPhoneNumber(chunk): print('Phone number found: ' + chunk) print('Done') سينتج البرنامج الناتج الآتي: Phone number found: 415-555-1011 Phone number found: 415-555-9999 Done في كل دورة لحلقة for سنأخذ 12 محرفًا من المتغير message ونسندها إلى المتغير chunk ➊، فمثلًا في أول دورة لحلقة التكرار تكون قيمة i هي 0، وستسند القيمة message[0:12] إلى المتغير chunk (أي السلسلة النصية 'Call me at 4')، وفي الدورة التالية تكون قيمة i تساوي 1، وستسند القيمة message[1:13] إلى chunk (أي السلسلة النصية 'all me at 41')، بعبارة أخرى: في كل دورة من حلقة for ستتغير قيمة chunk بزيادة مكان بدء عملية الاقتطاع بمقدار واحد، ويكون طولها 12 محرفًا: 'Call me at 4' 'all me at 41' 'll me at 415' 'l me at 415-' وهلم جرًا… سنمرر بعدئذٍ المتغير chunk إلى الدالة isPhoneNumber() للتحقق إن كان يطابق نمط أرقام الهواتف ➋، وإذا طابقها فسنطبع القيمة المطابقة. سنستمر في تنفيذ حلقة التكرار التي ستمر على السلسلة النصية كلها، وستختبر كل 12 محرفًا فيها على حدة، وتطبع قيمة chunk إن أعادت الدالة isPhoneNumber() القيمة True، وبعد المرور على جميع محارف السلسلة النصية message فسنطبع الكلمة Done. على الرغم من أن السلسلة النصية في هذا المثال message قصيرة، لكنها قد تكون طويلة جدًا وفيها ملايين المحارف، وسيعالجها البرنامج في أقل من ثانية؛ لكن إن كتبنا برنامجًا يطابق أرقام الهواتف باستخدام التعابير النمطية فسيعمل بسرعة مشابهة تقريبًا، لكن برمجته أسهل بكثير. العثور على تعبير نصي باستخدام التعابير النمطية يعمل البرنامج الذي كتبناه في القسم السابق عملًا صحيحًا، لكننا احتجنا إلى كتابة شيفرة كثيرة للقيام بأمر بسيط؛ فطول الدالة isPhoneNumber() هو 17 سطرًا لكنها تستطيع مطابقة نمط واحد من أرقام الهواتف. فماذا عن الأرقام المكتوبة بالشكل 415.555.4242 أو (415) 555-4242؟ ماذا لو كان هنالك رمز تحويل بعد رقم الهاتف مثل 415-555-4242 x99؟ ستفشل الدالة isPhoneNumber() بمطابقتها. صحيحٌ أنك تستطيع كتابة شيفرات إضافية للتحقق من هذه الحالات، لكن هنالك طريقة أسهل بكثير. التعابير النمطية Regular Expression أو اختصارًا Regex تصف نمط النصوص (ومن هنا أتى اسمها). فمثلًا \d في صياغة التعابير النمطية تعني محرف رقمي، أي رقم مفرد من 0 حتى 9. ويستعمل التعبير النمطي \d\d\d-\d\d\d-\d\d\d\d في بايثون لمطابقة النص الذي تطابقه الدالة isPhoneNumber() السابقة: سلسلة من 3 أرقام، ثم شرطة، ثم 3 أرقام، ثم شرطة، ثم 4 أرقام. ولن تطابق أي سلسلة نصية لها نمط آخر التعبير النمطي \d\d\d-\d\d\d-\d\d\d\d. لكن يمكن أن تكون التعابير النمطية أعقد من ذلك بكثير، فمثلًا إضافة الرقم 3 بين قوسين مجعدين {3} يعني «طابق هذا النمط 3 مرات»، وبالتالي يمكننا كتابة التعبير النمطي السابق بشكل مختصر \d{3}-\d{3}-\d{4} وستكون النتيجة نفسها. إنشاء كائنات Regex جميع دوال التعابير النمطية موجودة في الوحدة re، وعلينا استيرادها أولًا: >>> import re ملاحظة: أغلبية الأمثلة في هذا المقال تستعمل الوحدة re، لذا من المهم أن تتذكر استيرادها في بداية كل سكربت أو حينما تعيد تشغيل محرر Mu، وإلا فستحصل على رسالة الخطأ NameError: name 're' is not defined. تمرير سلسلة نصية تمثل التعبير النمطي إلى re.compile() سيعيد كائن Regex. لإنشاء كائن Regex يطابق نمط أرقام الهواتف السابق، فأدخل ما يلي إلى الطرفية التفاعلية. تذكر أن \d تعني «محرف رقمي»، و \d\d\d-\d\d\d-\d\d\d\d هو التعبير النمطي الذي سيطابق رقم الهاتف. >>> phoneNumRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d') يحتوي المتغير phoneNumRegex الآن على كائن Regex. مطابقة كائنات Regex يمتلك الكائن Regex التابع search() الذي يبحث في السلسلة النصية التي مررت إليها لأي مطابقة للتعبير النمطية. سيعيد التابع search() القيمة None إذا لم يُطابَق التعبير النمطي في السلسلة النصية، وإذا عُثر على التعبير النمطي فسيعيد التابع search() كائن Match، الذي فيه التابع group() الذي يعيد النص الذي جرت مطابقته مع التعبير النمطي (سنشرح المجموعات لاحقًا فلا تقلق): >>> phoneNumRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d') >>> mo = phoneNumRegex.search('My number is 415-555-4242.') >>> print('Phone number found: ' + mo.group()) Phone number found: 415-555-4242 اسم المتغير mo هو اسم عام لكائنات Match، قد يبدو المثال السابق معقدًا لكنه أقصر بكثير من برنامج isPhoneNumber.py. بدايةً مررنا التعبير النمطي الذي نريده إلى re.compile() وحفظنا كائن Regex الناتج في المتغير phoneNumRegex، ثم استدعينا التابع search() على phoneNumRegex ومررنا السلسلة النصية التي نريد البحث فيها عن النمط إلى التابع search(). ستخزن نتيجة البحث في المتغير mo، ونحن نعرف في هذا المثال أن التابع سيعيد الكائن Match، ولمعرفتنا أن المتغير mo سيحتوي على كائن Match وليس قيمة فارغة None، فاستدعينا التابع group() على mo لإعادة الناتج، ولأننا كتبنا mo.group() داخل الدالة print() فسنعرض الناتج الذي جربت مطابقته 415-555-4242. مراجعة لعملية لمطابقة التعابير النمطية هنالك عدة خطوات لاستعمال التعابير النمطية في بايثون، وكل واحدة منها بسيطة وسهلة: استيراد الوحدة Regex بكتابة import re. إنشاء كائن Regex مع الدالة re.compile()، وتذكر أن تستعمل سلسلة نصية خام raw بكتابة r قبلها. تمرير السلسلة النصية التي تريد البحث فيها إلى التابع search() الذي سيعيد كائن من النوع Match. استدعاء الدالة group() للكائن Match التي تعيد السلسلة النصية المطابقة. ملاحظة: صحيحٌ أنني أنصحك أن تجرب الشيفرات في الطرفية التفاعلية لكنك تستطيع استخدام تطبيقات ويب لاختبار التعابير النمطية التي تظهر لك بوضوح كيف يطابق التعبير النمطي النص الذي أدخلته. أنصحك بتجربة موقع pythex. ميزات إضافية لمطابقة النصوص عبر التعابير النمطية لقد تعلمنا الخطوات الأساسية لإنشاء والبحث عبر التعابير النمطية في بايثون، وأصبحنا جاهزين لتجربة ميزات أقوى لها. التجميع مع الأقواس لنقل أنك تريد أن تفصل رقم المنطقة من بقية رقم الهاتف. إذا أضفنا أقواسًا في التعابير النمطية فستنشِئ مجموعات groups كما في (\d\d\d)-(\d\d\d-\d\d\d\d)، ثم يمكننا استخدام التابع group() للكائن Match للحصول على النص المطابق من مجموعة معينة. ستخزن القيمة المطابقة من أول مجموعة في المجموعة 1، والمجموعة الثانية في 2… وإذا مررنا القيمة 1 أو 2 إلى التابع group() فسنحصل على أجزاء مختلفة من النص الذي جرت مطابقته. أما تمرير القيمة 0 أو عدم تمرير أي قيمة إلى التابع group() فسيعيد كامل النص الذي جرت مطابقته من التعبير النمطي: >>> phoneNumRegex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)') >>> mo = phoneNumRegex.search('My number is 415-555-4242.') >>> mo.group(1) '415' >>> mo.group(2) '555-4242' >>> mo.group(0) '415-555-4242' >>> mo.group() '415-555-4242' إذا أردت الحصول على جميع المجموعات في آن واحد، فاستعمل التابع groups() (لاحظ أن اسم التابع بالجمع وليس المفرد): >>> mo.groups() ('415', '555-4242') >>> areaCode, mainNumber = mo.groups() >>> print(areaCode) 415 >>> print(mainNumber) 555-4242 يعيد التابع mo.groups() صفًا tuple فيه أكثر من قيمة، ويمكنك استعمال الإسناد المتعدد لإسناد كل قيمة إلى متغير كما في السطر الآتي: areaCode, mainNumber = mo.groups() للأقواس معنى خاص في التعابير النمطية، لكن ماذا نفعل لو أردنا مطابقة الأقواس في النص؟ لنقل مثلًا أن رمز المنطقة في أرقام الهواتف التي تريد مطابقتها موجودة بين قوسين، وفي هذه الحالة سنحتاج إلى تهريب المحرفين ( و ) عبر شرطة مائلة خلفية: >>> phoneNumRegex = re.compile(r'(\(\d\d\d\)) (\d\d\d-\d\d\d\d)') >>> mo = phoneNumRegex.search('My phone number is (415) 555-4242.') >>> mo.group(1) '(415)' >>> mo.group(2) '555-4242' سيطابق المحرفان المهربان `)و `( في السلسلة النصية الخام الممررة إلى الدالة re.compile() الأقواس في السلسلة النصية التي سنبحث فيها. هنالك معانٍ خاصة للمحارف الآتية في التعابير النمطية: . ^ $ * + ? { } [ ] \ | ( ) في حال أردت مطابقة أحد تلك المحارف في النص الذي تريد البحث فيه، فعليك تهريبها بوضع خط مائل خلفي قبلها: \. \^ \$ \* \+ \? \{ \} \[ \] \\ \| \( \) تأكد أنك لم تخلط بين الأقواس المهربة وبين أقواس المجموعات في التعابير النمطية، إذا ظهرت لك رسالة خطأ حول «قوس ناقص» أو «أقواس غير متوازنة» فاعلم أنك نسيت إغلاق قوس مجموعة غير مهرب كما في المثال الآتي: >>> re.compile(r'(\(Parentheses\)') Traceback (most recent call last): --snip-- re.error: missing ), unterminated subpattern at position 0 تقول لك رسالة الخطأ أن هنالك قوس مفتوح في الفهرس 0 من السلسلة النصية r'((Parentheses)' الذي لا يوجد له قوس إغلاق. مطابقة أكثر من مجموعة باستخدام الخط العمودي محرف الخط العمودي | الذي يسمى أيضًا بالأنبوب Pipe يستعمل في أي مكان تريد فيه مطابقة أحد التعابير الموفرة، أي لنقل أن لدينا التعبير النمطي r'Batman|Yasmin' الذي سيطابق 'Batman' أو 'Yasmin'. فلو كانت الكلمتان Batman و Yasmin موجودةً في السلسلة النصية التي سنبحث فيها، فستعاد أول قيمة تطابق إلى الكائن Match: >>> heroRegex = re.compile (r'Batman|Yasmin') >>> mo1 = heroRegex.search('Batman and Yasmin') >>> mo1.group() 'Batman' >>> mo2 = heroRegex.search('Yasmin and Batman') >>> mo2.group() 'Yasmin' ملاحظة: يمكنك العثور على جميع حالات المطابقة باستخدام التابع findall() المشروحة في هذا المقال. يمكنك أيضًا استخدام الخط العمودي لمطابقة تعابير فرعية مختلفة، فمثلًا لو قلنا أننا نريد مطابقة أي سلسلة نصية من 'Batman' و 'Batmobile' و 'Batcopter' و 'Batbat'، ولأن كل هذه السلاسل النصية تبدأ بالكلمة Bat فمن المنطقي أن نكتب هذه السابقة مرة واحدة، ويمكننا فعل ذلك عبر الأقواس كما يلي: >>> batRegex = re.compile(r'Bat(man|mobile|copter|bat)') >>> mo = batRegex.search('Batmobile lost a wheel') >>> mo.group() 'Batmobile' >>> mo.group(1) 'Mobile' استدعاء التابع mo.group() يعيد النص 'Batmobile' بينما mo.group(1) يعيد النص المطابق داخل مجموعة الأقواس الأولى، أي 'mobile'. استخدام الخط العمودي | يتيح لنا مطابقة أحد التعابير النمطية الموفرة، وإذا أردنا أن نطابق الخط العمودي نفسه في السلسلة النصية المبحوث فيها فلا ننسى تهريبه |. المطابقة الاختيارية عبر إشارة الاستفهام قد ترغب أحيانًا بمطابقة نمط اختياريًا، أي أن التعبير النمطي سيطابق السلسلة النصية بغض النظر إن احتوت السلسلة النصية على ذاك النمط أم لا. وتشير علامة الاستفهام ? أن النمط الذي يسبقها اختياري: >>> batRegex = re.compile(r'Bat(wo)?man') >>> mo1 = batRegex.search('The Adventures of Batman') >>> mo1.group() 'Batman' >>> mo2 = batRegex.search('The Adventures of Batwoman') >>> mo2.group() 'Batwoman' لاحظ أن الجزء (wo)? يعني أن النمط wo هو مجموعة اختيارية، فسيطابَق التعبير النمطي لو احتوت السلسلة النصية على 0 أو 1 من السلسلة النصية wo داخلها. وهذا يعني أن التعبير النمطي سيطابق 'Batwoman' و 'Batman' معًا. لو عدنا إلى مثال أرقام الهواتف السابق، يمكننا تعديل التعبير النمطي لكي يبحث عن أرقام الهواتف التي تحتوي أو لا تحتوي على رمز المنطقة: >>> phoneRegex = re.compile(r'(\d\d\d-)?\d\d\d-\d\d\d\d') >>> mo1 = phoneRegex.search('My number is 415-555-4242') >>> mo1.group() '415-555-4242' >>> mo2 = phoneRegex.search('My number is 555-4242') >>> mo2.group() '555-4242' يمكنك أن تقول أن ? يعني «طابق النمط الفرعي السابق 0 مرة أو مرة واحدة»، وإذا أردنا مطابقة علامة الاستفهام فلا ننسى تهريبها بكتابة \?. المطابقة صفر مرة أو أكثر باستخدام رمز النجمة رمز النجمة * asterisk يعني «طابق صفر مرة أو أكثر»، فاستعمال النجمة يعني أن التعبير النمطي الذي يسبقها سيطابق لأي عدد من المرات في السلسلة النصية؛ فقد لا يكون موجودًا أو يكون مكررًا مرات كثيرة: >>> batRegex = re.compile(r'Bat(wo)*man') >>> mo1 = batRegex.search('The Adventures of Batman') >>> mo1.group() 'Batman' >>> mo2 = batRegex.search('The Adventures of Batwoman') >>> mo2.group() 'Batwoman' >>> mo3 = batRegex.search('The Adventures of Batwowowowoman') >>> mo3.group() 'Batwowowowoman' لاحظ أن التعبير النمطي (wo)* سيُطابَق 0 مرة في 'Batman'، ومرة واحدة في 'Batwoman'، وأربع مرات في 'Batwowowowoman'. إذا أردنا مطابقة النجمة نفسها فلا ننسَ تهريبها بكتابة \*. المطابقة مرة واحدة أو أكثر باستخدام إشارة الجمع على خلاف إشارة النجمة * التي تطابق النمط صفر مرة أو أكثر، تعني إشارة الجمع أو إشارة الزائد + «مطابقة النمط مرة واحدة أو أكثر»، هذا يعني أن النمط الذي يسبق إشارة + يجب أن يكون موجودًا على الأقل مرة واحدة، على خلاف إشارة النجمة التي تسمح بألّا يكون النمط موجودًا في السلسلة النصية. قارن بين أثر رمز النجمة في القسم السابق والمثال الآتي: >>> batRegex = re.compile(r'Bat(wo)+man') >>> mo1 = batRegex.search('The Adventures of Batwoman') >>> mo1.group() 'Batwoman' >>> mo2 = batRegex.search('The Adventures of Batwowowowoman') >>> mo2.group() 'Batwowowowoman' >>> mo3 = batRegex.search('The Adventures of Batman') >>> mo3 == None True التعبير النمطي Bat(wo)+man لا يطابق السلسلة النصية 'The Adventures of Batman' لأن من المطلوب وجود التعبير الفرعي wo مرة واحدة على الأقل. إذا أردنا مطابقة إشارة الزائد فلا ننسى تهريبها +. مطابقة النمط لعدد معين من المرات باستخدام الأقواس المجعدة إذا كانت لديك مجموعة تريد تكرارها لعدد معين من المرات، فأتبع تلك السلسلة برقم محاط بأقواس {}. فمثلًا التعبير النمطي (Ha){3} يطابق السلسلة النصية 'HaHaHa' لكنه لن يطابق 'HaHa' لأنها تحتوي على تكرارين للتعبير Ha فقط. وبدلًا من كتابة رقم واحد، يمكننا تحديد مجال من التكرارات بكتابة الحد الأدنى، ثم فاصلة، ثم الحد الأقصى ضمن القوسين، مثلًا التعبير النمطي (Ha){3,5} سيطابق 'HaHaHa' و 'HaHaHaHa' و 'HaHaHaHaHa'. يمكنك ألّا تحدد الرقم الأدنى أو الأقصى ضمن القوسين لتترك المجال مفتوحًا، فمثلًا (Ha){3,} سيطبق 3 تكرارات أو أكثر من المجموعة (Ha)، بينما (Ha){,5} سيطابق المجموعة (Ha) من 0 حتى 5 تكرارات. ستساعدنا الأقواس المجعدة على تقصير التعبير النمطي، إذ يتساوى التعبيران النمطيان الآتيان: (Ha){3} (Ha)(Ha)(Ha) وسيتساوي أيضًا: (Ha){3,5} ((Ha)(Ha)(Ha))|((Ha)(Ha)(Ha)(Ha))|((Ha)(Ha)(Ha)(Ha)(Ha)) جرب ما يلي في الطرفية التفاعلية: >>> haRegex = re.compile(r'(Ha){3}') >>> mo1 = haRegex.search('HaHaHa') >>> mo1.group() 'HaHaHa' >>> mo2 = haRegex.search('Ha') >>> mo2 == None True سيطابق (Ha){3} السلسلة النصية 'HaHaHa' لكنه لن يطابق 'Ha'، وبالتالي سيعيد التابع search() القيمة None. المطابقة الجشعة والمطابقة غير الجشعة لمّا كان التعبير (Ha){3,5} يطابق 3 أو 4 أو 5 تكرارات من Ha في السلسلة النصية 'HaHaHaHaHa' فستتسائل لماذا يعيد استدعاء التابع group() على الكائن Match السلسلة النصية 'HaHaHaHaHa' بدلًا من الاحتمالات الأخرى، ففي النهاية 'HaHaHa' و 'HaHaHaHa' هي مطابقات صالحة في التعبير النمطي (Ha){3,5}. تكون التعابير النمطية في بايثون جشعة greedy افتراضيًا، وهذا يعني أنه في الحالات غير المحددة ستحاول التعابير النمطية مطابقة أطول سلسلة نصية ممكنة؛ أما المطابقة غير الجشعة non-greedy (وتسمى أحيانًا بالمطابقة الكسولة lazy) تطابق أقصر سلسلة نصية ممكنة، ونستطيع تحديد أننا نريد مطابقة غير جشعة عبر وضع علامة استفهام بعد قوس الإغلاق المجعد. جرب ما يلي ولاحظ الاختلافات بين النسخة الجشعة وغير الجشعة وماذا ستطابق في السلسلة النصية نفسها: >>> greedyHaRegex = re.compile(r'(Ha){3,5}') >>> mo1 = greedyHaRegex.search('HaHaHaHaHa') >>> mo1.group() 'HaHaHaHaHa' >>> nongreedyHaRegex = re.compile(r'(Ha){3,5}?') >>> mo2 = nongreedyHaRegex.search('HaHaHaHaHa') >>> mo2.group() 'HaHaHa' لاحظ أن علامة الاستفهام لها معنيان في التعابير النمطية، الأول هو المطابقة غير الجشعة، والثاني هو جعل المجموعة اختيارية. وهذان المعنيان غير مرتبطين ببعضهما. التابع findall() بالإضافة إلى التابع search() تمتلك كائنات Regex التابع findall()، وفي حين أن التابع search() يعيد كائن Match لأول جزء مطابَق من السلسلة النصي التي نبحث فيها، يعيد التابع findall() جميع المطابقات في السلسلة النصية. لنرى كيف يعيد التابع search() الكائن Match على أول سلسلة نصية مطابقة: >>> phoneNumRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d') >>> mo = phoneNumRegex.search('Cell: 415-555-9999 Work: 212-555-0000') >>> mo.group() '415-555-9999' وفي المقابل لن يعيد التابع findall() الكائن Match، بل قائمة فيها سلاسل نصية، لطالما لم تكن هنالك مجموعات فرعية في التعبير النمطي؛ وتمثل كل سلسلة نصية في القائمة المعادة الجزء من النص المطابق للتعبير النمطي: >>> phoneNumRegex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d') # لا توجد تعابير فرعية >>> phoneNumRegex.findall('Cell: 415-555-9999 Work: 212-555-0000') ['415-555-9999', '212-555-0000'] أما لو كانت هنالك مجموعات أو تعابير فرعية في التعبير النمطي، فسيعيد التابع findall() قائمةً من الصفوف tuples، ويمثل كل صف مطابقةً، وعناصر الصف هي السلاسل النصية المطابقة لكل تعبير فرعي في التعبير النمطي. لنرى الكلام السابق عمليًا لنفهمه، جرب ما يلي في الطرفية التفاعلية ولاحظ وجود أنماط فرعية في التعبير النمطي: >>> phoneNumRegex = re.compile(r'(\d\d\d)-(\d\d\d)-(\d\d\d\d)') # توجد تعابير فرعية >>> phoneNumRegex.findall('Cell: 415-555-9999 Work: 212-555-0000') [('415', '555', '9999'), ('212', '555', '0000')] لنلخص ما الذي يعيد التابع findall(): عند استدعائه على تعبير نمطي لا يحتوي على مجموعات، مثل \d\d\d-\d\d\d-\d\d\d\d فسيعيد التابع findall() قائمةً من السلاسل النصية مثل ['415-555-9999', '212-555-0000']. عند استدعائه على تعبير نمطي يحتوي على مجموعات أو تعابير نمطية فرعية، مثل (\d\d\d)-(\d\d\d)-(\d\d\d\d) فسيعيد التابع findall() قائمةً من الصفوف التي تحتوي على سلاسل نصية، سلسلة نصية لكل مجموعة، مثل: [('415', '555', '9999'), ('212', '555', '0000')] فئات المحارف تعلمنا في مثال أرقام الهواتف السابق أن \d يطابق أي رقم، أي أنه اختصار للتعبير (0|1|2|3|4|5|6|7|8|9). هنالك عدد من فئات المحارف المختصرة، والتي تستطيع معرفتها من الجدول 7-1. اختصار فئة المحارف يمثل \d أي محرف يمثل رقمًا من 0 حتى 9 \D أي محرف ليس رقمًا من 0 حتى 9 \w أي حرف أو رقم أو الشرطة السفلية، يمكننا أن نقول أنه يطابق محارف «الكلمات» \W أي محرف ليس حرفًا أو رقمًا أو شرطةً سفلية (عكس \w) \s أي مسافة فارغة أو مسافة جدولة أو سطر جديد، يمكننا أن نقول أنه يطابق «الفراغات» \S أي محرف ليس فراغًا أو مسافة جدولة أو سطر جديد الجدول 7-1: اختصارات فئات المحارف الشائعة تساعد فئات المحارف Character classes على اختصار التعابير النمطية، ففئة المحارف [0-5] تطابق الأرقام من 0 إلى 5 فقط، وهذا أكثر اختصارًا من كتابة (0|1|2|3|4|5)، لاحظ أننا نملك \d لمطابقة الأرقام فقط، لكن \w يطابق الأرقام والأحرف والشرطة السفلية _، ولا يوجد اختصار لمطابقة الأحرف فقط، لكنك تستطيع أن تكتب [a-zA-Z] التي سنشرحها لاحقًا. >>> xmasRegex = re.compile(r'\d+\s\w+') >>> xmasRegex.findall('12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids, 7 swans, 6 geese, 5 rings, 4 birds, 3 hens, 2 doves, 1 partridge') ['12 drummers', '11 pipers', '10 lords', '9 ladies', '8 maids', '7 swans', '6 geese', '5 rings', '4 birds', '3 hens', '2 doves', '1 partridge'] سيطابق التعبير النمطي \d+\s\w+ أي نص فيه رقم واحد أو أكثر \d+ يكون متبوعًا بفراغ \s ويكون متبوعًا برقم أو حرف أو شرطة سفلية \w+. سيعيد التابع findall() السلاسل النصية المطابقة من التعبير النمطي في قائمة. كتابة فئات محارف مخصصة هنالك حالات نحتاج فيها إلى استخدام مجموعة من المحارف لكن الفئات المختصرة الشائعة (مثل \d و \w و \s …إلخ.) غير مناسبة لحالتنا؛ لذا يمكننا تعريف فئات المحارف بأنفسنا باستعمال الأقواس المربعة []. فمثلًا فئة المحارف [aeiouAEIOU] ستطابق أي حرف صوتي سواءً كان بالحالة الصغيرة أو الكبيرة: >>> vowelRegex = re.compile(r'[aeiouAEIOU]') >>> vowelRegex.findall('RoboCop eats baby food. BABY FOOD.') ['o', 'o', 'o', 'e', 'a', 'a', 'o', 'o', 'A', 'O', 'O'] يمكنك أيضًا تضمين مجال من الأرقام أو الأحرف باستخدام الشرطة، فمثلًا فئة المحارف [a-zA-Z0-9] ستطابق جميع الأحرف الصغيرة والأحرف الكبيرة والأرقام. لاحظ أن رموز التعابير النمطية العادية لن تفسر داخل الأقواس المربعة، فلا حاجة إلى تهريب المحارف . أو * أو ? أو () بخط مائل خلفي. فمثلًا فئة المحارف [0-5.] تطابق الأعداد من 0 إلى 5 ونقطة. ولا حاجة إلى تهريبها وكتابة [0-5\.]. إذا وضعنا رمز القبعة caret ^ بعد قوس بداية فئة المحارف فسيعني «الرفض»، أي سيعكس محتويات فئة المحارف، أي أنها ستطابق أي محرف ليس موجودًا في فئة المحارف المحددة: >>> consonantRegex = re.compile(r'[^aeiouAEIOU]') >>> consonantRegex.findall('RoboCop eats baby food. BABY FOOD.') ['R', 'b', 'C', 'p', ' ', 't', 's', ' ', 'b', 'b', 'y', ' ', 'f', 'd', '.', ' ', 'B', 'B', 'Y', ' ', 'F', 'D', '.'] فبدلًا من مطابقة الأحرف الصوتية، أصبحت فئة المحارف تطابق كل شيء عدا الأحرف الصوتية. رمز القبعة ورمز الدولار يمكننا أيضًا استخدام رمز القبعة ^ في بداية التعبير النمطي للإشارة أن المطابقة يجب أن تبدأ من بداية السلسلة النصية التي نبحث فيها عن النمط. وبالمثل يمكننا استخدام رموز الدولار $ في نهاية التعبير النمطي للإشارة أنه يجب أن تنتهي السلسلة النصية بالتعبير النمطي. يمكننا أن نستعمل الرمزين ^ و $ معًا للإشارة إلى أن السلسلة النصية كلها يجب أن تطابق التعبير النمطي، أي لا يسمح بإجراء مطابقة جزئية على السلسلة النصية فقط. لنأخذ مثالًا بسيطًا التعبير النمطي r'^Hello' الذي سيطابق أي سلسلة نصية تبدأ بالكلمة 'Hello': >>> beginsWithHello = re.compile(r'^Hello') >>> beginsWithHello.search('Hello, world!') <re.Match object; span=(0, 5), match='Hello'> >>> beginsWithHello.search('He said hello.') == None True التعبير النمطي r'\d$' يطابق السلاسل النصية التي تنتهي برقم من 0 إلى 9: >>> endsWithNumber = re.compile(r'\d$') >>> endsWithNumber.search('Your number is 42') <re.Match object; span=(16, 17), match='2'> >>> endsWithNumber.search('Your number is forty two.') == None True أما التعبير r'^\d+$' فهو يطابق السلاسل النصية التي تحتوي على رقم واحد على الأقل: >>> wholeStringIsNum = re.compile(r'^\d+$') >>> wholeStringIsNum.search('1234567890') <re.Match object; span=(0, 10), match='1234567890'> >>> wholeStringIsNum.search('12345xyz67890') == None True >>> wholeStringIsNum.search('12 34567890') == None True لاحظ أن آخر استدعائين للتابع search() يوضحان كيف يجب أن تطابق السلسلة النصية كلها النمط، وليس جزءًا منها. حرف البدل تسمى النقطة . في التعابير النمطية بحرف البدل wildcard، وهي تطابق أي محرف عدا السطر الجديد. فمثلًا: >>> atRegex = re.compile(r'.at') >>> atRegex.findall('The cat in the hat sat on the flat mat.') ['cat', 'hat', 'sat', 'lat', 'mat'] تذكر أن النقطة ستطابق محرفًا واحدًا فقط، وهذا هو السبب في أن التعبير النمطي طابق lat في السلسلة النصية flat فقط. إذا أردنا البحث عن رمز النقطة فلا ننسى تهريبه عبر خط مائل خلفي \.. مطابقة كل شيء مع النقطة والنجمة نحتاج أحيانًا إلى مطابقة كل شيء، فمثلًا لو أردنا مطابقة السلسلة النصية 'First Name:' متبوعةً بأي نصف، فحينها سنستعمل النقطة والنجمة .*. تذكر أن النقطة تعني أي حرف عدا السطر الجديد، والنجمة تعني تكرار النمط الذي يسبقها 0 مرة أو أكثر. >>> nameRegex = re.compile(r'First Name: (.*) Last Name: (.*)') >>> mo = nameRegex.search('First Name: Al Last Name: Sweigart') >>> mo.group(1) 'Al' >>> mo.group(2) 'Sweigart' تجري النقطة والنجمة مطابقةً جشعةً، أي أنها تحاول أن تطابق النصوص أكثر ما يمكن، ولجعلها مطابقة غير جشعة فيمكننا استخدام النقطة والنجمة وإشارة الاستفهام .*? وبالتالي ستطابق بايثون النمط مطابقةً غير جشعة. جرب المثال الآتي لترى الفرق بين النسخة الجشعة وغير الجشعة: >>> nongreedyRegex = re.compile(r'<.*?>') >>> mo = nongreedyRegex.search('<To serve man> for dinner.>') >>> mo.group() '<To serve man>' >>> greedyRegex = re.compile(r'<.*>') >>> mo = greedyRegex.search('<To serve man> for dinner.>') >>> mo.group() '<To serve man> for dinner.>' يمكننا ترجمة التعبيرين النمطيين السابقين إلى «طابق قوس بدء زاوية ثم أي شيء ثم قوس إغلاق زاوية»، لكن السلسلة النصية '<To serve man> for dinner.>' فيها مطابقتين لقوس إغلاق زاوية، ففي النسخة غير الجشعة تطابق بايثون أقصر سلسلة نصية ممكنة '<To serve man>'؛ أما في النسخة الجشعة فتحاول بايثون مطابقة أطول سلسلة نصية ممكنة: '<To serve man> for dinner.>'. مطابقة الأسطر الجديدة مع رمز النقطة تعلمنا أن النقطة والنجمة ستطابق كل المحارف عدا السطر الجديد، لكن بتمرير وسيط ثاني إلى الدالة re.compile() هو re.DOTALL فسنتمكن من استعمال النقطة لمطابقة جميع المحارف بما فيها السطر الجديد: >>> noNewlineRegex = re.compile('.*') >>> noNewlineRegex.search('Serve the public trust.\nProtect the innocent. \nUphold the law.').group() 'Serve the public trust.' >>> newlineRegex = re.compile('.*', re.DOTALL) >>> newlineRegex.search('Serve the public trust.\nProtect the innocent. \nUphold the law.').group() 'Serve the public trust.\nProtect the innocent.\nUphold the law.' التعبير النمطي noNewlineRegex لم نمرر إليه re.DOTALL حين استدعاء re.compile() لذا سيطابق النمط كل شيء حتى الوصول إلى محرف السطر الجديد؛ بينما newlineRegex مررنا إليه re.DOTALL حين استدعاء re.compile() لذا سيطابق كل شيء، لهذا أعاد استدعاء newlineRegex.search() السلسلة النصية كاملة بما فيها الأسطر الجديدة. مراجعة لرموز التعابير النمطية شرحنا الكثير من الرموز في هذا المقال، لنراجع سريعًا ما الذي تعلمناه حول أساسيات التعابير النمطية: يطابق ? المجموعة التي تسبقه 0 مرة أو 1 مرة. يطابق * المجموعة التي تسبقه 0 مرة أو أكثر. يطابق + المجموعة التي تسبقه 1 مرة أو أكثر. يطابق {n} المجموعة التي تسبقه n مرة تمامًا. يطابق {n,} المجموعة التي تسبقه n مرة أو أكثر. يطابق {, m} المجموعة التي تسبقه 0 مرة حتى m مرة. يطابق {n, m} المجموعة التي تسبقه n مرة على الأقل و m على الأكثر. تجري {n,m}? أو *? أو +? مطابقة غير جشعة. يطابق ^spam السلاسل النصية التي تبدأ بالكلمة spam. يطابق spam$ السلاسل النصية التي تنتهي بالكلمة spam. يطابق الرمز . أي محرف عدا السطر الجديد. تطابق فئة المحارف \d و \w و \s رقمًا أو كلمةً أو فراغًا على التوالي وبالترتيب. تطابق فئة المحارف \D و \W و \S أي شيء عدا أن يكون رقمًا أو كلمةً أو فراغًا على التوالي وبالترتيب. تطابق فئة المحارف [abc] أي شيء بين القوسين المربعين (مثل a أو b أو c). تطابق فئة المحارف [^abc] أي شيء ليس بين القوسين المربعين. المطابقة غير الحساسة لحالة الأحرف تطابق التعابير النمطية النص بنفس الحالة المحددة، فمثلًا تطابق التعابير النمطية الآتية سلاسل نصية مختلفة: >>> regex1 = re.compile('RoboCop') >>> regex2 = re.compile('ROBOCOP') >>> regex3 = re.compile('robOcop') >>> regex4 = re.compile('RobocOp') لكن في بعض الأحيان لا يهمنا ما هي حالة الأحرف، وكل ما نريده هو مطابقة النص بغض النظر عن حالته، فحينها نريد جعل التعابير النمطية غير حساسة لحالة الأحرف، وذلك بتمرير وسيطٍ ثانٍ للدالة re.compile() هو re.IGNORECASE أو re.I: >>> robocop = re.compile(r'robocop', re.I) >>> robocop.search('RoboCop is part man, part machine, all cop.').group() 'RoboCop' >>> robocop.search('ROBOCOP protects the innocent.').group() 'ROBOCOP' >>> robocop.search('Al, why does your programming book talk about robocop so much?').group() 'robocop' استبدال السلاسل النصية عبر التابع sub() لا تستعمل التعابير النمطية للعثور على أنماط من النصوص فقط، وإنما لاستبدالها أيضًا. يقبل التابع sub() في كائنات Regex معاملين، الأول هو السلسلة النصية التي نريد تبديل المطابقات إليها، والثاني هو السلسلة النصية التي نريد البحث فيها عن مطابقات لتبديلها. يعيد التابع sub() سلسلةً نصية بعد تطبيق جميع عمليات الاستبدال: >>> namesRegex = re.compile(r'Agent \w+') >>> namesRegex.sub('CENSORED', 'Agent Alice gave the secret documents to Agent Bob.') 'CENSORED gave the secret documents to CENSORED.' قد نحتاج أحيانًا إلى استخدام النص المطابق كجزء من النص الجديد، لذا يمكننا استخدام \1 و \2 و \3 في الوسيط الأول الممرر إلى التابع sub() للوصول إلى المجموعات الفرعية المطابقة. لنقل أننا نريد أن نخفي أسماء الأشخاص في مثالنا السابق، لكننا نريد إظهار الحرف الأول من اسمهم فقط، فحينها نستطيع استخدام التعبير النمطي Agent (\w)\w* وتمرير r'\1****' كأول معامل إلى التابع sub()، وستشير \1 إلى السلسلة النصية المطابقة من النمط الفرعي الأول، أي المجموعة (\w) في التعبير النمطي: >>> agentNamesRegex = re.compile(r'Agent (\w)\w*') >>> agentNamesRegex.sub(r'\1****', 'Agent Alice told Agent Carol that Agent Eve knew Agent Bob was a double agent.') A**** told C**** that E**** knew B**** was a double agent.' التعامل مع التعابير النمطية المعقدة التعابير النمطية التي تطابق نصًا بسيطًا تكون سهلة الفهم، لكن إذا أردنا مطابقة النصوص المعقدة فستصبح التعابير النمطية معقدة وطويلة، يمكننا التعامل مع هذا الإشكال بالطلب من الدالة re.compile() أن تتجاهل الفراغات والتعليقات داخل السلسلة النصية التي تمثل التعبير النمطي، وهذا النمط يسمى verbose mode، ويمكن تفعيله بتمرير re.VERBOSE إلى المعامل الثاني في الدالة re.compile(). فبدلًا من محاولة قراءة تعبير نمطي صعب مثل هذا: phoneRegex = re.compile(r'((\d{3}|\(\d{3}\))?(\s|-|\.)?\d{3}(\s|-|\.)\d{4} (\s*(ext|x|ext.)\s*\d{2,5})?)') نستطيع تقسيم التعبير النمطي إلى عدة أسطر مع تعليقات توضح وظيفة كل قسم: phoneRegex = re.compile(r'''( (\d{3}|\(\d{3}\))? # رمز المنطقة (\s|-|\.)? # فاصل \d{3} # أول 3 أرقام (\s|-|\.) # فاصل \d{4} # آخر 4 أرقام (\s*(ext|x|ext.)\s*\d{2,5})? # اللاحقة )''', re.VERBOSE) لاحظ أننا استعملنا علامة الاقتباس الثلاثية لإنشاء سلسلة نصية متعددة الأسطر، لكي نستطيع تقسيم التعبير على عدة أسطر وتسهيل قراءته. تتبع التعليقات داخل التعابير النمطية نفس قواعد التعليقات في بايثون: سيتم تجاهل كل شيء بعد الرمز #. لاحظ أن الفراغات الزائدة في السلسلة النصية متعددة الأسطر لا تؤثر على معنى التعبير النمطي ولا تعد جزءًا منه، لذا يمكنك استخدام الفراغات كيفما تشاء لتسهيل قراءة التعبير النمطي. استخدام re.IGNORECASE و re.DOTALL و re.VERBOSE ماذا لو أردنا استخدام re.VERBOSE لكتابة تعليقات في التعابير النمطية لكننا نريد أن نتجاهل حالة الأحرف أيضًا عبر re.IGNORECASE؟ للأسف لا تقبل الدالة re.compile() غير قيمة واحدة كثاني وسيط لها، لكن يمكننا تجاوز هذه المحدودية بجمع القيم re.IGNORECASE و re.DOTALL و re.VERBOSE مع بضعها عبر الخط العمودي | وهو يسمى في هذا السياق بعامل OR الثنائي Bitwise OR. أي أننا لو أردنا كتابة تعبير نمطي غير حساس لحالة الأحرف ويسمح بمطابقة محرف السطر الجديد برمز النقطة، فحينها سيكون استدعاء الدالة re.compile() كما يلي: >>> someRegexValue = re.compile('foo', re.IGNORECASE | re.DOTALL) ولو أردنا تضمين الخيارات كلها فنكتب: >>> someRegexValue = re.compile('foo', re.IGNORECASE | re.DOTALL | re.VERBOSE) أصل هذه الصيغة من الإصدارات القديمة من بايثون، وشرح العوامل الثنائية خارج سياق هذه السلسلة. لاحظ أن هنالك خيارات أخرى يمكن تمريرها إلى الدالة re.compile() لكنها غير شائعة الاستخدام، ويمكنك القراءة عنها في التوثيق الرسمي. مشروع: برنامج استخراج أرقام الهواتف وعناوين البريد الإلكتروني لنقل أنك موكل بمهمة مملة هي العثور على جميع أرقام الهواتف وعناوين البريد الإلكتروني في صفحة ويب طويلة أو مستند كبير. إذا كنت ستتصفح تلك الصفحة وتبحث عنها يدويًا فستأخذ منك وقتًا طويلًا وجهدًا كبيرًا؛ لكن إن كان لديك برنامج يمكنه البحث في الحافظة عن أرقام الهواتف وعناوين البريد الإلكتروني فسيكون ذلك رائعًا، فكل ما عليك فعله هو الضغط على Ctrl+A لتحديد كل النص ثم Ctrl+C لنسخه إلى الحافظة، ثم شغل برنامجك وسيعدل محتويات المحافظة ويضع فيها أرقام الهواتف وعناوين البريد الإلكتروني التي سيعثر عليها. قد يغريك حينما تبدأ بمشروع جديد أن تبدأ كتابة الشيفرات فورًا، لكن من الأفضل أن تنظر نظرةً شاملة على البرنامج قبل البدء بكتابته، وأنصحك أن ترسم خطةً بسيطةً لما يجب على برنامجك فعله، دون التفكير بالشيفرات وكيفية كتابتها، وإنما اكتب العناوين العريضة فقط. فمثلًا لو أردنا العمل على برنامج استخراج أرقام الهواتف وعناوين البريد الإلكتروني فسنحتاج إلى: الحصول على النص من الحافظة. العثور على جميع أرقام الهواتف وعناوين البريد الإلكتروني. لصق الناتج في الحافظة. يمكنك أن تبدأ الآن بالتفكير كيفية كتابة ذلك في بايثون. سيحتاج برنامجك إلى: استخدام الوحدة pyperclip لنسخ ولصق النصوص من الحافظة. إنشاء تعبيران نمطيان، واحد لمطابقة أرقام الهواتف والثاني لمطابقة عناوين البريد الإلكتروني. العثور على جميع المطابقات لكلي التعبيرين النمطيين، وليس أول مطابقة فقط. تنسيق الناتج في سلسلة نصية واحدة جاهزة ولصقها في الحافظة. عرض رسالة خطأ إن لم يعثر على مطابقات في النص. هذا هو المخطط العام للمشروع، وسنركز على كيفية حل كل خطوة حين كتابة الشيفرات. لاحظ أننا تعلمنا كيفية التعامل مع كل خطوة من الخطوات السابقة في بايثون. الخطوة 1: إنشاء تعبير نمطي لأرقام الهواتف علينا بداية إنشاء تعبير نمطي للبحث عن أرقام الهواتف، لننشِئ ملفًا باسم phoneAndEmail.py ونكتب فيه: #! python3 # phoneAndEmail.py - البحث عن أرقام الهواتف وعناوين البريد في الحافظة import pyperclip, re phoneRegex = re.compile(r'''( (\d{3}|\(\d{3}\))? # رمز المنطقة (\s|-|\.)? # فاصل (\d{3}) # أول 3 أرقام (\s|-|\.) # فاصل (\d{4}) # آخر 4 أرقام (\s*(ext|x|ext.)\s*(\d{2,5}))? # اللاحقة )''', re.VERBOSE) # TODO: إنشاء التعبير النمطي لعناوين البريد الإلكتروني # TODO: العثور على المطابقات في الحافظة # TODO: لصق الناتج في الحافظة التعليقات التي تبدأ بالكلمة TODO هي لبنية التطبيق، وسنبدلها إلى شيفرات لاحقًا. يبدأ رقم الهاتف برمز منطقة اختياري، لذا نجعل المجموعة الفرعية التي تدل على رمز المنطقة اختياريةً بإضافة علامة استفهام ?. ولأن رمز المنطقة يمكن أن يكون 3 أرقام (أي \d{3}) أو 3 أرقام ضمن أقواس (أي \(\d{3}\)) فاستعملنا الخط العمودي | للفصل بينهما. لاحظ أننا استطعنا إضافة التعليقات إلى التعبير النمطي متعدد الأسطر لكي نتذكر ماذا يفعل كل سطر. يمكن أن يكون الفاصل في أرقام الهواتف فراغًا \s أو شرطة - أو نقطة .، لذا فصلنا بين هذه الاحتمالات بخط عمودي. بقية أقسام التعبير النمطي واضحة وسهلة: 3 أرقام، يتبعها فاصل، يتبعها 4 أرقام، وآخر قسم هو اللاحقة الاختيارية التي تبدأ بأي عدد من الفراغات يليها الكلمة ext أو x أو ext.، ويليها رقمين إلى 5 أرقام. ملاحظة: من السهل أن نخلط بين التعابير النمطية التي تحتوي على مجموعات عبر استخدام الأقواس ()، وبين الأقواس المُهربة ) و (. تذكر أن تتأكد أنك تستخدم النوع الصحيح من الأقواس إن حصلت على رسالة خطأ تشير إلى قوسٍ ناقص. الخطوة 2: إنشاء تعبير نمطي لعناوين البريد الإلكتروني علينا الآن إنشاء تعبير نمطي لمطابقة عناوين البريد الإلكتروني: #! python3 # phoneAndEmail.py - البحث عن أرقام الهواتف وعناوين البريد في الحافظة import pyperclip, re phoneRegex = re.compile(r'''( --مقتطع-- # إنشاء التعبير النمطي لعناوين البريد الإلكتروني emailRegex = re.compile(r'''( ➊ [a-zA-Z0-9._%+-]+ # اسم المستخدم ➋ @ # @ إشارة ➌ [a-zA-Z0-9.-]+ # اسم النطاق (\.[a-zA-Z]{2,4}) # اللاحقة )''', re.VERBOSE) # TODO: العثور على المطابقات في الحافظة # TODO: لصق الناتج في الحافظة يكون اسم المستخدم جزءًا من البريد الإلكتروني ➊، وفيه محارف واحدة أو أكثر مما يلي: الأحرف الكبيرة والصغيرة، والأرقام، والنقطة، والشرطة السفلية، وإشارة النسبة المئوية، وإشارة زائد، وشرطة عادية. يمكننا جمع كل ما سبق في فئة المحارف الآتية [a-zA-Z0-9._%+-]. يفصل بين اسم المستخدم والنطاق برمز @ ➋؛ ولا يسمح اسم النطاق بنفس المحارف التي يسمح بها اسم المستخدم، ولذا يجب أن ننشِئ فئة محارف فيها الأرقام والأحرف والنقط والشرطات [a-zA-Z0-9.-] ➌. آخر جزء هو اللاحقة، مثل .com (يسمى تقنيًا بالنطاق في أعلى مستوى top-level domain) الذي هو رمز النقطة ويلي أي شيء، ويفترض أن يكون بين 2 و 4 محارف. هنالك قواعد كثيرة جدًا لمطابقة عناوين البريد الإلكتروني، والتعبير النمطي الذي كتبناه لن يطابق جميع عناوين البريد الصالحة قاعديًا، لكنه سيطابق جميع عناوين البريد الشائعة. الخطوة 3: العثور على جميع المطابقات في الحافظة أصبح لدينا الآن التعابير النمطية التي ستطابق أرقام الهواتف وعناوين البريد الإلكتروني عبر الوحدة re في بايثون، واستطعنا الوصول إلى محتويات الحافظة عبر الدالة paste() في الوحدة pyperclip. نحتاج الآن إلى استخدام التابع findall() للكائن Regex لإعادة قائمة من الصفوف. تأكد أن برنامجك يشبه البرنامج الآتي: #! python3 # phoneAndEmail.py - البحث عن أرقام الهواتف وعناوين البريد في الحافظة import pyperclip, re phoneRegex = re.compile(r'''( --مقتطع-- # العثور على المطابقات في الحافظة text = str(pyperclip.paste()) ➊ matches = [] ➋ for groups in phoneRegex.findall(text): phoneNum = '-'.join([groups[1], groups[3], groups[5]]) if groups[8] != '': phoneNum += ' x' + groups[8] matches.append(phoneNum) ➌ for groups in emailRegex.findall(text): matches.append(groups[0]) # TODO: لصق الناتج في الحافظة هنالك صف لكل مطابقة ناتجة، وكل صف يحتوي على السلاسل النصية في كل مجموعة في التعبير النمطي، تذكر أن المجموعة 0 تطابق كامل التعبير النمطي، لذا ما يهمنا من الناتج هو المجموعة في الفهرس 0. سنخزن المطابقات في قائمة باسم matches ➊، وسنبدأ برنامجنا بقائمة فارغة، ثم لدينا حلقتَي for. ففي الحلقة الخاصة بالبريد الإلكتروني نضيف محتويات المجموعة 0 إلى القائمة matches ➌؛ أما في الحلقة الخاصة بأرقام الهواتف فلا نريد أن نضيف ناتج المجموعة 0 إلى القائمة. صحيحٌ أن برنامجنا يستطيع مطابقة أرقام الهواتف بأكثر من صيغة، لكننا نريد إضافة أرقام الهواتف بصيغة واحدة معيارية، لذا ننشِئ المتغير phoneNum الذي يحتوي على قيمة المجموعات 1 و 3 و 5 و 8 من النص المطابق، وهي تمثل رقم المنطقة، وأول 3 أرقام، وآخر 4 أرقام، واللاحقة على التوالي وبالترتيب. الخطوة 4: جمع المطابقات في سلسلة نصية ونسخها إلى الحافظة أصبح لدينا الآن عناوين البريد الإلكتروني وأرقام الهواتف كقائمة من السلاسل النصية المخزنة في matches، إذا أردنا نسخ هذه القائمة إلى الحافظة فسنستعمل الدالة pyperclip.copy() التي تقبل سلسلة نصية، لكن المطابقات موجودة لدينا في قائمة، لذا نحن بحاجة إلى استخدام التابع join() على القائمة matches. للتأكد أن برنامجنا يعمل كما ينبغي له، فلنطبع أي مطابقات إلى الطرفية، وإذا لم تطابق أي عناوين بريدية أو أرقام هواتف فسيخبر البرنامجُ المستخدمَ بذلك. #! python3 # phoneAndEmail.py - البحث عن أرقام الهواتف وعناوين البريد في الحافظة --مقتطع-- for groups in emailRegex.findall(text): matches.append(groups[0]) # Copy results to the clipboard. if len(matches) > 0: pyperclip.copy('\n'.join(matches)) print('Copied to clipboard:') print('\n'.join(matches)) else: print('No phone numbers or email addresses found.') تشغيل البرنامج للتجربة، افتح المتصفح وتوجه إلى صفحة تواصل معنا من موقع nostarch.com، ثم حددها كلها بالضغط على Ctrl+A ثم انسخها إلى الحافظة Ctrl+C، ثم شغل البرنامج. يجب أن يكون الناتج كما يلي: Copied to clipboard: 800-420-7240 415-863-9900 415-863-9950 info@nostarch.com media@nostarch.com academic@nostarch.com info@nostarch.com أفكار لمشاريع مشابهة هنالك تطبيقات كثيرة لعملية مطابقة أنماط من النص (واستبدلها عبر التابع sub())، فمثلًا يمكنك: العثور على جميع روابط URL التي تبدأ بالسابقة http:// أو https://. العثور على التواريخ بصيغها المختلفة مثل 3/14/2022 أو 03-14-2022 أو 2022/3/14 وتبديلها إلى صيغة واحدة قياسية موحدة. إزالة البيانات الحساسة مثل أرقام البطاقات البنكية من المستندات. العثور على الأخطاء الشائعة مثل الفراغات المكررة بين الكلمات، أو الكلمات المكررة خطأً، أو علامات الترقيم المكررة. الخلاصة صحيحٌ أن الحواسيب قادرة على البحث عن النصوص بسرعة، لكن يجب أن نخبرها تحديدًا ما الذي عليها البحث عنه. تسمح لنا التعابير النمطية أن نحدد نمطًا من المحارف الذي نريد البحث عنه، وتستخدم التعابير النمطية في البرمجة أو حتى في محررات النصوص أو مع برامج معالجة جداول البيانات. تأتي الوحدة re مع أساس لغة بايثون، وتسمح لنا ببناء كائنات Regex، التي تمتلك عددًا من التوابع: التابع search() للبحث عن مطابقة واحدة، والتابع findall() للعثور على جميع المطابقات، والتابع sub() لإجراء عمليات البحث والاستبدال. ستجد المزيد من المعلومات في توثيق بايثون الرسمي عن التعابير النمطية، وفي موقع regular-expressions.info. مشاريع تدريبية لكي تتدرب، اكتب برامج لتنفيذ المهام الآتية. التحقق من التاريخ اكتب تعبيرًا نمطيًا يستطيع التعرف على التواريخ بالصيغة DD/MM/YYYY، افترض أن الأيام تتراوح بين 01 إلى 31، والأشهر من 01 إلى 12، والسنوات من 1000 إلى 2999. لاحظ أنه إذا كان اليوم أو الشهر متألف من رقم واحد فيجب أن يسبق بصفر 0. لا حاجة أن يتعرف التعبير النمطي على الأرقام الصحيحة لكل شهر من أشهر السنة، أو إذا كانت السنوات كبيسة؛ فلا بأس أن يقبل تاريخًا مثل 31/02/2022 أو 31/04/2022. خزن السلاسل النصية الناتج في متغيرات باسم month و day و year، ثم اكتب شيفرة تتحقق إن كان التاريخ صالحًا، فأشهر نسيان/أبريل وحزيران/يونيو وأيلول/سبتمبر وتشرين الثاني/نوفمبر لها 30 يومًا، وشهر شباط/فبراير له 28 يومًا، وبقية الأشهر 31 يومًا. يكون طول شهر شباط/فبراير 29 يومًا في السنوات الكبيسة، والسنة الكبيرة هي كل سنة تكون قابلة للقسمة على 4، عدا السنوات القابلة للقسمة على 100؛ ما لم تكن السنة قابلة للقسم على 400. لاحظ أن هذه العمليات الحسابية تجعل من المستحيل كتابة تعبير نمطي يمكنه فعل كل ذلك. التحقق من كلمة مرور قوية اكتب دالةً تستخدم التعابير النمطية للتأكد أن كلمة المرور الممررة إليها قوية. يمكننا تعريف كلمة المرور القوية أنها بطول 8 محارف على الأقل، وتحتوي على أحرف كبيرة وصغيرة، وفيها رقم واحد على الأقل. قد تحتاج إلى اختبار كلمة المرور على أكثر من تعبير نمطي للتأكد من قوتها. نسخة من الدالة strip() باستخدام التعابير النمطية اكتب دالةً تقبل سلسلةً نصيةً وتفعل فيها كما تفعله الدالة strip() التي تعلمناها في المقال السابق. إذا لم يمرر إليها أي وسائل بخلاف السلسلة النصية، فسنحذف جميع الفراغات من بداية ونهاية السلسلة النصية؛ وإلا فسنحذف المحارف المحددة في الوسيط الثاني الممرر إلى الدالة. ترجمة -بتصرف- للفصل Pattern Matching With Regular Expressions من كتاب Automate the Boring Stuff with Python. اقرأ أيضًا المقال السابق: معالجة النصوص باستخدام لغة بايثون python التعابير النمطية في البرمجة القوائم Lists في لغة بايثون تهيئة بيئة العمل في بايثون Python
-
النصوص هي أكثر أنواع البيانات التي ستتعامل معها في برنامجك. لقد تعلمت كيفية جمع سلسلتين نصيتين مع بعضها عبر العامل +، لكنك تستطيع أكثر من ذلك بكثير؛ إذ تستطيع استخراج سلاسل نصية فرعية، وإضافة وحذف الفراغات، وتحويل الأحرف إلى أحرف كبيرة أو صغيرة، والتحقق من تنسيق السلاسل تنسيقًا صحيحًا؛ وتستطيع أيضًا كتابة برنامج بايثون للوصول إلى الحافظة في حاسوبك لنسخ ولصق النصوص. كل ما سبق ستتعلمه في هذا المقال وسنزيد على ذلك؛ وسنرى مشروعين عمليين: حافظة بسيطة التي تخزن عدة سلاسل نصية من النصوص، وبرنامج لأتمتة العملية المملة لتنسيق النصوص يدويًا. التعامل مع السلاسل النصية لنلقِ نظرةً على الطرائق التي تسمح لنا فيها بايثون بالكتابة والطباعة والوصول إلى السلاسل النصية في برامجنا. السلاسل النصية المجردة كتابة القيم النصية في شيفرة بايثون هو أمر بالغ السهولة: نبدأ وننتهي بعلامة اقتباس مفردة. لكن ماذا سيحدث لو أردنا استخدام علامة اقتباس داخل السلسلة النصية؟ إذا كتبنا 'That is Alice's cat.' فسيحدث خطأ لأن بايثون ستظن أن السلسلة النصية قد انتهت بعد Alice وبقية السطر s cat.' هي شيفرة غير صالحة في بايثون. لحسن الحظ هنالك عدة طرائق لكتابة السلاسل النصية. علامات الاقتباس المزدوجة يمكن أن تبدأ السلاسل النصية وتنتهي عبر كتابة علامتي اقتباس مزدوجتين، وهي تعمل كما في علامات الاقتباس المفردة. إحدى فوائد استخدام علامات الاقتباس المزدوجة هي إمكانية كتابة علامة الاقتباس الفردية داخلها: >>> spam = "That is Alice's cat." ولأن السلسلة النصية تبدأ بعلامة اقتباس مزدوجة، فستعلم بايثون أن علامة الاقتباس المفردة هي جزء من السلسلة النصية ولا تمثل نهايتها. لكن ماذا لو احتجنا إلى استخدام علامات الاقتباس المفردة والمزدوجة معًا في السلسلة النصية؟ سنحتاج هنا إلى محارف التهريب. محارف التهريب تسمح لنا محارف التهريب escape characters باستخدام محارف ليس من الممكن إدراجها مباشرةً في السلسلة النصية. يتألف محرف التهريب من خط مائل خلفي backslash \ متبوعًا بأحد المحارف التي نريد إضافتها إلى السلسلة النصية؛ وصحيحٌ أن اسمه هو «محرف» التهريب، لكن يتألف من محرفين ويشار إليه عادةً بصيغة المفرد escape character. فمثلًا محرف التهريب لعلامة الاقتباس المفردة هو \' وذب الشيفرة الآتية: >>> spam = 'Say hi to Bob\'s brother.' ولأننا وضعنا خط مائل خلفي قبل علامة الاقتباس المفردة في Bob\'s فستعلم بايثون أننا لا نقصد أن ننهي السلسلة النصية. تسمح لنا محارف التهريب\' و \" بوضع علامات الاقتباس داخل السلاسل النصية المحاطة بعلامات اقتباس مفردة ومزدوجة على الترتيب. الجدول 6-1 يوضح بعض محارف التهريب التي تستطيع استخدامها. فيما يلي محارف التهريب: \': علامة اقتباس مفردة \": علامة اقتباس مزدوجة \t: علامة الجدولة tab \n: سطر جديد newline \\: خط مائل خلفي جرب السلسلة النصية الآتية: >>> print("Hello there!\nHow are you?\nI\'m doing fine.") Hello there! How are you? I'm doing fine. السلاسل النصية الخام يمكنك وضع الحرف r قبل علامة الاقتباس في بداية السلسلة النصية لجعلها سلسلة نصية خام. السلسلة النصية الخام raw string تتجاهل جميع محارف التهريب وتظهر جميع الخطوط المائلة الخلفية كما هي: >>> print(r'That is Carol\'s cat.') That is Carol\'s cat. ولأنها سلسلة نصية خام فستعدّ بايثون الخط الخلفي المائل على أنه جزء من السلسلة النصية وليس جزءًا من محرف التهريب. يمكن أن تكون السلاسل النصية الخام مفيدةً إن كانت تكتب سلاسل نصية فيها عدد من الخطوط المائلة الخلفية، مثل مسارات الملفات في نظام ويندوز r'C:\Users\Al\Desktop' أو التعابير النمطية regular expressions المشروحة في المقال القادم. السلاسل النصية متعددة الأسطر بعلامات اقتباس ثلاثية صحيحٌ أن بإمكاننا استخدام محرف التهريب \n لطابعة سطر جديد داخل سلسلة نصية، لكن من الأسهل استخدام السلاسل النصية متعددة الأسطر. تبدأ السلسلة النصية متعددة الأسطر في بايثون بثلاث علامات اقتباس مفردة أو مزدوجة، وستعد جميع علامات الاقتباس ومسافات الجدولة tabs والأسطر الجديدة على أنها جزءٌ من السلسلة النصية المحاطة بعلامات اقتباس ثلاثية. لاحظ أن قواعد تنسيق بايثون الخاصة بالمسافات البادئة في الكتل البرمجية لا تنطبق على السلاسل النصية متعددة الأسطر. print('''Dear Alice, Eve's cat has been arrested for catnapping, cat burglary, and extortion. Sincerely, Bob''') احفظ ما سبق في ملف باسم catnapping.py وشغله: Dear Alice, Eve's cat has been arrested for catnapping, cat burglary, and extortion. Sincerely, Bob لاحظ أن علامة الاقتباس المفردة في Eve's لا تحتاج إلى تهريب، فتهريب علامات الاقتباس المفردة والمزدوجة هو أمرٌ اختياري حين استخدام السلاسل النصية متعددة الأسطر. السلسلة النصية الموجودة في دالة print() الآتية تكافئ المثال السابق دون استخدام السلاسل النصية متعددة الأسطر: print('Dear Alice,\n\nEve\'s cat has been arrested for catnapping, cat burglary, and extortion.\n\nSincerely,\nBob') التعليقات متعددة الأسطر صحيحٌ أن رمز المربع # يرمز إلى بداية تعليق حتى نهاية السطر، لكن من الشائع استخدام سلسلة نصية متعددة الأسطر للتعليقات التي تمتد لأكثر من سطر. الشيفرات الآتية صالحة تمامًا في بايثون: """This is a test Python program. Written by Al Sweigart al@inventwithpython.com This program was designed for Python 3, not Python 2. """ def spam(): """This is a multiline comment to help explain what the spam() function does.""" print('Hello!') فهرسة وتقسيم السلاسل النصية تستعمل السلاسل النصية الفهارس ويمكن تقسيمها كما في القوائم lists. يمكنك أن تعدّ السلسلة النصية 'Hello, world!' على أنها قائمة وكل حرف فيها يمثل عنصرًا في تلك القائمة مع فهرس مرتبط به. ' H e l l o , w o r l d ! ' 0 1 2 3 4 5 6 7 8 9 10 11 12 لاحظ تضمين الفراغ وإشارة التعجب، وسيكون عدد الأحرف هو 13، بدءًا من الحرف H في الفهرس 0 إلى الحرف ! في الفهرس 12. >>> spam = 'Hello, world!' >>> spam[0] 'H' >>> spam[4] 'o' >>> spam[-1] '!' >>> spam[0:5] 'Hello' >>> spam[:5] 'Hello' >>> spam[7:] 'world!' إذا حددت فهرسًا فستحصل على المحرف الموجود في ذاك الموضع في السلسلة النصية، وإذا حددت مجالًا من فهرسٍ ما إلى آخر فسيُضمَّن المحرف الموجود في فهرس البداية ولن يضمن المحرف الموجود في فهرس النهاية، ولذا إذا كان لدينا المتغير spam الذي فيه 'Hello, world!' فإن spam[0:5] هو 'Hello'؛ فالسلسلة النصية الجزئية التي ستحصل عليها من spam[0:5] ستتضمن كل محرف موجود في المجال spam[0] حتى spam[4] ولن تحتوي الفاصلة الموجودة في الفهرس 5 ولا الفراغ في المحرف 6. هذا السلوك يشبه سلوك الدالة range(5) التي ستؤدي -حين استعمالها مع for- إلى المرور على الأرقام حتى الرقم 5 دون تضمينه. لاحظ أن تقسيم السلاسل النصية لا يغير السلسلة النصية الأصلية، ويمكننا حفظ القيمة الناتجة في متغير منفصل: >>> spam = 'Hello, world!' >>> fizz = spam[0:5] >>> fizz 'Hello' بتقسيم السلسلة النصية ثم تخزينها في متغير آخر فسنتمكن من الوصول إلى السلسلة النصية الأصلية والسلسلة النصية المقتطعة بسهولة. العوامل in و not in مع السلاسل النصية يمكن استخدام العاملين in و not in مع السلاسل النصية كما في القوائم lists. التعبير البرمجي الذي فيه سلسلتين نصيتين مجموعٌ بينها بالعامل in أو not in سينتج القيمة المنطقية True أو False: >>> 'Hello' in 'Hello, World' True >>> 'Hello' in 'Hello' True >>> 'HELLO' in 'Hello, World' False >>> '' in 'spam' True >>> 'cats' not in 'cats and dogs' False تختبر التعابير البرمجية السابقة إن كانت السلسلة النصية الأولى (كما هي بحذافيرها، مع حالة الأحرف فيها) موجودةً في السلسلة النصية الثانية. وضع السلاسل النصية داخل سلاسل نصية أخرى من الشائع في البرمجة وضع سلسلة نصية داخل سلسلة نصية أخرى، واستعملنا حتى الآن العامل + لجمع السلاسل النصية كما يلي: >>> name = 'Al' >>> age = 4000 >>> 'Hello, my name is ' + name + '. I am ' + str(age) + ' years old.' 'Hello, my name is Al. I am 4000 years old.' لكن ذلك يتطلب كتابةً كثيرة، ومن الأسهل أن ندس السلاسل النصية داخل بعضها string interpolation، وبهذه الطريقة نستعمل العامل %s داخل السلاسل النصية كمؤشر لكي يستبدل مسبقًا إلى القيم التي تلي السلسلة النصية. إحدى ميزات استخدام هذه الطريقة هو عدم حاجتنا إلى استدعاء الدالة str() لتحويل القيم إلى سلاسل نصية: >>> name = 'Al' >>> age = 4000 >>> 'My name is %s. I am %s years old.' % (name, age) 'My name is Al. I am 4000 years old.' أضافت بايثون 3.6 ما يسمى بالسلاسل النصية المنسقة f-strings، وهي تشبه دس السلاسل النصية لكنها تتيح إضافة التعابير البرمجية بين قوسين مجعدين مباشرةً؛ تذكر أن تضيف الحرف f قبل علامة الاقتباس الابتدائية في السلسلة النصية المنسقة: >>> name = 'Al' >>> age = 4000 >>> f'My name is {name}. Next year I will be {age + 1}.' 'My name is Al. Next year I will be 4001.' إذا لم تتذكر تضمين الحرف f قبل علامة الاقتباس فستعامل الأقواس على أنها جزء من السلسلة النصية: >>> 'My name is {name}. Next year I will be {age + 1}.' 'My name is {name}. Next year I will be {age + 1}.' توابع مفيدة للتعامل مع السلاسل النصية يشرح هذا القسم أكثر التوابع شيوعًا التي تحلل السلاسل النصية وتعالجها وتنتج سلاسل نصية معدلة. التوابع upper() و lower() و isupper() و islower() يعيد التابعان upper() و lower() سلسلةً نصيةً جديدةً تحوَّل فيها أحرف السلسلة النصية الأصلية إلى حالة الأحرف الكبيرة أو الصغيرة على التوالي. أما المحارف غير النصية أو غير اللاتينية فتبقى كما هي: >>> spam = 'Hello, world!' >>> spam = spam.upper() >>> spam 'HELLO, WORLD!' >>> spam = spam.lower() >>> spam 'hello, world!' لاحظ أن هذان التابعان لا يغيران السلسلة النصية الأصلية وإنما يعيدان سلسلةً نصيةً جديدةً. إذا أردت تعديل السلسلة النصية الأصلية فعليك استدعاء التابع upper() أو lower() على السلسلة النصية ثم إسناد الناتج مباشرةً إلى المتغير الذي يحتوي على السلسلة النصية الأصلية؛ أي أننا سنكتب شيئًا يشبه spam = spam.upper() لتعديل السلسلة النصية المخزنة في المتغير spam بدلًا من كتابة spam.upper() فقط، وهذا يشبه حالة وجود متغير اسمه eggs يحتوي القيمة 10، فكتابة التعبير البرمجي eggs + 3 لا يؤدي إلى تغيير القيمة المخزنة في المتغير eggs مباشرة، وإنما علينا كتابة eggs = eggs + 3. التابعان upper() و lower() مفيدان إن أردنا مقارنة سلسلتين نصيتين مقارنةً غير حساسةٍ لحالة الأحرف. فمثلًا السلسلتان النصيتان 'great' و 'GREat' غير متساويتين؛ لكن قد لا يهمنا في بعض الحالات التي نطلب فيها مدخلات المستخدم إن كان قد كتب Great أم GREAT أم gREAt. انظر المثال الآتي الذي نحول فيه السلسلة النصية إلى حالة الأحرف الصغيرة: print('How are you?') feeling = input() if feeling.lower() == 'great': print('I feel great too.') else: print('I hope the rest of your day is good.') حينما تشغل البرنامج السابق فسيظهر لك سؤال، وإذا أدخلت الكلمة great بأي شكل من الأشكال فستظهر لك العبارة 'I feel great too.'. من المفيد جدًا إضافة آلية للتعامل مع اختلاف حالات الأحرف في مدخلات المستخدم في برامجك، إذ سيسهل ذلك استخدامها كثيرًا. How are you? GREat I feel great too. يعيد التابعان isupper() و islower() قيمةً منطقية True إن كانت السلسلة النصية تحتوي على حرف واحد على الأقل وكان ذاك الحرف كبيرًا أو صغيرًا على التوالي وبالترتيب؛ وإلا فستعيد القيمة False: >>> spam = 'Hello, world!' >>> spam.islower() False >>> spam.isupper() False >>> 'HELLO'.isupper() True >>> 'abc12345'.islower() True >>> '12345'.islower() False >>> '12345'.isupper() False ولأن القيمة المعادة من التابعين upper() و lower() هي سلاسل نصية، فيمكننا استدعاء توابع التعامل مع السلاسل النصية على القيم المعادة من تلك التوابع مباشرةً، وتبدو هذه التعابير البرمجية على أنها سلسلة من التوابع وراء بعضها كما في المثال الآتي: >>> 'Hello'.upper() 'HELLO' >>> 'Hello'.upper().lower() 'hello' >>> 'Hello'.upper().lower().upper() 'HELLO' >>> 'HELLO'.lower() 'hello' >>> 'HELLO'.lower().islower() True مجموعة توابع isX() بالإضافة إلى التابعين islower() و isupper() هنالك عدد من التوابع التي يبدأ اسمها بالكلمة is، وتعيد تلك التوابع قيمةً منطقية التي تشرح طبيعة السلسلة النصية. هذه بعض تلك التوابع: isalpha() يعيد True إذا احتوت السلسلة النصية على أحرف عادية فقط ولم تكن فارغة. isalnum() يعيد True إذا احتوت السلسلة النصية على أحرف عادية وأرقام فقط ولم تكن فارغة. isdecimal() يعيد True إذا احتوت السلسلة النصية على أرقام فقط ولم تكن فارغة. isspace() يعيد True إذا احتوت السلسلة النصية على فراغات عادية ومسافات جدولة tabs وأسطر جديدة newlines فقط ولم تكن فارغة. istitle() يعيد True إذا بدأت السلسلة النصية بحرف كبير متبوعٌ بمجموعة من الأحرف الصغيرة. لنجرب تلك التوابع عمليًا: >>> 'hello'.isalpha() True >>> 'hello123'.isalpha() False >>> 'hello123'.isalnum() True >>> 'hello'.isalnum() True >>> '123'.isdecimal() True >>> ' '.isspace() True >>> 'This Is Title Case'.istitle() True >>> 'This Is Title Case 123'.istitle() True >>> 'This Is not Title Case'.istitle() False >>> 'This Is NOT Title Case Either'.istitle() False تفيد توابع السلاسل النصية isX() حينما نريد التحقق من مدخلات المستخدم، فالبرنامج الآتي يطلب من المستخدم إدخال عمره وكلمة مرور صالحة: while True: print('Enter your age:') age = input() if age.isdecimal(): break print('Please enter a number for your age.') while True: print('Select a new password (letters and numbers only):') password = input() if password.isalnum(): break print('Passwords can only have letters and numbers.') طلبنا من المستخدم في أول حلقة while أن يدخل عمره، ونخزنه في المتغير age. إذا كانت قيمة age هي قيمة عددية صحيحة، فسنخرج من أول حلقة while وندخل في الثانية التي تسأله عن كلمة المرور. وإلا فسنسأل المستخدم عن عمره مجددًا. في حلقة while الثانية طلبنا كلمة المرور وخزناها في المتغير password، وسنخرج من الحلقة إن كانت كلمة المرور تتألف من أرقام أو أحرف، وإلا فسنطلب من المستخدم إدخال كلمة مرور صالحة مجددًا. Enter your age: forty two Please enter a number for your age. Enter your age: 42 Select a new password (letters and numbers only): secr3t! Passwords can only have letters and numbers. Select a new password (letters and numbers only): secr3t تمكّنا من التحقق من صلاحية مدخلات المستخدم في المثال السابق باستخدام التابعين isdecimal() و isalnum()، ولم نقبل كتابة forty two بل قبلنا 42، ولم نقبل secr3t! بل قبلنا secr3t. التابعان startswith() و endswith() يعيد التابعان startswith() و endswith() القيمة True إن بدأت أو انتهت السلسلة النصية التي استدعت عليها (على التوالي) بالسلسلة النصية الممررة إلى التابع؛ وإلا فستعيد False: >>> 'Hello, world!'.startswith('Hello') True >>> 'Hello, world!'.endswith('world!') True >>> 'abc123'.startswith('abcdef') False >>> 'abc123'.endswith('12') False >>> 'Hello, world!'.startswith('Hello, world!') True >>> 'Hello, world!'.endswith('Hello, world!') True هذه التوابع هي بديل مفيد لعامل المساواة == إذا أردنا التحقق من الجزء الأول أو الأخير من السلسلة النصية فقط، بدلًا من مقارنتها كلها. التابعان join() و split() يفيد التابع join() حينما يكون لدينا قائمة فيها سلاسل نصية ونريد أن نجمعها كلها مع بعضها بعضًا في سلسلة نصية واحدة؛ ويستدعى التابع join() على سلسلة نصية، ويقبل معاملًا هو قائمة list فيها سلاسل نصية، ويعيد سلسلةً نصيةً تساوي دمج السلاسل النصية كلها: >>> ', '.join(['cats', 'rats', 'bats']) 'cats, rats, bats' >>> ' '.join(['My', 'name', 'is', 'Simon']) 'My name is Simon' >>> 'ABC'.join(['My', 'name', 'is', 'Simon']) 'MyABCnameABCisABCSimon' لاحظ أن السلسلة النصية التي استدعينا عليها التابع join() أصبحت موجودة بين كل سلسلتين نصيتين في القائمة الممررة كوسيط. فمثلًا حين استدعاء join(['cats', 'rats', 'bats']) على السلسلة النصية ', ' فيكون الناتج هو 'cats, rats, bats'. تذكر أن التابع join() يستدعى على سلسلة نصية ونمرر إليه قائمة، وليس العكس. يفعل التابع split() عكس فعل التابع join() تمامًا: يستدعى على سلسلة نصية وتعيد قائمةً من السلاسل النصية: >>> 'My name is Simon'.split() ['My', 'name', 'is', 'Simon'] ستُقسَم السلسلة النصية 'My name is Simon' افتراضيًا عند كل محرف يمثل فراغًا (سواءً كان فراغًا عاديًا ' ' أو محرف جدولة tab أو سطرًا جديدًا newline)، ولن تضمن الفراغات في السلاسل النصية الموجودة في القائمة المعادة من استدعاء هذا التابع. يمكننا تمرير محرف الفصل إلى التابع split() لتحديد محرف آخر غير محارف الفراغات: >>> 'MyABCnameABCisABCSimon'.split('ABC') ['My', 'name', 'is', 'Simon'] >>> 'My name is Simon'.split('m') ['My na', 'e is Si', 'on'] من الشائع استدعاء التابع split() لتقسيم سلسلة نصية متعددة الأسطر في مكان وقوع محرف السطر الجديد: >>> spam = '''Dear Alice, How have you been? I am fine. There is a container in the fridge that is labeled "Milk Experiment." Please do not drink it. Sincerely, Bob''' >>> spam.split('\n') ['Dear Alice,', 'How have you been? I am fine.', 'There is a container in the fridge', 'that is labeled "Milk Experiment."', '', 'Please do not drink it.', 'Sincerely,', 'Bob'] يسمح لنا استدعاء التابع split() مع تمرير محرف السطر الجديد '\n' إلى تقسيم سلسلة نصية متعددة الأسطر إلى قائمة يمثل فيها كل عنصر سطرًا من الأسطر. تقسيم السلاسل النصية باستخدام التابع partition() يمكن أن يقسم التابع partition() سلسلةً نصية إلى أقسام، ويعمل بتمرير سلسلة نصية إليه كفاصل، التي سيبحث عنها في السلسلة النصية التي استدعي عليها، ويعيد صفًا tuple فيه السلسلة النصية التي تسبق الفاصل، والفاصل، والسلسلة النصية التي تلي الفاصل: >>> 'Hello, world!'.partition('w') ('Hello, ', 'w', 'orld!') >>> 'Hello, world!'.partition('world') ('Hello, ', 'world', '!') إذا احتوت السلسلة النصية التي تستدعي التابع partition() عليها على أكثر من تكرار للفاصل، فستقسم السلسلة النصية عند أول وقوع له فقط: >>> 'Hello, world!'.partition('o') ('Hell', 'o', ', world!') وإن لم يعثر على الفاصل في السلسلة النصية، فستعاد السلسلة النصية التي استدعي عليها التابع كأول عنصر في الصف، وستكون السلسلتان النصيتان الباقيتان فارغتين: >>> 'Hello, world!'.partition('XYZ') ('Hello, world!', '', '') يمكننا استخدام نشر المتغيرات بالإسناد الجماعي لإسناد السلاسل النصية المعادة إلى ثلاثة متغيرات: >>> before, sep, after = 'Hello, world!'.partition(' ') >>> before 'Hello,' >>> after 'world!' يفيد التابع partition() إن كنت تحتاج إلى الحصول على السلسلة النصية التي تسبق فاصلًا محددًا، والفاصل نفسه، والسلسلة النصية التي تلي ذاك الفاصل. محاذاة النصوص عبر rjust() و ljust() و center() يعيد التابعان rjust() و ljust() سلسلةً نصية محاطة بفراغات افتراضيًا. يمثل أول وسيط يمرر إلى تلك التوابع قيمةً لعدد الفراغات المحيطة بالسلسلة النصية: >>> 'Hello'.rjust(10) ' Hello' >>> 'Hello'.rjust(20) ' Hello' >>> 'Hello, World'.rjust(20) ' Hello, World' >>> 'Hello'.ljust(10) 'Hello ' التابع 'Hello'.rjust(10) يعني أننا نريد محاذاة السلسلة النصية 'Hello' إلى اليمين ويكون طول السلسلة النصية الكاملة هو 10. ولما كانت 'Hello' هي 5 محارف، فستضاف 5 فراغات على يسارها، مما يؤدي إلى إعادة سلسلة نصية طولها 10 محارف وتكون فيها محاذاة 'Hello' على اليمين. وسيطٌ اختياري للتابعين rjust() و ljust() يحدد محرفًا للملء بخلاف الفراغ: >>> 'Hello'.rjust(20, '*') '***************Hello' >>> 'Hello'.ljust(20, '-') 'Hello---------------' التابع center() يعمل مثل التابعين ljust() و rjust() لكنه يوسِّط النص بدلًا من محاذاته إلى اليسار أو اليمين: >>> 'Hello'.center(20) ' Hello ' >>> 'Hello'.center(20, '=') '=======Hello========' تفيد هذه التوابع حينما نريد طباعة جداول من البيانات ويكون لها تباعد صحيح. اكتب البرنامج الآتي واحفظه في الملف picnicTable.py: def printPicnic(itemsDict, leftWidth, rightWidth): print('PICNIC ITEMS'.center(leftWidth + rightWidth, '-')) for k, v in itemsDict.items(): print(k.ljust(leftWidth, '.') + str(v).rjust(rightWidth)) picnicItems = {'sandwiches': 4, 'apples': 12, 'cups': 4, 'cookies': 8000} printPicnic(picnicItems, 12, 5) printPicnic(picnicItems, 20, 6) عرفنا الدالة printPicnic() التي تأخذ قاموسًا كمعامل لها، واستخدمنا التوابع center() و ljust() و rjust() لطباعة المعلومات بصيغة جميلة تشبه الجدول. القاموس الذي سنمرره إلى الدالة printPicnic() هو picnicItems، ولدينا في القاموس picnicItems 4 صندويشات و 12 تفاحة و 4 كاسات و 8,000 كعكة (نعم ثمانية آلاف!). نرغب بتنظيم هذه المعلومات في عمودين، ونضع اسم العنصر على اليسار والكمية على اليمين. لفعل ذلك نحتاج إلى أن نقرر كم سيكون عرض العمودين الأيسر والأيمن؛ لذا نحتاج إلى تمرير هاتين القيمتين مع القاموس إلى الدالة printPicnic() عبر المعاملين leftWidth لعرض العمود الأيسر من الجدول و rightWidth لعرض العمود الأيمن من الجدول. ستطبع الدالة printPicnic() عنوانًا متوسطًا فوق العنصر PICNIC ITEMS، ثم ستمر على عناصر القاموس وتطبع كل زوج من القيم في سطر وتحاذي المفتاح على اليسار وله حاشية متألفة من فواصل، والقيمة على اليمين ولها حاشية متألف من فراغات. بعد تعريف الدالة printPicnic() سنعرف القاموس picnicItems ونستدعي الدالة printPicnic() مرتين، مع تمرير عرض مختلف للعمودين الأيسر والأيمن؛ يكون فيها عرض العمود الأيسر 12 والأيمن 5 في المرة الأولى، ثم 20 و 6 في المرة الثانية. ---PICNIC ITEMS-- sandwiches.. 4 apples...... 12 cups........ 4 cookies..... 8000 -------PICNIC ITEMS------- sandwiches.......... 4 apples.............. 12 cups................ 4 cookies............. 8000 رأينا كيف استفدنا من rjust() و ljust() و center() لطباعة سلاسل نصية منسقة تنسيقًا جميلًا، حتى لو لم نكن نعرف كم سيكون العدد الدقيق لمحارف السلسلة النصية التي سنطبعها. حذف الفراغات عبر strip() و rstrip() و lstrip() نحتاج أحيانًا إلى حذف الفراغات البيضاء (التي هي محارف الفراغ العادي ومسافة الجدولة tab والسطر الجديد newline) من الطرف الأيسر أو الأيمن أو من كلا طرفي السلسلة النصية. سيعيد التابع strip() سلسلةً نصيةً جديدةً لا تحتوي على أي فراغات في بدايتها أو نهايتها، بينما يحذف التابعان lstrip() و rstrip() الفراغات البيضان من الطرف الأيسر أو الأيمن على التوالي: >>> spam = ' Hello, World ' >>> spam.strip() 'Hello, World' >>> spam.lstrip() 'Hello, World ' >>> spam.rstrip() ' Hello, World' يمكننا تمرير وسيط اختياري يحتوي على المحارف التي نريد حذفها بدلًا من الفراغات: >>> spam = 'SpamSpamOliveSpamEggsSpamSpam' >>> spam.strip('ampS') 'OliveSpamEggs' تمرير الوسيط 'ampS' إلى التابع strip() سيؤدي إلى حذف جميع تكرارات المحارف a و m و p و S من بداية ونهاية السلسلة النصية spam. لاحظ أن ترتيب الأحرف في السلسلة النصية الممررة إلى التابع strip() لا يهم، فكتابة strip('ampS') تكافئ كتابة strip('mapS') أو strip('Spam'). القيم العددية للأحرف مع الدالتين ord() و chr() تخزن الحواسيب البيانات على هيئة بايتات، وهي سلاسل من البتات بنظام العد الثنائي، وهذا يعني أننا نحتاج إلى تحويل النصوص إلى أرقام لكي نستطيع تخزينها؛ ولهذا يكون لكل محرف character قيمة رقمية مرتبطة به تسمى Unicode code point. فمثلًا القيمة الرقمية 65 تمثل 'A'، والقيمة 52 تمثل '4'، والقيمة 33 تمثل '!'. يمكننا استخدام الدالة ord() للحصول على القيمة الرقمية لسلسلة نصية تحتوي محرفًا واحدًا، والدالة chr() للحصول على المحرف الذي وفرنا قيمته الرقمية: >>> ord('A') 65 >>> ord('4') 52 >>> ord('!') 33 >>> chr(65) 'A' تفيد هذه الدوال حينما نحتاج إلى ترتيب المحارف أو إجراء عمليات رياضية عليها: >>> ord('B') 66 >>> ord('A') < ord('B') True >>> chr(ord('A')) 'A' >>> chr(ord('A') + 1) 'B' هنالك تفاصيل كثيرة عن يونيكود وأرقام المحارف، لكنها خارجة عن سياق هذه السلسلة. نسخ ولصق السلاسل النصية باستخدام الوحدة pyperclip تمتلك الوحدة pyperclip الدالتين copy() و paste() التي يمكنها إرسال واستقبال النص من حافظة نظام التشغيل الذي تستعمله. إذ يسهل عليك إرسال ناتج برنامجك إلى الحافظة أن تلصقه في رسالةٍ بريدية أو في محرر النصوص أو غيره من البرمجيات. تشغيل سكربتات بايثون خارج محرر Mu شغلنا كل سكربتات بايثون التي كتبناها حتى الآن باستخدام الصدفة التفاعلية ومحرر الشيفرات Mu؛ لكننا لسنا بحاجة إلى فتح محرر Mu في كل مرة نريد فيها تنفيذ سكربتات بايثون التي كتبناها. لحسن الحظ هنالك طرائق تسهل علينا تشغيل سكربتات بايثون، لكن هذه الطرائق تختلف من نظام ويندوز إلى MacOS ولينكس، وهي مشروحة في المقال الأول من هذه السلسلة، لذا أنصحك بالانتقال إلى المقال الأول لتعرف كيف تشغل سكربتات بايثون بسهولة على نظامك، وكيف تمرر خيارات سطر الأوامر command line arguments إليها، لاحظ أنك لا تستطيع تمرير خيارات سطر الأوامر باستخدام محرر Mu. الوحدة pyperclip غير مضمنة في بايثون، لذا عليك تثبيتها باتباع التعليمات المذكورة في المقال الأول من السلسلة أيضًا. يمكنك أن تدخل ما يلي في الصدفية التفاعلية بعد تثبيت الوحدة pyperclip: >>> import pyperclip >>> pyperclip.copy('Hello, world!') >>> pyperclip.paste() 'Hello, world!' إذا غيّر أحد البرامج محتويات الحافظة فستعيدها الدالة paste()، فانسخ مثلًا عبارةً من المتصفح أو من محرر الشيفرات ثم استدعِ الدالة paste(): >>> pyperclip.paste() 'For example, if I copied this sentence to the clipboard and then called paste(), it would look like this:' مشروع: حافظة فيها رسائل تلقائية إذا سبق وأن رددت على عدد كبير من رسائل البريد الإلكتروني بعبارات متشابهة فمن المرجح أنك قد قضيت وقتًا طويلًا في الكتابة على الحاسوب، ومن المرجح أن لديك ملف نصي فيه تلك العبارات ليسهل عليك نسخها ولصقها من الحافظة؛ لكن يمكن لحافظة نظام تشغيلك الاحتفاظ برسالة واحدة فقط في آن واحد وهذا ليس مريحًا في العمل. لنحاول سويةً تسهيل هذه المهمة بكتابة برنامج يخزن عددًا من العبارات. الخطوة 1: تصميم البرنامج وبنى المعطيات نرغب أن نشغل هذا البرنامج من سطر الأوامر مع تمرير وسيط إليه الذي يحتوي على كلمة مفتاحية واحدة مثل «موافق» agree أو «مشغول» busy. وستُنسَخ الرسالة المرتبطة بتلك الكلمة المفتاحية إلى الحافظة لكي يستطيع المستخدم لصقها مباشرةً في الردود، وبهذا يمكن أن نستعمل عبارات طويلة دون الحاجة إلى إعادة كتابتها كل مرة. مشاريع الفصول هذا هو أول «مشروع» في هذه السلسلة ومن الآن فصاعدًا ستجد في كل مقال عددًا من المشاريع التي تستعمل المفاهيم المشروحة في ذاك المقال وتبدأ هذه المشاريع من الصفر حتى الحصول على برنامج يعمل تمامًا، وأنصحك وبشدة أن تطبق أولًا بأول ولا تكتفي بالقراءة فقط. افتح نافذة محرر جديدة واحفظ البرنامج الآتي باسم mclip.py، ستحتاج إلى بدء البرنامج مع الرمز #! (الذي يسمى shebang، راجع المقال الأول من هذه السلسلة)، ثم كتابة تعليق يشرح باختصار ما يفعله البرنامج. لمّا كنّا نريد أن نربط كل جملة نصية بمفتاح، فمن المنطقي أن نخزنها في قاموس، والذي سيكون هو بنية المعطيات الأساسية في برنامجنا: #! python3 # mclip.py - A multi-clipboard program. TEXT = {'agree': """Yes, I agree. That sounds fine to me.""", 'busy': """Sorry, can we do this later this week or next week?""", 'upsell': """Would you consider making this a monthly donation?"""} الخطوة 2: التعامل مع وسائط سطر الأوامر تخزن الوسائط الممررة من سطر الأوامر command line arguments في المتغير sys.argv (راجع المقال الأول من هذه السلسلة لمزيدٍ من المعلومات حول استخدام وسائط سطر الأوامر في نظامك). يجب أن يكون أول عنصر في القائمة sys.argv هو سلسلة نصية تمثل اسم الملف 'mclip.py' أما العنصر الثاني في القائمة فهو قيمة الوسيط الأول الممرر عبر سطر الأوامر. سيمثل الوسيط الأول قيمة مفتاح العبارة التي نريد نسخها، ولأننا نريد إجبار المستخدم على تمرير وسيط في سطر الأوامر، فستعرض رسالةً إلى المستخدم إن نسي توفيره للبرنامج (إذا كانت القائمة sys.argv تحتوي على أقل من قيمتين): #! python3 # mclip.py - A multi-clipboard program. TEXT = {'agree': """Yes, I agree. That sounds fine to me.""", 'busy': """Sorry, can we do this later this week or next week?""", 'upsell': """Would you consider making this a monthly donation?"""} import sys if len(sys.argv) < 2: print('Usage: python mclip.py [keyphrase] - copy phrase text') sys.exit() keyphrase = sys.argv[1] # أول وسيط في سطر الأوامر هو مفتاح العبارة التي نريد نسخها الخطوة 3: نسخ العبارة الصحيحة أصبح لدينا مفتاح العبارة مخزنًا في المتغير keyphrase، لذا ستحتاج إلى التحقق إن كان هذا المفتاح موجودًا في القاموس TEXT، فإن كان موجودًا فسننسخ العبارة المرتبطة بهذا المفتاح إلى الحافظة باستخدام الدالة pyperclip.copy()، ولمّا كنّا نستعمل الوحدة pyperclip فسنحتاج إلى استيرادها أيضًا. لاحظ أننا لسنا بحاجة إلى إنشاء المتغير keyphrase، إذ نستطيع استخدام sys.argv[1] في أي مكان استعملنا فيه keyphrase لكن وجود متغير باسم keyphrase سيسهل قراءة الشيفرة كثيرًا: #! python3 # mclip.py - A multi-clipboard program. TEXT = {'agree': """Yes, I agree. That sounds fine to me.""", 'busy': """Sorry, can we do this later this week or next week?""", 'upsell': """Would you consider making this a monthly donation?"""} import sys, pyperclip if len(sys.argv) < 2: print('Usage: py mclip.py [keyphrase] - copy phrase text') sys.exit() keyphrase = sys.argv[1] # first command line arg is the keyphrase if keyphrase in TEXT: pyperclip.copy(TEXT[keyphrase]) print('Text for ' + keyphrase + ' copied to clipboard.') else: print('There is no text for ' + keyphrase) ستبحث الشيفرة في القاموس TEXT عن المفتاح، وإن وجدت فسنحصل على العبارة المرتبطة بذاك المفتاح، ثم ننسخها إلى الحافظة، ونطبع رسالة فيها العبارة المنسوخة، وإن لم نعثر على المفتاح فستظهر رسالة للمستخدم تخبره بعدم وجود هكذا مفتاح. السكربت السابق كامل، ويمكننا اتباع التعليمات الموجودة في المقال الأول لتشغيل البرامج السطرية بسهولة، وأصبح لدينا الآن طريقة سريعة لنسخ الرسائل الطويلة إلى الحافظة بسهولة. لا تنسَ أن تعدل قيمة القاموس TEXT في كل مرة تحتاج فيها إلى إضافة عبارة جديدة. إذا كنت على نظام ويندوز فيمكنك إنشاء ملف دفعي batch file لتشغيل البرنامج باستخدام نافذة تشغيل البرامج Run (بالضغط على Win+R). أدخل ما يلي في محرر النصوص واحفظه باسم mclip.bat في مجلد C:\Windows: @py.exe C:\path_to_file\mclip.py %* @pause يمكننا بعد إنشاء الملف الدفعي أن نشغل برنامجنا بالضغط على Win+R ثم كتابة mclip key_phrase. مشروع: إضافة قائمة منقطة إلى ويكيبيديا حينما تعدل مقالًا في ويكيبيديا، فيمكنك إنشاء قائمة منقطة بوضع كل عنصر من عناصر القائمة في سطر خاص به ووضع نجمة قبله؛ لكن لنقل أن لدينا قائمة طويلة جدًا من العناصر التي تريد تحويلها إلى قائمة منقطة، هنا يمكنك أن تقضي بعض الوقت بإضافة رمز النجمة في بداية كل سطر يدويًا أو تكتب سكربتًا يؤتمت هذه العملية. يأخذ السكربت bulletPointAdder.py النص الموجود في الحافظة ويضيف نجمة وفراغًا إلى بداية كل سطر فيه، ثم يحفظ النص الناتج إلى الحافظة. فمثلًا إذا نسخت النص الآتي من صفحة ويكيبيديا المسماة «قائمة القوائم» List of Lists of Lists إلى الحافظة: Lists of animals Lists of aquarium life Lists of biologists by author abbreviation Lists of cultivars ثم شغلت البرنامج bulletPointAdder.py فستصبح محتويات الحافظة كما يلي: * Lists of animals * Lists of aquarium life * Lists of biologists by author abbreviation * Lists of cultivars ونستطيع الآن لصق النص الناتج إلى ويكيبيديا كقائمة منقطة. الخطوة 1: النسخ واللصق من وإلى الحافظة نريد من برنامج bulletPointAdder.py أن يفعل ما يلي: يأخذ النص الموجود في الحافظة يجري عليه عمليات يحفظ الناتج في الحافظة الخطوة الثانية صعبة بعض الشيء، لكن الخطوات 1 و 3 سهلة جدًا: كل ما علينا فعله هو استدعاء الدالتين pyperclip.copy() و pyperclip.paste(). لننشِئ برنامجًا ينفذ الخطوات 1 و 3: #! python3 # bulletPointAdder.py - Adds Wikipedia bullet points to the start # of each line of text on the clipboard. import pyperclip text = pyperclip.paste() # TODO: فصل الأسطر وإضافة رمز النجمة pyperclip.copy(text) التعليق الذي يبدأ بالكلمة TODO هو تذكير لنا أن علينا إكمال هذا الجزء من البرنامج، لذا ستكون الخطوة القادمة هي برمجة هذا الجزء. الخطوة 2: فصل الأسطر وإضافة النجمة ناتج استدعاء pyperclip.paste() يعيد النص الموجود في الحافظة كسلسة نصية واحدة، إذ سيبدو مثال «قائمة القوائم» الذي نسخناه على الشكل الآتي: 'Lists of animals\nLists of aquarium life\nLists of biologists by author abbreviation\nLists of cultivars' يوجد المحرف \n في السلسلة النصية السابقة لعرضها على عدة أسطر حين لصقها من الحافظة، لاحظ أن السلسلة النصية السابقة «متعددة» الأسطر لوجود محرف التهريب \n. ستحتاج إلى إضافة نجمة إلى بداية كل سطر من الأسطر السابقة. يمكنك أن تكتب شيفرة تبحث عن المحرف \n وتضيف نجمةً قبله، أو أن تستعمل التابع split() لإعادة قائمة من السلاسل النصية يمثل كل عنصر فيها سطرًا من السلسلة النصية الأصلية، وبعد ذلك تضيف النجمة قبل كل عنصر: #! python3 # bulletPointAdder.py - Adds Wikipedia bullet points to the start # of each line of text on the clipboard. import pyperclip text = pyperclip.paste() # فصل الأسطر وإضافة النجمة lines = text.split('\n') for i in range(len(lines)): # المرور على جميع عناصر القائمة lines[i] = '* ' + lines[i] # إضافة نجمة وفراغ قبل كل عنصر pyperclip.copy(text) قسمنا النص في مكان السطر الجديد ليمثل كل عنصر في القائمة سطرًا واحدًا، وخزّنا الناتج في المتغير lines، ثم مررنا على عناصر القائمة lines، ولكل عنصر أضفنا نجمة وفراغًا في بداية السطر. أصبحت لدينا الآن قائمة باسم lines فيها عناصر القائمة وقبل كل عنصر رمزُ النجمة. الخطوة 3: دمج الأسطر المعدلة تحتوي القائمة lines على الأسطر المعدلة التي تبدأ بالنجمة، لكن الدالة pyperclip.copy() تتعامل مع السلاسل النصية وليس مع القوائم، لذا نحتاج إلى تحويل القائمة إلى سلسلة نصية، وذلك بجمع عناصر مع بعضها بعضًا عبر التابع join(): #! python3 # bulletPointAdder.py - Adds Wikipedia bullet points to the start # of each line of text on the clipboard. import pyperclip text = pyperclip.paste() # فصل الأسطر وإضافة النجمة lines = text.split('\n') for i in range(len(lines)): # المرور على جميع عناصر القائمة lines[i] = '* ' + lines[i] # إضافة نجمة وفراغ قبل كل عنصر text = '\n'.join(lines) pyperclip.copy(text) حين تشغيل البرنامج السابق فسيبدل محتويات الحافظة ويضيف نجمةً قبل كل سطر موجود فيها. أصبح البرنامج جاهزًا للتجربة. حتى لو لم تكن بحاجة إلى أتمتة هذه المهمة البسيطة (فهنالك محرر مرئي لويكيبيديا مثلًا) لكن قد تستفيد من برامج مشابهة لأتمتة عمليات بسيطة لمعالجة النصوص، مثل إزالة الفراغات، أو تحويل حالة الأحرف، أو أيًّا كان غرضك من تعديل محتويات الحافظة. برنامج قصير: اللغة السرية " أين الحلقة السابقة؟ يحتاج المثال إلى إعادة صياغة كليًا" هنالك لغة سرية يلعب الأطفال ويستعملونها اسمها Pig Latin، وهي تحريف للكلمات الإنكليزية بقواعد بسيطة. فلو بدأت الكلمة بحرف متحرك فتضاف الكلمة yay إلى نهايتها، وإذا بدأت الكلمة بحرف ساكن أو تركيب ساكن (مثل ch أو gr) فسينقل ذاك الساكن إلى نهاية البرنامج مع إلحاق ay. لنكتب برنامجًا للتحويل إلى اللغة السرية يطبع شيئًا يشبه المثال الآتي: Enter the English message to translate into Pig Latin: My name is AL SWEIGART and I am 4,000 years old. Ymay amenay isyay ALYAY EIGARTSWAY andyay Iyay amyay 4,000 yearsyay oldyay. يستعمل برنامجنا التوابع التي تعرفنا عليها في هذا المقال احفظ السكربت الآتي في ملف باسم pigLat.py: # English to Pig Latin print('Enter the English message to translate into Pig Latin:') message = input() VOWELS = ('a', 'e', 'i', 'o', 'u', 'y') pigLatin = [] # قائمة الكلمات في اللغة السرية for word in message.split(): # فصل الكلمات التي لا تبدأ بأحرف prefixNonLetters = '' while len(word) > 0 and not word[0].isalpha(): prefixNonLetters += word[0] word = word[1:] if len(word) == 0: pigLatin.append(prefixNonLetters) continue # فصل الكلمات التي لا تنتهي بحرف suffixNonLetters = '' while not word[-1].isalpha(): suffixNonLetters += word[-1] word = word[:-1] # تذكر حالة الأحرف wasUpper = word.isupper() wasTitle = word.istitle() word = word.lower() # تحويل الكلمة إلى الحالة الصغيرة لتحويلها # فصل الأحرف الساكنة في بداية الكلمة prefixConsonants = '' while len(word) > 0 and not word[0] in VOWELS: prefixConsonants += word[0] word = word[1:] # إضافة اللاحقة الخاصة باللغة السرية إلى نهاية الكلمة if prefixConsonants != '': word += prefixConsonants + 'ay' else: word += 'yay' # إعادة الكلمة إلى حالتها الأصلية if wasUpper: word = word.upper() if wasTitle: word = word.title() # إضافة الرموز التي استخرجناها سابقًا pigLatin.append(prefixNonLetters + word + suffixNonLetters) # جمع الكلمات مع بعضها إلى سلسلة نصية print(' '.join(pigLatin)) لنلقِ نظرةً على الشيفرة من بدايتها: # English to Pig Latin print('Enter the English message to translate into Pig Latin:') message = input() VOWELS = ('a', 'e', 'i', 'o', 'u', 'y') طلبنا في البداية من المستخدم أن يدخل نصًا إنكليزيًا لتحويله إلى اللغة السرية، وأنشأنا أيضًا ثابتًا يحتوي على الأحرف الصوتية في الإنكليزية (بالإضافة إلى الحرف y) في صف tuple من السلاسل النصية. أنشأنا بعد ذلك المتغير pigLatin لتخزين الكلمات بعد تحويلها إلى اللغة السرية: pigLatin = [] # قائمة الكلمات في اللغة السرية for word in message.split(): # فصل الكلمات التي لا تبدأ بأحرف prefixNonLetters = '' while len(word) > 0 and not word[0].isalpha(): prefixNonLetters += word[0] word = word[1:] if len(word) == 0: pigLatin.append(prefixNonLetters) continue نريد أن نعالج كل كلمة بمفردها، لذا استعملنا التابع message.split() للحصول على قائمة بالكلمات، فالسلسلة النصية 'My name is AL SWEIGART and I am 4,000 years old.' مع التابع split() سيعيد الناتج. ['My', 'name', 'is', 'AL', 'SWEIGART', 'and', 'I', 'am', '4,000', 'years', 'old.']. سنحتاج إلى الاحتفاظ بأي رموز في بداية ونهاية كل كلمة، فلو كانت لدينا الكلمة 'old.' فستحول إلى 'oldyay.' بدلًا من 'old.yay'. سنحتفظ بهذه الرموز في متغير باسم prefixNonLetters. # فصل الكلمات التي لا تنتهي بحرف suffixNonLetters = '' while not word[-1].isalpha(): suffixNonLetters += word[-1] word = word[:-1] سنبدأ حلقة تكرار تستدعي التابع isalpha() على أول حرف من الكلمة لتتأكد إن كان علينا إزالة حرف من الكلمة وإضافته إلى نهاية السلسلة النصية prefixNonLetters، وإذا كانت الكلمة كلها تتألف من رموز أو أرقام مثل '4,000' فسنضيفها كما هي إلى القائمة pigLatin وننتقل إلى الكلمة التالية لنحولها إلى اللغة السرية. سنحتفظ بالرموز في نهاية السلسلة النصية word، وهذا يشبه الحلقة السابقة. نرغب أن يتذكر البرنامج حالة الأحرف للكلمة لكي نستطيع استعادتها بعد عملية التحويل: # تذكر حالة الأحرف wasUpper = word.isupper() wasTitle = word.istitle() word = word.lower() # تحويل الكلمة إلى الحالة الصغيرة لتحويلها سنستخدم الكلمة المخزنة في المتغير word بحالتها الصغيرة حتى نهاية دورة حلقة for. لتحويل إحدى الكلمات مثل sweigart إلى eigart-sway فسنتحتاج إلى إزالة جميع الأحرف الساكنة من بداية الكلمة word: # فصل الأحرف الساكنة في بداية الكلمة prefixConsonants = '' while len(word) > 0 and not word[0] in VOWELS: prefixConsonants += word[0] word = word[1:] استخدمنا حلقة تكرار تشبه الحلقة التي أزالت الرموز من بداية الكلمة word، لكننا الآن نزيد الأحرف الساكنة ونخزنها في متغير باسم prefixConsonants. إذا لم يبقَ أي حرف ساكن في بداية الكلمة، فهذا يعني أنها أصبحت كلها في المتغير prefixConsonants، ويمكننا الآن جمع قيمة ذاك المتغير مع السلسلة النصية 'ay' في نهاية المتغير word. أما خلاف ذلك فهذا يعني أن word تبدأ بحرف صوتي، وسنحتاج إلى إضافة 'yay': # إضافة اللاحقة الخاصة باللغة السرية إلى نهاية الكلمة if prefixConsonants != '': word += prefixConsonants + 'ay' else: word += 'yay' تذكر أننا جعلنا الكلمة بحالة الأحرف الصغيرة word = word.lower(). أما إذا كانت القيمة word بحالة الأحرف الكبيرة أو نسق العناوين Title case فعلينا تحويلها إلى حالتها الأصلية: # إعادة الكلمة إلى حالتها الأصلية if wasUpper: word = word.upper() if wasTitle: word = word.title() وفي نهاية دورة حلقة for سنضيف الكلمة مع أي رموز سابقة أو لاحقة إلى القائمة pigLatin: # إضافة الرموز التي استخرجناها سابقًا pigLatin.append(prefixNonLetters + word + suffixNonLetters) # جمع الكلمات مع بعضها إلى سلسلة نصية print(' '.join(pigLatin)) بعد نهاية حلقة التكرار فسنجمع عناصر القائمة pigLatin مع بعضها باستخدام التابع join()، وستمرر سلسلة نصية واحدة إلى الدالة print() لطباعة الجملة السرية. الخلاصة النصوص هي أكثر أنواع البيانات شيوعًا، وتأتي بايثون مع مجموعة من توابع معالجة النصوص المفيدة. ستستخدم الفهرسة والتقسيم وتوابع السلاسل النصية في كل برنامج بايثون تكتبه تقريبًا. صحيحٌ أن البرامج التي تكتبها الآن لا تبدو معقدة جدًا، فهي لا تحتوي على واجهات مستخدم رسومية فيها صور ونصوص ملونة، فكل ما نستعمله هو الدالة print() لطباعة النصوص و input() لقبول مدخلات المستخدم؛ لكن يستطيع المستخدم إدخال مجموعة كبيرة من النصوص عبر نسخها إلى الحافظة، مما يفيد كثيرًا في معالجة كميات كبيرة من النصوص. وصحيحٌ أن هذه البرامج لا تملك واجهات رسومية جميلة لكنها تفعل الكثير بجهدٍ قليل. طريقة أخرى لمعالجة كمية كبيرة من النصوص هي كتابة الملفات وقراءتها من نظام الملفات المحلي مباشرةً، وسنتعلم كيفية فعل ذلك في مقال لاحق. لقد شرحنا حتى الآن المفاهيم الأساسية في برمجة بايثون! ستتعلم مفاهيم جديدة في بقية هذه السلسلة، لكن يفترض أنك تعرف ما يكيفك لكتابة برامج مفيدة تستطيع عبرها أتمتة المهام. قد تظن أنك لا تمتلك المعرفة الكافية في بايثون لتنزيل صفحات الويب أو تحديث جداول البيانات أو إرسال الرسائل البريدية، لكن هنا يأتي دور الوحدات الخارجية في بايثون، التي توفر لك ما تحتاج إليه لفعل كل ذلك وأكثر منه، والتي سنتعلمها سويةً في المقالات القادمة. مشاريع تدريبية لكي تتدرب، اكتب برامج لتنفيذ المهام الآتية. طابع جداول اكتب دالةً اسمها printTable() تقبل قائمةً فيها قوائم تحتوي على سلاسل نصية، وتعرضها في جدول منسق تكون فيه محاذاة الأعمدة إلى اليمين. افترض أن لكل القوائم الداخلية العدد نفسه من السلاسل النصية، فمثلًا: tableData = [['apples', 'oranges', 'cherries', 'banana'], ['Alice', 'Bob', 'Carol', 'David'], ['dogs', 'cats', 'moose', 'goose']] يجب أن تطبع الدالة printTable() ما يلي: apples Alice dogs oranges Bob cats cherries Carol moose banana David goose تلميح: يجب أن تبحث عن أطول سلسلة نصية في كل قائمة داخلية، كي يتسع العمود لجميع السلاسل النصية. يمكنك تخزين العرض الأقصى لكل عمود في قائمة من الأرقام. يمكن أن تبدأ الدالة printTable() بالسطر colWidths = [0] * len(tableData) الذي سينشِئ قائمةً تحتوي على الرقم 0 يساوي عدد القوائم الداخلية الموجودة في القائمة tableData، وبالتالي ستخزن عرض أطول سلسلة نصية في tableData[0] في العنصر colWidths[0]، وعرض أطول سلسلة نصية في القائمة tableData[1] في العنصر colWidths[1] وهلم جرًا… ثم يمكنك الحصول على أكبر قيمة في القائمة colWidths لتعرف القيمة التي ستمررها كعرض إلى التابع rjust(). ترجمة -بتصرف- للفصل Manipulating Strings من كتاب Automate the Boring Stuff with Python. اقرأ أيضًا المقال السابق: القواميس وهيكلة البيانات في بايثون python القوائم Lists في لغة بايثون تهيئة بيئة العمل في بايثون Python
-
سنشرح في هذا المقال نوع البيانات المسمى بالقاموس dictionary، الذي يوفر طريقة مرنة للوصول إلى البيانات وتنظيمها، ثم سنتعلم كيفية كتابة لعبة إكس-أو عبر دمج ما تعلمناه في المقالات السابقة من هذه السلسلة مع القواميس. نوع البيانات dictionary القواميس تشبه القوائم في أنها مجموعة قابلة للتعديل mutable من القيم، لكن بدلًا من وجود الفهارس الرقمية للعناصر كما في القوائم، يمكن استخدام مختلف أنواع البيانات في القواميس. نسمي المفاتيح في القواميس بالمفاتيح keys، وكل مفتاح مرتبط بقيمة، ونسمي ذلك زوجًا من المفاتيح والقيم key-value pair. نكتب القواميس في بايثون بإحاطها بقوسين مجعدين أو معقوصين {}: >>> myCat = {'size': 'fat', 'color': 'gray', 'disposition': 'loud'} السطر السابق يسند قاموسًا إلى المتغير myCat، ومفاتيح هذا القاموس هي 'size' و 'color' و 'disposition'، والقيم المرتبطة بتلك المفاتيح هي 'fat' و 'gray' و 'loud' على التوالي وبالترتيب. يمكنك الوصول إلى هذه القيم عبر مفاتيحها: >>> myCat['size'] 'fat' >>> 'My cat has ' + myCat['color'] + ' fur.' 'My cat has gray fur.' ما يزال بالإمكان استخدام الأعداد الصحيحة مفاتيحًا لقيم القواميس، لكن ليس من الضروري أن تبدأ من الصفر 0 ويمكنك استخدام أي رقم: >>> spam = {12345: 'Luggage Combination', 42: 'The Answer'} مقارنة القواميس والقوائم على خلاف القوائم، لا تكون القواميس مرتبةً، فأول عنصر في قائمة اسمها spam سيكون spam[0]، لكن لا يوجد «أول» عنصر في القاموس. سيكون ترتيب العناصر مهمًا في حال أردنا تحديد إن كانت قائمتان متساويتين، بينما لا يفرق ترتيب كتابة أزواج المفاتيح-القيم في القواميس: >>> spam = ['cats', 'dogs', 'moose'] >>> olive = ['dogs', 'moose', 'cats'] >>> spam == olive False >>> eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'} >>> steak = {'species': 'cat', 'age': '8', 'name': 'Zophie'} >>> eggs == steak True ولأن القواميس غير مرتبة، فلا يمكن تقسيمها كما في القوائم. محاولة الوصول إلى مفتاح غير موجود في قاموس ما سيؤدي إلى حدوث الخطأ KeyError، وهي تشبه رسالة الخطأ IndexError في القوائم حين محاولة الوصول إلى قيمة خارج مجال فهارس القائمة. لاحظ رسالة الخطأ الآتية التي تظهر لعدم وجود المفتاح 'color': >>> spam = {'name': 'Zophie', 'age': 7} >>> spam['color'] Traceback (most recent call last): File "<pyshell#1>", line 1, in <module> spam['color'] KeyError: 'color' وصحيحٌ أن القواميس غير مرتبة، لكن إمكانية استخدام أي قيمة تريدها للمفاتيح يسمح لنا بترتيب البيانات بطرائق رائعة! لنقل أننا نريد كتابة برنامج يخزن معلومات حول أعياد ميلاد أصدقائك، يمكنك أن تكتب الشيفرة الآتية birthdays.py التي تستخدم القواميس وتجعل أسماء أصدقائك مفاتيحًا للقيم: ➊ birthdays = {'Alice': 'Apr 1', 'Bob': 'Dec 12', 'Carol': 'Mar 4'} while True: print('Enter a name: (blank to quit)') name = input() if name == '': break ➋ if name in birthdays: ➌ print(birthdays[name] + ' is the birthday of ' + name) else: print('I do not have birthday information for ' + name) print('What is their birthday?') bday = input() ➍ birthdays[name] = bday print('Birthday database updated.') أنشأنا قاموسًا وخزناه في المتغير birthdays ➊، ثم تحققنا إن كان الاسم المدخل موجودًا في القاموس عبر الكلمة المحجوزة in ➋ كما كنا نفعل مع القوائم. إذا كان الاسم موجودًا في القاموس فيمكنك الوصول إلى القيمة المرتبطة به عبر استخدام الأقواس المربعة ➌، وإلّا فيمكنك إضافتها باستخدام صيغة الأقواس المربعة مع عامل الإسناد ➍: Enter a name: (blank to quit) Alice Apr 1 is the birthday of Alice Enter a name: (blank to quit) Eve I do not have birthday information for Eve What is their birthday? Dec 5 Birthday database updated. Enter a name: (blank to quit) Eve Dec 5 is the birthday of Eve Enter a name: (blank to quit) للأسف ستحذف كل المعلومات التي أدخلتها في هذا البرنامج حين انتهاء تنفيذه. ستتعلم كيفية حفظ الملفات على القرص في مقال لاحق من هذه السلسلة. القواميس المرتبة في بايثون 3.7 صحيح أن القواميس غير مرتبة ولا يوجد فيها عنصر «أول»، لكن القواميس في الإصدار 3.7 من بايثون وما يليه ستتذكر ترتيب أزواج القيم الموجودة فيها حين إنشاء متسلسل sequence منها. فمثلًا لاحظ أن ترتيب العناصر في القائمتين المنشأتين من القاموسين eggs و steak يطابق ترتيب إدخالها: >>> eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'} >>> list(eggs) ['name', 'species', 'age'] >>> steak = {'species': 'cat', 'age': '8', 'name': 'Zophie'} >>> list(steak) ['species', 'age', 'name'] ستبقى القواميس غير مرتبة، ولا يمكنك أن تصل إلى العناصر فيها عبر فهرس رقمي مثل eggs[0] أو steak[2]، لا يجدر بك الاعتماد على هذا السلوك لأن بايثون لا تتذكر ترتيب إدخال العناصر في الإصدارات القيمة منها، فلاحظ المثال الآتي الذي لا يطابق ترتيب عناصر القاموس الناتج النهائي الترتيبَ الذي أدخلتها به، ناتج التنفيذ الآتي على إصدار بايثون 3.5: >>> spam = {} >>> spam['first key'] = 'value' >>> spam['second key'] = 'value' >>> spam['third key'] = 'value' >>> list(spam) ['first key', 'third key', 'second key'] التوابع keys() و values() و items() هنالك ثلاثة توابع خاصة بالقواميس التي تعيد قيمًا شبيهة بالقوائم list-like من مفاتيح القواميس أو قيمها أو كلًا من المفاتيح والقيم معًا وهي التوابع keys() و values() و items() بالترتيب. القيم المعادة من هذه التوابع ليست قوائم حقيقيةً، فلا يمكننا تعديلها وليس لها التابع append()، لكن أنواع البيانات المعادة (وهي dictkeys و dictvalues و dict_items بالترتيب) يمكن أن تستخدم في حلقات التكرار for: >>> spam = {'color': 'red', 'age': 42} >>> for v in spam.values(): ... print(v) red 42 ستمر حلقة for هنا على كل قيمة في القاموس spam، يمكن لحلقة for المرور على المفاتيح فقط، وعلى المفاتيح والقيم معًا: >>> for k in spam.keys(): ... print(k) color age >>> for i in spam.items(): ... print(i) ('color', 'red') ('age', 42) حين استخدامنا للتوابع keys() و values() و items() فيمكن للحلقة for المرور على قيم المفاتيح أو قيم العناصر أو قيم أزواج المفتاح-القيمة على التوالي. لاحظ أن القيم المعادة من items() هي صفوف tuples تحتوي على المفتاح ثم قيمته. إذا أردتَ قائمةً حقيقية من ناتج أحد تلك التوابع، فيمكننا تمرير القيمة الشبيهة بالقوائم إلى الدالة list() كما يلي: >>> spam = {'color': 'red', 'age': 42} >>> spam.keys() dict_keys(['color', 'age']) >>> list(spam.keys()) ['color', 'age'] يأخذ السطر list(spam.keys()) القيمة ذات النوع dict_keys المعادة من التابع keys() ويمررها إلى الدالة list()، والتي تعيد بدورها قائمةً فيها ['color', 'age']. يمكنك استخدام الإسناد المتعدد مع حلقة for لإسناد المفتاح والقيمة إلى متغيرات منفصلة: >>> spam = {'color': 'red', 'age': 42} >>> for k, v in spam.items(): ... print('Key: ' + k + ' Value: ' + str(v)) Key: age Value: 42 Key: color Value: red التحقق من وجود مفتاح أو قيمة في قاموس نتذكر من المقال السابق أن العاملين in و not in يمكن أن يستخدما للتحقق من وجود قيمة في قائمة. ويمكننا استخدام نفس العاملين للتحقق من وجود قيمة ما في مفتاح أو قيمة في قاموس: >>> spam = {'name': 'Zophie', 'age': 7} >>> 'name' in spam.keys() True >>> 'Zophie' in spam.values() True >>> 'color' in spam.keys() False >>> 'color' not in spam.keys() True >>> 'color' in spam False لاحظ أن كتابة 'color' in spam هي مطابقة لكتابة 'color' in spam.keys(). فإذا أردت التحقق من وجود (أو عدم وجود) مفتاح ما في قاموس فاستعمل الكلمة المحجوزة in (أو not in) مع القاموس نفسه. التابع get() من الرتيب أن نتحقق من وجود مفتاح ما في القاموس قبل الوصول إلى القيمة المرتبطة به، لكن لحسن الحظ هنالك تابع باسم get() يأخذ وسيطين: الأول هو المفتاح الذي نريد الوصول إلى قيمته، والثاني هو قيمة افتراضية ستعاد إن لم يكن المفتاح موجودًا: >>> picnicItems = {'apples': 5, 'cups': 2} >>> 'I am bringing ' + str(picnicItems.get('cups', 0)) + ' cups.' 'I am bringing 2 cups.' >>> 'I am bringing ' + str(picnicItems.get('eggs', 0)) + ' eggs.' 'I am bringing 0 eggs.' ولعدم وجود المفتاح 'eggs' في القاموس picnicItems فستعاد القيمة 0 من التابع get()، وإن لم نستعمل التابع get() في المثال السابق فستظهر رسالة خطأ كما يلي: >>> picnicItems = {'apples': 5, 'cups': 2} >>> 'I am bringing ' + str(picnicItems['eggs']) + ' eggs.' Traceback (most recent call last): File "<pyshell#34>", line 1, in <module> 'I am bringing ' + str(picnicItems['eggs']) + ' eggs.' KeyError: 'eggs' التابع setdefault() الحاجة إلى ضبط قيمة في القاموس مرتبطة بمفتاح معين إن لم يكن ذاك المفتاح موجودًا مسبقًا هو أمرٌ شائع، وتكون الشيفرة بالشكل الآتي: spam = {'name': 'Pooka', 'age': 5} if 'color' not in spam: spam['color'] = 'black' يوفر التابع setdefault() طريقة أسهل لفعل ذلك بسطر برمجي وحيد، فأول وسيط يمرر إلى التابع هو المفتاح الذي سنتحقق من وجوده، والوسيط الثاني هو القيمة التي ستُضبَط إن لم يكن المفتاح موجودًا. إذا كان المفتاح موجودًا فسيعيد التابع setdefault() قيمة ذاك المفتاح: >>> spam = {'name': 'Pooka', 'age': 5} >>> spam.setdefault('color', 'black') 'black' >>> spam {'color': 'black', 'age': 5, 'name': 'Pooka'} >>> spam.setdefault('color', 'white') 'black' >>> spam {'color': 'black', 'age': 5, 'name': 'Pooka'} حينما استدعينا التابع setdefault() أول مرة، فتغيرت قيمة القاموس spam إلى {'color': 'black', 'age': 5, 'name': 'Pooka'}، وسيعيد التابع setdefault() القيمة 'black' لأنها القيمة المضبوطة للمفتاح 'color' حاليًا. لكن حين استدعاء spam.setdefault('color', 'white') فإن القيمة لن تتغير إلى 'white' لأن القاموس spam يحتوي على مفتاح باسم 'color'. التابع setdefault() هو اختصار جميل للتأكد من وجود مفتاح معين وضبط قيمته. هذا مثال عن برنامج يعدّ عدد مرات وجود كل حرف في سلسلة نصية. احفظ المثال الآتي باسم characterCount.py: message = 'It was a bright cold day in April, and the clocks were striking thirteen.' count = {} for character in message: ➊ count.setdefault(character, 0) ➋ count[character] = count[character] + 1 print(count) سيمر البرنامج على كل محرف في السلسلة النصية المخزنة في المتغير message، ويعد كم مرة يظهر فيها كل محرف. استدعاء الدالة setdefault() ➊ سيضمن وجود المفتاح في القاموس count ويضبط قيمته الافتراضية إلى 0، وبالتالي لا يرمي البرنامج الخطأ KeyError حين تنفيذ التعبير البرمجي count[character] = count[character] + 1 ➋. سيبدو الناتج كما يلي: {' ': 13, ',': 1, '.': 1, 'A': 1, 'I': 1, 'a': 4, 'c': 3, 'b': 1, 'e': 5, 'd': 3, 'g': 2, 'i': 6, 'h': 3, 'k': 2, 'l': 3, 'o': 2, 'n': 4, 'p': 1, 's': 3, 'r': 5, 't': 6, 'w': 2, 'y': 1} سترى من الناتج السابق أن الحرف c الصغير مكرر 3 مرات، بينما الفراغ مكرر 13 مرة، والحرف A الكبير يظهر مرة واحدة. سيعمل البرنامج السابق على جميع السلاسل النصية بغض النظر عن محتويات المتغير message حتى لو كان يحتوي على مليون حرف! تجميل الطباعة إذا استوردت الوحدة pprint في برامجك، فيمكنك الوصول إلى الدالتين pprint() و pformat() التي «تجمل طباعة» pretty print قيم القواميس، ستستفيد من هذه الدوال إن أردت عرض قيم القواميس عرضًا أجمل من طريقة عرض الدالة print()، لنعدل المثال السابق: import pprint message = 'It was a bright cold day in April, and the clocks were striking thirteen.' count = {} for character in message: count.setdefault(character, 0) count[character] = count[character] + 1 pprint.pprint(count) سيظهر لنا الناتج الجميل الآتي: {' ': 13, ',': 1, '.': 1, 'A': 1, 'I': 1, --snip-- 't': 6, 'w': 2, 'y': 1} سنستفيد فعليًا من الدالة pprint.pprint() عندما يحتوي القاموس على قوائم أو قواميس متشعبة داخله nested. إذا أردت الحصول على قيمة النص المجمّل بدلًا من طباعته على الشاشة مباشرةً، فاستدعِ الدالة pprint.pformat(). السطران الآتيان متكافئان تمامًا: pprint.pprint(someDictionaryValue) print(pprint.pformat(someDictionaryValue)) استخدام بنى المعطيات لنمذجة عناصر حقيقية كان بإمكاننا لعب الشطرنج مع شخص آخر عن بعد قليل ظهور الإنترنت، فكان يعد كل لاعب رقعة الشطرنج في منزله، ثم يبادلان الرسائل البريدية يصف كل منهما خطوته، ولكي يستطيعوا فعل ذلك كان لاعبو الشطرنج بحاجة إلى وصف حالة رقعة الشطرنج والحركات التي يجريها وصفًا لا لبس فيه. تمثل الفراغات في رقعة الشطرنج في التأشير الجبري Algebraic chess notation بإحداثيات تتألف من حرف ورقم كما في الشكل 5-1. الشكل 5-1: إحداثيات رقعة الشطرنج في التأشير الجبري تُعرّف قطع الشطرنج بالأحرف: K للملك king، و Q للملكة queen (يسميها البعض بالوزير)، و R للقلعة rook، و B للفيل bishop، و N للحصان knight. أما الجنود فلا رمز لهم. يكون وصف الحركات متألفًا من الحرف الذي يمثل القطعة، وإحداثيات الوجهة. وزوج من تلك الحركات يصف ما يحدث في دورٍ واحد (بفرض أن صاحب اللون الأبيض يبدأ أولًا)؛ فمثلًا التأشير 2. Nf3 Nc6 يعني أن الأبيض حرك الحصان إلى f3 والأسود حرك الحصان إلى c6 في الدور الثاني من اللعبة. هنالك المزيد من القواعد للتأشير الجبري للشطرنج، لكن الفكرة التي أحاول إيصالها هي أننا نستطيع وصف لعبة الشطرنج دون الحاجة إلى أن نكون أمام رقعة، ويمكن أن يكون خصمك في الطرف الثاني من الكوكب، وإذا كانت لديك ذاكرة ومخيلة جيدة فلا تحتاج إلى رقعة شطرنج حقيقية من الأساس: فيمكنك أن تقرأ الحركات من الرسائل البريدية وتحدِّث الرقعة الموجودة في مخيلتك! تمتلك الحواسيب ذواكر رائعة، فيمكن أن يخزن الحاسوب مليارات السلاسل النصية من الشكل '2. Nf3 Nc6'، وبهذا يمكن أن تلعب الحواسيب الشطرنج دون لوحة حقيقية؛ فما تفعله الحواسيب هو نمذجة البيانات لتمثيل رقعة الشطرنج، ويمكنك كتابة شيفرة تفعل ذلك بنفسك. هنا تلعب القوائم والقواميس دورها، فمثلًا القاموس {'1h': 'bking', '6c': 'wqueen', '2g': 'bbishop', '5h': 'bqueen', '3e': 'wking'} يمثل الرقعة في الشكل 5-2: الشكل 5-2: رقعة شطرنج منمذجة وفق قيمة قاموس لكن لمثالنا القادمة سنستعمل لعبة أسهل وأبسط من الشطرنج وهي لعبة إكس-أو. لعبة إكس-أو لعبة إكس-أو (تسمى بالإنكليزية tic-tac-toe) تشبه رمز # كبير فيه 9 خانات يمكن أن تكون قيمها X أو O أو أن تكون فارغة. لتمثيل هذه الرقعة بقاموس، فيجب أن نسند لكل خانة زوجًا من المفتاح-القيمة كما في الشكل 5-3. الشكل 5-3: خانات رقعة إكس-أو مع المفاتيح الموافقة لها يمكنك استخدام القيم النصية لتمثيل ما هو موجود في كل خانة في الرقعة: 'x' أو 'o' أو ' ' (فراغ)، وبالتالي نحتاج إلى تسع سلاسل نصية، يمكنك استخدام قاموس من القيم لهذا الأمر، فالقيمة النصية المرتبطة مع المفتاح 'top-R' تمثل القيمة في الركن العلوي الأيمن، والسلسل النصية المرتبطة مع المفتاح 'low-L' تمثل الركن السفلي الأيسر، والسلسلة النصية المرتبطة مع المفتاح 'mid-m' تمثل المنتصف، وهلم جرًا للبقية. هذا القاموس هو بنية معطيات تمثل رقعة لعبة إكس-أو، ولنخزن هذا القاموس في متغير باسم theBoard، ولنحفظ الشيفرة الآتية في ملف باسم ticTacToe.py: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} بنية المعطيات المخزنة في المتغير theBoard تمثل رقعة إكس-أو الموضحة في الشكل 5-4. الشكل 5-4: لوحة إكس-أو فارغة ولما كانت قيمة كل مفتاح في القاموس theBoard هي فراغ واحد فيمثل ذاك القاموس رقعةً فارغةً تمامًا. وإذا بدأت اللاعب X واختار الخانة في المنتصف تمامًا فسيصبح القاموس الذي يمثل الرقعة على الشكل: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} أصبحت بنية المعطيات theBoard تمثل الرقعة الموضحة في الشكل 5-5. الشكل 5-5: الحركة الأولى وتكون رقعة ربح فيها اللاعب O بوضع الشكل O في الصف العلوي كما يلي: theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O', 'mid-L': 'X', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': 'X'} وهي ممثلة في الشكل 5-6. الشكل 5-6: ربح اللاعب O وبالتأكيد لا يستطيع أن يرى اللاعب إلا ما يطبع على الشاشة أمامه، ولا يعرف محتويات المتغيرات، فلننشئ دالةً تطبع القاموس الذي يحتوي على الرقعة على الشاشة. أضف ما يلي إلى ملف ticTacToe.py: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) printBoard(theBoard) حينما تشغل هذا البرنامج فستطبع رقعة إكس-أو فارغة: | | -+-+- | | -+-+- | | يمكن أن تتوالى الدالة printBoard() أي بنية معطيات تمثل رقعة إكس-أو تمررها إليها، جرب تغيير الشيفرة إلى ما يلي: theBoard = {'top-L': 'O', 'top-M': 'O', 'top-R': 'O', 'mid-L': 'X', 'mid-M': 'X', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': 'X'} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) printBoard(theBoard) ستعرض الرقعة الآتية على الشاشة: O|O|O -+-+- X|X| -+-+- | |X ولأنك أنشأت بنية معطيات تمثل لوحة إكس-أو وكتبت الشيفرة في printBoard() التي تفسر بنية المعطيات وتظهر الرقعة، فأنت كتبت برنامجًا «ينمذج» models رقعة إكس-أو. كان بإمكانك تنظيم البيانات في بنية المعطيات بطريقة مختلفة، فمثلًا يمكنك استخدام المفتاح 'TOP-LEFT' بدلًا من 'top-L'، لكن طالما كانت شيفرتك تعمل مع بنية المعطيات التي لديك، فأنت كتبت عملية النمذجة بشكل صحيح. فمثلًا تتوقع الدالة printBoard() أن بنية المعطيات التي تمثل الرقعة هي قاموس فيه مفاتيح لجميع الخانات التسع، لكن إن كان في قاموسك مفتاحٌ ناقص وليكن 'mid-L' فلن يعمل برنامجك: O|O|O -+-+- Traceback (most recent call last): File "ticTacToe.py", line 10, in <module> printBoard(theBoard) File "ticTacToe.py", line 6, in printBoard print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) KeyError: 'mid-L' لنضف الآن الشيفرة التي تسمح للاعبين بإدخال حركاتهم. لنعدل برنامجنا ليبدو كما يلي: theBoard = {'top-L': ' ', 'top-M': ' ', 'top-R': ' ', 'mid-L': ' ', 'mid-M': ' ', 'mid-R': ' ', 'low-L': ' ', 'low-M': ' ', 'low-R': ' '} def printBoard(board): print(board['top-L'] + '|' + board['top-M'] + '|' + board['top-R']) print('-+-+-') print(board['mid-L'] + '|' + board['mid-M'] + '|' + board['mid-R']) print('-+-+-') print(board['low-L'] + '|' + board['low-M'] + '|' + board['low-R']) turn = 'X' for i in range(9): ➊ printBoard(theBoard) print('Turn for ' + turn + '. Move on which space?') ➋ move = input() ➌ theBoard[move] = turn ➍ if turn == 'X': turn = 'O' else: turn = 'X' printBoard(theBoard) الشيفرة الجديدة تطبع اللوحة في بداية كل دور ➊ ثم تطلب المدخلات من اللاعب الحالي ➋ ثم تحدث الرقعة وفقًا لذلك ➌ ثم تبدل اللاعب الحالي ➍ قبل الانتقال إلى الدور القادم. | | -+-+- | | -+-+- | | Turn for X. Move on which space? mid-M | | -+-+- |X| -+-+- | | --snip-- O|O|X -+-+- X|X|O -+-+- O| |X Turn for X. Move on which space? low-M O|O|X -+-+- X|X|O -+-+- O|X|X صحيحٌ أن البرنامج ليس لعبة إكس-أو كاملة، فلن يتحقق إن ربح أحد اللاعبين مثلًا؛ لكنه كافٍ لمعرفة كيفية استخدام بنى المعطيات في برامج حقيقية. القواميس والقوائم المتشعبة نمذجة لوحة إكس-أو هو أمر سهل: تحتاج اللوحة إلى قاموس فيه 9 مفاتيح تمثل خاناتها. لكن إن أردت نموذج أمور أكثر تعقيدًا فستجد أنك تحتاج إلى القواميس والقوائم التي تحتوي على قواميس وقوائم أخرى داخلها. تناسب القوائم تخزين سلسلة مرتبة من القيم، بينما تفيد القواميس بتخزين قواميس التي ترتبط فيها المفاتيح مع القيم. هذا مثال يستعمل قاموسًا يحتوي على قواميس داخله فيها الأغراض التي أتى بها الضيوف إلى الرحلة. يمكن أن تقرأ الدالة totalBrought() بنية المعطيات وتحسب العدد الكلي للعناصر المجلوبة من كل الضيوف: allGuests = {'Alice': {'apples': 5, 'pretzels': 12}, 'Bob': {'steak sandwiches': 3, 'apples': 2}, 'Carol': {'cups': 3, 'apple pies': 1}} def totalBrought(guests, item): numBrought = 0 ➊ for k, v in guests.items(): ➋ numBrought = numBrought + v.get(item, 0) return numBrought print('Number of things being brought:') print(' - Apples ' + str(totalBrought(allGuests, 'apples'))) print(' - Cups ' + str(totalBrought(allGuests, 'cups'))) print(' - Cakes ' + str(totalBrought(allGuests, 'cakes'))) print(' - Steak Sandwiches ' + str(totalBrought(allGuests, 'steak sandwiches'))) print(' - Apple Pies ' + str(totalBrought(allGuests, 'apple pies'))) داخل الدالة totalBrought() هنالك حلقة for تدور على أزواج مفتاح-قيمة في المتغير guests ➊، وسنسند داخل الحلقة اسم كل ضيف إلى المتغير k، وسنسند القاموس الذي يحتوي على قائمة الأغراض التي سيجلبها معه إلى الرحلة إلى المتغير v. إذا كان أحد المعامل item موجودة في القاموس، فستضاف قيمته (كمية الأغراض المجلوبة) إلى المتغير numBrought ➋، لكن إذا لم يكن المفتاح موجودًا فيسعيد التابع get() القيمة 0 لإضافتها إلى numBrought. سيكون ناتج تنفيذ البرنامج كما يلي: Number of things being brought: - Apples 7 - Cups 3 - Cakes 0 - Steak Sandwiches 3 - Apple Pies 1 قد يبدو لك أن حالة الاستخدام السابقة بسيطة ولا حاجة إلى نمذجتها، لكن فكر أن الدالة totalBrought() يمكن أن تتعامل بسهولة مع قاموس يحتوي على آلاف الضيوف، وكل ضيف يجلب آلاف العناصر، وتنظيم هذه المعلومات في بنية معطيات واضحة ووجود الدالة totalBrought() سيوفر عليك وقتًا كثيرًا. يمكنك أن تنمذج العناصر في بنى المعطيات بالطريقة التي تراها مناسبة، لطالما كانت بقية الشيفرة في برنامج قادرةً على التعامل مع بنية المعطيات المنشأة. حينما تبدأ البرمجة فلا تقلق أن تنمذج البيانات بالطريقة «الصحيحة»، فكلما ازدادت خبرتك أصبحت تخطر ببالك طرائق أكثر فاعلية وكفاءة للنمذجة، لكن أهم ما في الأمر أن تعمل بنية المعطيات مع احتياجات برنامجك. الخلاصة يمكن أن تحتوي القوائم والقواميس على عدّة قيم، بما فيها قوائم وقواميس أخرى. القواميس مفيدة لأنك تستطيع ربط مفتاح معين مع قيمة، على عكس القوائم التي هي سلسلة من القيم المرتبة. يمكن الوصول إلى القيم داخل القاموس عبر استخدام الأقواس المربعة كما في القوائم، لكن بدلًا من استخدام فهرس رقمي فيمكن أن تكون المفاتيح في القواميس من مختلف القيم سواءً كانت أعدادًا صحيحةً أو أعدادًا عشريةً أو سلاسل نصية أو صفوف tuples. يمكنك تمثيل الكائنات الحقيقية بتنظيم قيم البرنامج في بنى المعطيات، ورأينا مثال ذلك عمليًا على لعبة إكس-أو. مشاريع تدريبية لكي تتدرب، اكتب برامج لتنفيذ المهام الآتية. مدقق لقواميس الشطرنج استعملنا في هذا المقال قيمةً مثل {'1h': 'bking', '6c': 'wqueen', '2g': 'bbishop', '5h': 'bqueen', '3e': 'wking'} لتمثيل رقعة الشطرنج. اكتب دالةً باسم isValidChessBoard() التي تقبل وسيطًا هو قاموس وتعيد القيمة True أو False اعتمادًا إن كان القاموس صالحًا لتمثيل رقعة الشطرنج. تحتوي الرقعة السليمة على ملك أسود واحد وملك أبيض واحد. ويمكن لأيٍ من اللاعبين امتلاك 16 قطعة كحد أقصى، و 8 جنود كحد أقصى، ويجب أن تكون جميع القطع في المجال بين 1a و 8h، أي لا يمكن أن تكون القطعة في المكان 9z. يجب أن تبدأ أسماء القطع بحرف w أو b لتمثيل اللونين الأبيض أو الأسود، متبوعًا بإحدى الكلمات pawn أو knight أو bishop أو rook أو queen أو king. قائمة الأدوات في لعبة نحن نعمل على لعبة فيها قائمة أدوات يمكن أن يمتلكها اللاعب، والتي سننمذجها باستخدام بنية معطيات تتألف من قاموس تكون فيه مفاتيحه هي سلاسل نصية تصف القيمة الموجودة في قائمة الأدوات، وقيمتها هي عدد نصي يمثل عدد الأدوات التي يمتلكها اللاعب مثلًا القاموس {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12} تعني أن اللاعب يملك حبلًا واحدًا، و 6 شعلات، و 42 قطعة ذهبية …إلخ. اكتب دالةً باسم displayInventory() التي تأخذ أي قاموس يمثل قائمة أدوات ويعرضه بالشكل: Inventory: 12 arrow 42 gold coin 1 rope 6 torch 1 dagger Total number of items: 62 تلميح: يمكنك استخدام حلقة for للمرور على جميع المفاتيح في القاموس: # inventory.py stuff = {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12} def displayInventory(inventory): print("Inventory:") item_total = 0 for k, v in inventory.items(): # أكمل الشيفرة هنا print("Total number of items: " + str(item_total)) displayInventory(stuff) دالة تحويل قائمة إلى قاموس في لعبة لنفترض أن لاعبنا في اللعبة السابقة قد حصل على غنيمة ممثلة في القائمة الآتية: playerLoot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby'] اكتب دالةً باسم addToInventory(inventory, addedItems) حيث أن المعامل inventory هو قاموس يمثل قائمة أدوات اللاعب (كما في المثال السابق) والمعامل addedItems يشبه المتغير playerLoot. يجب أن تعيد الدالة addToInventory() قاموسًا يمثل قائمة الأدوات المحدثة. لاحظ أن القائمة الموجودة في addedItems قد تحتوي على عدة نسخ من نفس الأداة. يفترض أن تكون شيفرتك شبيهة بما يلي: def addToInventory(inventory, addedItems): # اكتب الدالة هنا inv = {'gold coin': 42, 'rope': 1} dragonLoot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby'] inv = addToInventory(inv, dragonLoot) displayInventory(inv) يجب أن يظهر البرنامج السابق (مع الدالة displayInventory() من المثال السابق) الناتج الآتي: Inventory: 45 gold coin 1 rope 1 ruby 1 dagger Total number of items: 48 ترجمة -بتصرف- للفصل Dictionaries And Structuring DATA من كتاب Automate the Boring Stuff with Python. اقرأ أيضًا المقال السابق: القوائم Lists في لغة بايثون أنواع البيانات والعمليات الأساسية في لغة بايثون تعلم لغة بايثون النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون
- 1 تعليق
-
- 1
-
قبل أن تبدأ بكتابة البرامج عبر بايثون، هنا موضوع مهم يجب أن تعرفه إلى جانب ما تعرفت عليه بالمقالات السابقة من هذه السلسلة، وهو أنواع البيانات خصوصًا القوائم Lists والصفوف tuples. يمكن أن تحتوي القوائم والصفوف على قيم متعددة، مما يجعل كتابة البرامج التي تعالج مقدرًا كبيرًا من البيانات أمرًا سهلًا. ولأن القوائم تستطيع احتواء قوائم أخرى فيها فيمكنك استخدامها لترتيب البيانات ترتيبًا هيكليًا. سنناقش في هذا المقال أساسيات القوائم، وسنتحدث عن التوابع methods، وهي دوال مرتبطة بنوع بيانات معين تجري عمليات عليه. ثم سنتحدث باختصار عن أنواع البيانات المتسلسلة الأخرى مثل الصفوف tuples والسلاسل النصية strings، وكيف تقارن مع بعضها بعضًا. وسنغطي في المقال القادم نوعًا جديدًا من البيانات وهو القاموس dictionary. نوع البيانات list القائمة هي قيمة تحتوي على قيم أخرى متعددة داخلها بترتيب متسلسل. والمصطلح «قيمة القائمة» list value يشير إلى القائمة نفسها، والتي هي القيمة التي يمكن أن تخزن في متغير أو تمرر إلى دالة كغيرها من القيم، ولا تشير إلى القيم الموجودة داخل القائمة. تبدو القائمة بالشكل الآتي: ['cat', 'bat', 'rat', 'elephant'] وكما كنّا نكتب السلاسل النصية محاطةً بعلامتَي اقتباس لتحديد متى تبدأ وتنتهي السلسلة النصية، فتبدأ القائمة بقوس مربع وتنتهي بقوس مربع آخر []. وتسمى القيم داخل القائمة بعناصر القائمة items، ويفصل بين عناصر القائمة بفاصلة. يمكنك إدخال ما يلي في الصدفة التفاعلية للتجربة: >>> [1, 2, 3] [1, 2, 3] >>> ['cat', 'bat', 'rat', 'elephant'] ['cat', 'bat', 'rat', 'elephant'] >>> ['hello', 3.1415, True, None, 42] ['hello', 3.1415, True, None, 42] ➊ >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam ['cat', 'bat', 'rat', 'elephant'] المتغير spam ➊ له قيمة واحدة، وهي قيمة القائمة، ولكن القائمة نفسها تحتوي على عناصر أخرى. لاحظ أن القيمة [] تعني قائمة فارغة لا تحتوي على قيم مثلها كمثل السلسلة النصية الفارغة ''. الوصول إلى عناصر القائمة عبر الفهرس لنقل أن لديك القائمة ['cat', 'bat', 'rat', 'elephant'] مخزنةً في متغير باسم spam، حينها ستكون نتيجة التعبير spam[0] هي القيمة 'cat' ونتيجة التعبير spam[1] هي 'bat' وهلم جرًا للبقية. العدد الصحيح الموجود داخل الأقواس المربعة يسمى فهرسًا index، والقيمة الأولى في القائمة يشار إليها بالفهرس 0، والقيمة الثانية بالفهرس 1، والثالثة بالفهرس 2 …إلخ. يُظهر الشكل التالي قائمةً مسندةً إلى المتغير spam مع توضيح فهارس كل قيمة فيها. لاحظ أن فهرس العنصر الأول هو 0 ويكون فهرس آخر عنصر مساويًا لطول القائمة ناقص واحد. أي أن الفهرس 3 في قائمةٍ لها أربع قيم يشير إلى آخر عنصر. الشكل 1: قائمة مخزنة في متغير مع فهارس كل عنصر فيها على سبيل المثال، أدخل التعابير البرمجية الآتية في الصدفة التفاعلية وابدأ بضبط قيمة المتغير spam: >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam[0] 'cat' >>> spam[1] 'bat' >>> spam[2] 'rat' >>> spam[3] 'elephant' >>> ['cat', 'bat', 'rat', 'elephant'][3] 'elephant' ➊ >>> 'Hello, ' + spam[0] ➋ 'Hello, cat' >>> 'The ' + spam[1] + ' ate the ' + spam[0] + '.' 'The bat ate the cat.' لاحظ أن نتيجة التعبير 'Hello, ' + spam[0]➊ هي 'Hello, ' + 'cat' ذلك لأن نتيجة spam[0] هي 'cat', وبالنهاية ستكون نتيجة التعبير هي السلسلة النصية التالية: 'Hello, cat' ➋. ستحصل على رسالة الخطأ IndexError إذا استخدمت فهرسًا يتجاوز عدد عناصر القائمة. >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam[10000] Traceback (most recent call last): File "<pyshell#9>", line 1, in <module> spam[10000] IndexError: list index out of range يجب أن تكون الفهارس أعدادًا صحيحةً فقط، ولا يقبل بالأعداد العشرية؛ فالمثال الآتي يتسبب بخطأ TypeError: >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam[1] 'bat' >>> spam[1.0] Traceback (most recent call last): File "<pyshell#13>", line 1, in <module> spam[1.0] TypeError: list indices must be integers or slices, not float >>> spam[int(1.0)] 'bat' يمكن أن تحتوي القائمة على قوائم أخرى عناصر لها، ويمكن الوصول إلى القيم بإدخال أكثر من فهرس: >>> spam = [['cat', 'bat'], [10, 20, 30, 40, 50]] >>> spam[0] ['cat', 'bat'] >>> spam[0][1] 'bat' >>> spam[1][4] 50 يدل الفهرس الأول على أي عنصر من القائمة الأولى يجب استخدامه، والفهرس الثاني يدل على العنصر الموجود في القائمة الثانية، فمثلًا spam[0][1] يطبع 'bat'، وهي القيمة الثانية من القائمة الموجودة في الفهرس الأول. وإذا استخدمت فهرسًا واحدًا فسيطبع البرنامج القائمة الموجودة في ذلك الفهرس كاملةً. الفهارس السالبة صحيحٌ أن أرقام الفهارس تبدًا من 0، لكنك تستطيع استخدام الأعداد الصحيحة السالبة قيمًا للفهارس. فالقيمة -1 تشير إلى آخر عنصر في القائمة، والقيمة -2 تشير إلى العنصر ما قبل الأخير …إلخ. جرب ما يلي في الصدفة التفاعلية: >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam[-1] 'elephant' >>> spam[-3] 'bat' >>> 'The ' + spam[-1] + ' is afraid of the ' + spam[-3] + '.' 'The elephant is afraid of the bat.' الحصول على قائمة من قائمة أخرى عبر التقطيع slice ستحصل على قيمة واحدة حين استخدام الفهارس، لكن التقطيع slice يعيد أكثر من قيمة من تلك القائمة على شكل قائمة جديدة؛ ونكتبه ضمن القوسين المربعين كما في الفهارس لكن سنضع عددين صحيحين يفصل بينهما بنقطتين رأسيتين :، لاحظ الاختلاف بينهما: spam[2] ينتج عنصرًا واحدًا موجودًا في الفهرس المحدد. spam[1:4] ينتج قائمة فيها أكثر من عنصر. يكون أول عدد صحيح حين التقطيع هو مكان بدء القطع، والعدد الثاني هو مكان نهاية القطع، وستصل القائمة المقتطعة إلى فهرس العنصر الثاني دون تضمينه في الناتج، وستكون نتيجة القطع هي قائمة جديدة: >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam[0:4] ['cat', 'bat', 'rat', 'elephant'] >>> spam[1:3] ['bat', 'rat'] >>> spam[0:-1] ['cat', 'bat', 'rat'] يمكنك اختصارًا أن تزيل أحد الفهرسين من عملية التقطيع مع الإبقاء على النقطتين الرأسيتين :، فإزالة الفهرس الأول تماثل استخدام الفهرس 0 وتشير إلى بداية القائمة، وإزالة الفهرس الثاني تماثل تمرير طول القائمة كاملًا وبالتالي ستنتهي عملية القطع عند نهاية القائمة: >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam[:2] ['cat', 'bat'] >>> spam[1:] ['bat', 'rat', 'elephant'] >>> spam[:] ['cat', 'bat', 'rat', 'elephant'] الحصول على طول القائمة عبر الدالة len() ستعيد الدالة len() عدد عناصر قائمة تُمرَّر إليها، وهي تشبه إحصاء عدد المحارف في سلسلة نصية: >>> spam = ['cat', 'dog', 'moose'] >>> len(spam) 3 تغيير القيم في قائمة عبر الفهارس تعودنا أن قيمة المتغير تكون على يسار عامل الإسناد، كما في spam = 42، ويمكننا فعل المثل مع القوائم بكتابة فهرس العنصر الذي نريد تغيير قيمته، مثلًا spam[1] = 'aardvark' يعني «أسند القيمة الموجودة في الفهرس 1 في القائمة spam إلى السلسلة النصية 'aardvark': >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam[1] = 'aardvark' >>> spam ['cat', 'aardvark', 'rat', 'elephant'] >>> spam[2] = spam[1] >>> spam ['cat', 'aardvark', 'aardvark', 'elephant'] >>> spam[-1] = 12345 >>> spam ['cat', 'aardvark', 'aardvark', 12345] جمع القوائم Concatenation وتكرارها Replication يمكن أن تجمع القوائم وتكرر كما في السلاسل النصية، فالعامل + يجمع بين قائمتين لإنشاء قائمة جديدة، والعامل * يستخدم لتكرار سلسلة نصية عددًا من المرات: >>> [1, 2, 3] + ['A', 'B', 'C'] [1, 2, 3, 'A', 'B', 'C'] >>> ['X', 'Y', 'Z'] * 3 ['X', 'Y', 'Z', 'X', 'Y', 'Z', 'X', 'Y', 'Z'] >>> spam = [1, 2, 3] >>> spam = spam + ['A', 'B', 'C'] >>> spam [1, 2, 3, 'A', 'B', 'C'] إزالة القيم من القوائم عبر عبارة del تستخدم العبارة del لحذف قيم معينة من قائمة. جميع القيم الموجودة في القائمة بعد العنصر المحذوف سيتغير فهرسها ويصبح أقل بمقدار 1. مثلًا: >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> del spam[2] >>> spam ['cat', 'bat', 'elephant'] >>> del spam[2] >>> spam ['cat', 'bat'] يمكن أن تستعمل العبارة del على المتغيرات العادية أيضًا، وإذا حاولت استخدام أحد المتغيرات بعد حذفه فستحصل على خطأ NameError لأن المتغير لم يعد موجودًا. لكن عمليًا من النادر جدًا أن تحذف قيمة أحد المتغيرات يدويًا وإنما تستعمل استعمالًا رئيسيًا لحذف أحد عناصر القوائم. التعامل مع القوائم حينما تبدأ بالبرمجة، قد يغيرك إنشاء متغيرات لكل قيمة من مجموعة قيم تنتمي إلى مجموعة محددة، فمثلًا لو أردت كتابة أسماء قططي فقد أفكر بكتابة شيفرة كالآتية: catName1 = 'Zophie' catName2 = 'Pooka' catName3 = 'Simon' catName4 = 'Lady Macbeth' catName5 = 'Fat-tail' catName6 = 'Miss Cleo' لكن هذه شيفرة تعيسة! فماذا يحصل لو كان عدد القطط في برنامج متغيرًا؟ فلن يستطيع برنامجك تخزين أسماء قصص لا تملك متغيرات لها. أفضل إلى ذلك أن هذه الأنواع من البرنامج فيها تكرار كثير للشيفرات نفسها، انظر إلى مقدار التكرار في المثال الآتي الذي أنصحك بكتابته باسم AllMyCats1.py وتجربته: print('Enter the name of cat 1:') catName1 = input() print('Enter the name of cat 2:') catName2 = input() print('Enter the name of cat 3:') catName3 = input() print('Enter the name of cat 4:') catName4 = input() print('Enter the name of cat 5:') catName5 = input() print('Enter the name of cat 6:') catName6 = input() print('The cat names are:') print(catName1 + ' ' + catName2 + ' ' + catName3 + ' ' + catName4 + ' ' + catName5 + ' ' + catName6) بدلًا من استخدام أسماء متغيرات متكررة، يمكنك إنشاء متغير واحد يحتوي على قائمة بكل القسم، فهذه نسخة محسنة من المثال السابق، التي نستخدم فيها قائمةً واحدةً يمكن أن تحتوي أي عدد من أسماء القطط التي يمكن أن يدخلها المستخدم. جرب المثال AllMyCats2.py: catNames = [] while True: print('Enter the name of cat ' + str(len(catNames) + 1) + ' (Or enter nothing to stop.):') name = input() if name == '': break catNames = catNames + [name] # جمع القوائم print('The cat names are:') for name in catNames: print(' ' + name) ناتج تجربة المثال السابق: Enter the name of cat 1 (Or enter nothing to stop.): Zophie Enter the name of cat 2 (Or enter nothing to stop.): Pooka Enter the name of cat 3 (Or enter nothing to stop.): Simon Enter the name of cat 4 (Or enter nothing to stop.): Lady Macbeth Enter the name of cat 5 (Or enter nothing to stop.): Fat-tail Enter the name of cat 6 (Or enter nothing to stop.): Miss Cleo Enter the name of cat 7 (Or enter nothing to stop.): The cat names are: Zophie Pooka Simon Lady Macbeth Fat-tail Miss Cleo الفائدة من استخدام القوائم هي أن بياناتك أصبحت مهيكلة هيكلةً أفضل، وسيكون برنامجك مرنًا في معالجة البيانات والتعامل مع مجموعة مشتركة من المتغيرات. استخدام حلقات for مع القوائم تعلمنا في المقال الثاني من هذه السلسلة عن حلقات التكرار for لتنفيذ كتلة من الشيفرات لعدد معين من المرات؛ لكن تقنيًا ما تفعله for هو تكرار الشيفرة داخلها مرةً واحدةً لكل عنصر من عناصر القائمة. فالشيفرة: for i in range(4): print(i) ستخرج الناتج الآتي: 0 1 2 3 هذا لأن القيمة المعادة من تنفيذ range(4) هي قيمة متسلسلة sequence value التي تعاملها بايثون معاملةً شبيهة بالقائمة [0, 1, 2, 3] (سنشرح المتسلسلات Sequences لاحقًا في هذا المقال). سيكون ناتج البرنامج السابق مماثلًا تمامًا لما يلي: for i in [0, 1, 2, 3]: print(i) حلقة التكرار for السابقة تمر على جميع عناصر القائمة [0, 1, 2, 3] وتضبط قيمة المتغير i إلى قيمة كل عنصر منها. من الشائع استخدام التعبير range(len(someList)) مع حلقة التكرار for في بايثون للمرور على جميع عناصر قائمة ما: >>> supplies = ['pens', 'staplers', 'flamethrowers', 'binders'] >>> for i in range(len(supplies)): ... print('Index ' + str(i) + ' in supplies is: ' + supplies[i]) Index 0 in supplies is: pens Index 1 in supplies is: staplers Index 2 in supplies is: flamethrowers Index 3 in supplies is: binders استخدام range(len(supplies)) في المثال السابق مناسب لأن الشيفرة داخل الحلقة تستطيع الوصول إلى الفهرس عبر المتغير i وإلى القيمة المرتبطة بذاك الفهرس عبر supplies[i]، والأفضل من ذلك كله أن range(len(supplies)) ستؤدي إلى المرور على جميع عناصر القائمة بغض النظر عن عددها. العاملان in و not in يمكنك معرفة إن كانت قيمةٌ ما موجودةً -أو غير موجودةٍ- في قائمة ما باستخدام العاملين in و not in. وكما في بقية العوامل، يستعمل العاملان in و not in في التعابير وتربطان قيمتين: قيمة نرغب بالبحث عنها في القائمة والقائمة التي نرغب بالبحث فيها؛ وتكون نتيجة هذه التعابير قيمة منطقية بوليانية: >>> 'howdy' in ['hello', 'hi', 'howdy', 'heyas'] True >>> spam = ['hello', 'hi', 'howdy', 'heyas'] >>> 'cat' in spam False >>> 'howdy' not in spam False >>> 'cat' not in spam True فمثلًا يسمح البرنامج الآتي للمستخدم بكتابة اسم قطته ليتأكد إن كان اسمها ضمن قائمة من أسماء القطط. احفظه باسم myPets.py وجربه: myPets = ['Zophie', 'Pooka', 'Fat-tail'] print('Enter a pet name:') name = input() if name not in myPets: print('I do not have a pet named ' + name) else: print(name + ' is my pet.') سيشبه الناتج ما يلي: Enter a pet name: Footfoot I do not have a pet named Footfoot خدعة للإسناد المتعدد هنالك اختصار يسمى تقنيًا بنشر الصفوف tuple unpacking يسمح لنا بإسناد عدة قيمة لمتغيرات اعتمادًا على قائمة في سطر واحد. فبدلًا من كتابة: >>> cat = ['fat', 'gray', 'loud'] >>> size = cat[0] >>> color = cat[1] >>> disposition = cat[2 نستطيع أن نكتب: >>> cat = ['fat', 'gray', 'loud'] >>> size, color, disposition = cat يجب أن يكون عدد المتغيرات وطول القائمة متساوٍ تمامًا، وإلا فستعطيك بايثون الخطأ ValueError: >>> cat = ['fat', 'gray', 'loud'] >>> size, color, disposition, name = cat Traceback (most recent call last): File "<pyshell#84>", line 1, in <module> size, color, disposition, name = cat ValueError: not enough values to unpack (expected 4, got 3) استخدام الدالة enumerate() مع القوائم بدلًا من استخدام range(len(someList)) مع حلقة تكرار for للوصول إلى قيمة الفهرس لكل عنصر من عناصر القائمة، فيمكننا استدعاء الدالة enumerate() بدلًا منها. ففي كل دورة لحلقة التكرار ستعيد الدالة enumerate() قيمتين: فهرس العنصر الموجود في القائمة والعنصر نفسه على شكل قائمة. فالشيفرة الآتية مماثلة في الوظيفة للمثال الموجود في قسم «استخدام حلقات for مع القوائم»: >>> supplies = ['pens', 'staplers', 'flamethrowers', 'binders'] >>> for index, item in enumerate(supplies): ... print('Index ' + str(index) + ' in supplies is: ' + item) Index 0 in supplies is: pens Index 1 in supplies is: staplers Index 2 in supplies is: flamethrowers Index 3 in supplies is: binders الدالة enumerate() مفيدة إن كنت تريد الوصول إلى العنصر وفهرسه ضمن حلقة التكرار. استخدام الدالتين random.choice() و random.shuffle() مع القوائم الوحدة random فيها عدة دوال تقبل القوائم كمعاملات لها. الدالة random.choice() تعيد عنصرًا مختارًا عشوائيًا من القائمة: >>> import random >>> pets = ['Dog', 'Cat', 'Moose'] >>> random.choice(pets) 'Dog' >>> random.choice(pets) 'Cat' >>> random.choice(pets) 'Cat' يمكنك أن تقول أن random.choice(someList) هي نسخة مختصرة من someList[random.randint(0, len(someList) – 1]. الدالة random.shuffle() تعيد ترتيب العناصر ضمن القائمة عشوائيًا، وتعدل القائمة مباشرة دون إعادة قائمة جديدة: >>> import random >>> people = ['Alice', 'Bob', 'Carol', 'David'] >>> random.shuffle(people) >>> people ['Carol', 'David', 'Alice', 'Bob'] >>> random.shuffle(people) >>> people ['Alice', 'David', 'Bob', 'Carol'] عوامل الإسناد المحسنة من الشائع حين إسناد قيمة ما إلى متغير أن تستعمل قيمة المتغير الابتدائية أساسًا للقيمة الجديدة. فمثلًا بعد إسنادك القيمة 42 للمتغير spam وأردت زيادة قيمة المتغير بمقدار واحد: >>> spam = 42 >>> spam = spam + 1 >>> spam 43 يمكنك بدلًا من ذلك استخدام عامل الإسناد المحسن += لنفس النتيجة: >>> spam = 42 >>> spam += 1 >>> spam 43 هنالك عوامل إسناد محسنة للعوامل + و - و * و/ و % موضحة في الجدول التالي: عامل الإسناد المحسن التعبير البرمجي المكافئ spam += 1 spam = spam + 1 spam -= 1 spam = spam - 1 spam *= 1 spam = spam * 1 spam /= 1 spam = spam / 1 spam %= 1 spam = spam % 1 الجدول 1: عوامل الأسناد المحسنة يمكن استخدام عامل الإسناد المحسن += لدمج قائمتين، والمعامل *= لتكرار قائمة: >>> spam = 'Hello,' >>> spam += ' world!' >>> spam 'Hello world!' >>> olive = ['Zophie'] >>> olive *= 3 >>> olive ['Zophie', 'Zophie', 'Zophie'] التوابع Methods يمكننا القول مجازًا أن التابع method يكافئ الدوال لكنها «تستدعى على» قيمة ما. فمثلًا إذا استدعيت دالة القوائم index() -التي سنشرحها بعد قليل- على قائمة فستكتب: list.index('hello')؛ أي أن التابع يأتي بعد القيمة ويفصل عنها بنقطة. لكل نوع من أنواع البيانات مجموعة توابع خاصة به، ولنوع بيانات القوائم عدد من التوابع المفيدة للبحث والإضافة والحذف ومختلف عمليات التعديل الأخرى. العثور على قيمة في قائمة عبر التابع index() تمتلك القوائم التابع index() الذي تقبل معاملًا وهو القيمة التي سيجري البحث عنها في القائمة، وإذا كان العنصر موجودًا فسيعاد فهرس ذاك العنصر، وإذا لم يكن موجودًا فستطلق بايثون الخطأ ValueError: >>> spam = ['hello', 'hi', 'howdy', 'heyas'] >>> spam.index('hello') 0 >>> spam.index('heyas') 3 >>> spam.index('howdy howdy howdy') Traceback (most recent call last): File "<pyshell#31>", line 1, in <module> spam.index('howdy howdy howdy') ValueError: 'howdy howdy howdy' is not in list وعند وجود قيم مكررة في القائمة فسيعاد الفهرس لأول قيمة يُعثَر عليها، لاحظ أن التابع index() قد أعاد 1 وليس 3 في هذا المثال: >>> spam = ['Zophie', 'Pooka', 'Fat-tail', 'Pooka'] >>> spam.index('Pooka') 1 إضافة قيم إلى القوائم عبر append() و insert() استخدم التابعين append() و insert() لإضافة قيم جديدة إلى قائمة: >>> spam = ['cat', 'dog', 'bat'] >>> spam.append('moose') >>> spam ['cat', 'dog', 'bat', 'moose'] أدى استدعاء التابع append() إلى إضافة قيمة المعامل الممرر إليه إلى نهاية القائمة. التابع insert() يضيف قيمةً جديدةً إلى أي فهرس في القائمة، ويكون الوسيط الأول الممرر إلى التابع insert() هو فهرس القيمة الجديدة، والوسيط الثاني هو القيمة التي نريد إضافتها: >>> spam = ['cat', 'dog', 'bat'] >>> spam.insert(1, 'chicken') >>> spam ['cat', 'chicken', 'dog', 'bat'] لاحظ أن الشيفرة التي كتبناها هي spam.append('moose') و spam.insert(1, 'chicken') وليست spam = spam.append('moose') أو spam = spam.insert(1, 'chicken')، إذ لا يعيد التابع append() أو insert() القيمة الجديدة للقائمة spam (وفي الواقع تكون نتيجة استدعائها هي None، فلا حاجة إلى تخزين قيمة استدعاء تلك التوابع في متغير، بل تعدل تلك التوابع القائمةَ مباشرةً. سنتحدث بالتفصيل عن القوائم القابلة للتغيير وغير القابلة للتغيير لاحقًا في هذا المقال. التوابع التي ترتبط بنوع بيانات محدد -مثل append() و insert()- هي خاصة بذاك النوع، فلا يمكن استخدامها على قيم أخرى مثل السلاسل النصية أو الأعداد الصحيحة. لاحظ ظهور رسالة الخطأ AttributeError في المثال الآتي: >>> eggs = 'hello' >>> eggs.append('world') Traceback (most recent call last): File "<pyshell#19>", line 1, in <module> eggs.append('world') AttributeError: 'str' object has no attribute 'append' >>> olive = 42 >>> olive.insert(1, 'world') Traceback (most recent call last): File "<pyshell#22>", line 1, in <module> olive.insert(1, 'world') AttributeError: 'int' object has no attribute 'insert' إزالة القيم من القوائم عبر التابع remove() نمرر إلى التابع remove() القيمة التي نريد حذفها من القائمة التي يستدعى التابع عليها: >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam.remove('bat') >>> spam ['cat', 'rat', 'elephant'] إذا حاولنا حذف قيمة غير موجودة في القائمة فسيظهر الخطأ ValueError: >>> spam = ['cat', 'bat', 'rat', 'elephant'] >>> spam.remove('chicken') Traceback (most recent call last): File "<pyshell#11>", line 1, in <module> spam.remove('chicken') ValueError: list.remove(x): x not in list إذا تكررت القيمة التي نريد حذفها أكثر من مرة في القائمة فستزال أول نسخة من تلك القيمة: >>> spam = ['cat', 'bat', 'rat', 'cat', 'hat', 'cat'] >>> spam.remove('cat') >>> spam ['bat', 'rat', 'cat', 'hat', 'cat'] لاحظ أن العبارة del مفيدة حينما تعرف فهرس العنصر الذي تريد حذفه من القائمة، بينما يفيد التابع remove() إذا كنت تعرف قيمة العنصر الذي تريد حذفه. ترتيب عناصر قائمة عبر التابع sort() يمكن ترتيب القوائم التي تحتوي على أعداد أو على سلاسل نصية باستخدام التابع sort(): >>> spam = [2, 5, 3.14, 1, -7] >>> spam.sort() >>> spam [-7, 1, 2, 3.14, 5] >>> spam = ['ants', 'cats', 'dogs', 'badgers', 'elephants'] >>> spam.sort() >>> spam ['ants', 'badgers', 'cats', 'dogs', 'elephants'] يمكنك تمرير القيمة True قيمةً للوسيط المسمى reverse لجعل التابعsort() يرتب النتائج ترتيبًا عكسيًا: >>> spam.sort(reverse=True) >>> spam ['elephants', 'dogs', 'cats', 'badgers', 'ants'] هنالك ثلاثة أمور أساسية يجب عليك معرفتها حول التابع sort(): بدايةً يرتب التابع sort() عناصر القائمة مباشرةً دون إعادة قائمة جديدة، أي ليس هنالك فائدة من كتابة شيء يشبه spam = spam.sort(). الأمر الثاني هو أنك لا تستطيع ترتيب القوائم التي تحتوي على قيم عددية ونصية في آن واحد، لأن بايثون لا تعرف كيف تقارن هذه القيم مع بعضها بعضًا. لاحظ الخطأ TypeError في المثال الآتي: >>> spam = [1, 3, 2, 4, 'Alice', 'Bob'] >>> spam.sort() Traceback (most recent call last): File "<pyshell#70>", line 1, in <module> spam.sort() TypeError: '<' not supported between instances of 'str' and 'int' وأخيرًا، يستعمل التابع sort() ترتيب ASCII للسلاسل النصية بدلًا من الترتيب الهجائي، هذا يعني أن الأحرف الكبيرة في الإنكليزية تأتي قبل الأحرف الصغيرة أي أن a سيكون بعد Z مباشرةً: >>> spam = ['Alice', 'ants', 'Bob', 'badgers', 'Carol', 'cats'] >>> spam.sort() >>> spam ['Alice', 'Bob', 'Carol', 'ants', 'badgers', 'cats'] إذا أردت ترتيب القيم ترتيبًا هجائيًا، فمرر القيمة str.lower للوسيط المسمى key في التابع sort(): >>> spam = ['a', 'z', 'A', 'Z'] >>> spam.sort(key=str.lower) >>> spam ['a', 'A', 'z', 'Z'] هذا سيجعل التابع sort() يتعامل مع جميع عناصر القائمة كما لو أنها في حالة الأحرف الصغيرة دون تعديل القيم نفسها. قلب ترتيب عناصر قائمة عبر التابع reverse() إذا احتجت إلى قلب ترتيب عناصر إحدى القوائم سريعًا فاستعمل التابع reverse(): >>> spam = ['cat', 'dog', 'moose'] >>> spam.reverse() >>> spam ['moose', 'dog', 'cat'] وكما في التابع sort()، لا يعيد التابع reverse() قائمةً بل يعدلها مباشرةً، ولهذا نكتب spam.reverse() وليس spam = spam.reverse(). استثناءات من قواعد المسافات البادئة في بايثون في أغلبية الحالات، يكون عدد المسافات البادئة قبل كل سطر برمجي دليلًا يقول لمفسر لغة بايثون في أي كتلة ينتمي ذاك السطر. لكن هنالك بعض الاستثناءات لهذه القائمة، فمثلًا يمكن أن تمتد كتابة القائمة على أكثر من سطر في الشيفرة المصدرية، ولا تهم المسافة البادئة هنا، لأن مفسر بايثون يفهم أن تعريف القائمة لا ينتهي إلا بوجود قوس الإغلاق [. فيمكن أن يكون لدينا شيفرة كما يلي: spam = ['apples', 'oranges', 'bananas', 'cats'] print(spam) لكن عمليًا يستعمل أغلبية المبرمجين المسافات البادئة استعمالًا صحيحًا لتسهل عملية قراءة شيفرتهم. يمكنك أن تقسم تعليمة برمجية واحدة على أكثر من سطر باستخدام محرف إكمال السطر \ في نهاية السطر، يمكنك أن تقول أن محرف إكمال السطر \ يقول «سنكمل هذه التعليمة في السطر القادم»، ولن تكون المسافة البادئة في السطر الذي يلي محرف إكمال السطر \ مهمةً، فالشيفرة الآتية صحيحة: print('Four score and seven ' + \ 'years ago...') ستستفيد من هذه الأمور حينما تحتاج إلى تقسيم الأسطر الطويلة إلى أسطر أقصر لتسهيل قابلية قراءتها. مثال عملي: إعادة كتابة برنامج الكرة السحرية باستخدام القوائم يمكننا كتابة نسخة أفضل من مثال الكرة السحرية الذي كتبناه عبر عبارات elif سابقًا، إذ يمكننا إنشاء قائمة واحدة وسنجعل البرنامج يتعامل معها مباشرةً. احفظ ما يلي في ملف باسم magic8ball2.py: import random messages = ['It is certain', 'It is decidedly so', 'Yes definitely', 'Reply hazy try again', 'Ask again later', 'Concentrate and ask again', 'My reply is no', 'Outlook not so good', 'Very doubtful'] print(messages[random.randint(0, len(messages) - 1)]) حينما تجرب هذا البرنامج فسيبدو ناتجه كما في المثال magic8ball.py الأصلي. لاحظ التعبير الذي استخدمناه كفهرس للقائمة messages: random.randint (0, len(messages) - 1). وهو يولد عدد عشوائيًا نستعمله كفهرس بغض النظر عن عدد العناصر الموجودة في القائمة messages. ذاك التعبير يولد قيمةً عشوائيًا بين 0 وقيمة len(messages) - 1، والقائدة من هذه الطريقة أننا نستطيع إضافة وإزالة العناصر من القائمة messages دون الحاجة إلى تغيير أي شيفرات أخرى، فعندما تحدث شيفرة البرنامج مستقبلًا لإضافة عناصر جديدة إلى القائمة، فلا تضطر إلى تعديلات إضافية، وكلما قللت من الأمور التي تغيرها قلَّت احتمالية استحداث علل برمجية جديدة. أنواع البيانات المتسلسلة Sequence Data Types القوائم هي إحدى أنواع البيانات التي تمثل قائمةً من القيم، لكنها ليست الوحيدة. فمثلًا السلاسل النصية strings والقوائم lists متشابهة جدًا إذا تخيلنا أن السلسلة النصية هي «قائمة» من المحارف. أنواع البيانات المتسلسلة في بايثون تتضمن القوائم lists، والسلاسل النصية strings، وكائنات المجالات range objects المعادة من الدالة range()، والصفوف tuples. أغلبية الأمور التي تستطيع فعلها مع القوائم يمكنك فعلها مع بقية أنواع البيانات المتسلسلة، بما في ذلك: الفهرسة، والتقطيع، واستخدامها مع حلقات for، واستخدام len()، واستخدام العوامل in و not in. جرب المثال الآتي لترى ذلك عمليًا: >>> name = 'Zophie' >>> name[0] 'Z' >>> name[-2] 'i' >>> name[0:4] 'Zoph' >>> 'Zo' in name True >>> 'z' in name False >>> 'p' not in name False >>> for i in name: ... print('* * * ' + i + ' * * *') * * * Z * * * * * * o * * * * * * p * * * * * * h * * * * * * i * * * * * * e * * * أنواع البيانات القابلة وغير القابلة للتعديل تختلف القوائم والسلاسل النصية عن بعضها اختلافًا جوهريًا، فالقوائم هي من أنواع البيانات القابلة للتعديل mutable data type، فيمكن أن تضاف أو تحذف أو تعدل عناصرها؛ بينما السلاسل النصية غير قابلة للتعديل immutable، فإذا حاولت ضبط قيمة أحد محارف السلسلة النصية يدويًا فسيحدث الخطأ TypeError كما في المثال الآتي: >>> name = 'Zophie a cat' >>> name[7] = 'the' Traceback (most recent call last): File "<pyshell#50>", line 1, in <module> name[7] = 'the' TypeError: 'str' object does not support item assignment الطريقة المعتمدة «لتعديل» سلسلة نصية هي استخدام التقطيع والجمع لبناء سلسلة نصية جديدة اعتمادًا على أجزاء من السلسلة النصية القديمة: >>> name = 'Zophie a cat' >>> newName = name[0:7] + 'the' + name[8:12] >>> name 'Zophie a cat' >>> newName 'Zophie the cat' استعملنا [0:7] و [8:12] للإشارة إلى المحارف التي لا نريد تعديلها، لاحظ أن السلسلة النصية الأصلية 'Zophie a cat' لم تعدل، بل أنشأنا سلسلة نصية جديدة. وصحيحٌ أن القوائم قابلة للتعديل، لكن السطر الثاني في المثال الآتي لن يعدل القائمة eggs: >>> eggs = [1, 2, 3] >>> eggs = [4, 5, 6] >>> eggs [4, 5, 6] لم تبدل القيم في القائمة eggs هنا؛ بل أُنشِئت قائمة جديدة [4, 5, 6] وكتبت فوق القائمة القديمة [1, 2, 3] كما في الشكل الآتي: الشكل 2: ما يحدث عند إسناد قائمة جديدة إلى متغير إذا أردت فعليًا تعديل القائمة الأصلية المخزنة في eggs لتحتوي على [4, 5, 6]، فعليك فعل شيء يشبه ما يلي: >>> eggs = [1, 2, 3] >>> del eggs[2] >>> del eggs[1] >>> del eggs[0] >>> eggs.append(4) >>> eggs.append(5) >>> eggs.append(6) >>> eggs [4, 5, 6] يوضح الشكل الموالي التعديلات السبع التي جرت على القائمة eggs لتصل إلى النتيجة النهائية. الشكل 3: تُغيِّر العبارة del والتابع append() قيمة القائمة مباشرة تغيير قيمة نوع بيانات قابل للتعديل (مثل استخدام العبارة del والتابع append() كما في الثالث السابق) سيؤدي إلى تغيير القيمة في مكانها، وذلك لأن قيمة المتغير لا تبدل إلى قائمة جديدة. قد يبدو لك الآن أن الفرق بين أنواع البيانات القابلة وغير القابلة للتعديل تافه ولا يهم، لكن قسم «تمرير المرجعيات» سيشرح لك الفرق في السلوك حين استدعاء الدوال مع وسائط قابلة للتعديل ووسائط غير قابلة للتعديل. لكن قبل ذلك دعنا نتعلم عن نوع بيانات جديد وهو الصفوف tuples، وهو يشبه القوائم لكنه غير قابل للتعديل. الصفوف Tuples يكاد يماثل نوع البيانات tuple القوائم تمامًا، مع استثناء أمرين اثنين: الأول أننا نعرف الصفوف عبر قوسين هلاليين () بدلًا من القوسين المربعين []: >>> eggs = ('hello', 42, 0.5) >>> eggs[0] 'hello' >>> eggs[1:3] (42, 0.5) >>> len(eggs) 3 والثاني -وهو المهم- أن الصفوف هي نوع بيانات غير قابل للتعديل كما في السلاسل النصية، أي لا يمكننا تعديل قيم عناصر الصف أو إضافتها أو حذفها. لاحظ رسالة الخطأ TypeError حين تنفيذ المثال الآتي: >>> eggs = ('hello', 42, 0.5) >>> eggs[1] = 99 Traceback (most recent call last): File "<pyshell#5>", line 1, in <module> eggs[1] = 99 TypeError: 'tuple' object does not support item assignment إذا كانت لديك قيمة واحدة في الصف، فيمكنك أن تطلب من بايثون تعريف ذاك الصف بوضع فاصلة بعد تلك القيمة، وإلا فستظن بايثون أنك كتبت قيمةً عاديةً ضمن قوسين، فالفاصلة هنا هي ما سيخبر بايثون أن ما تريده هو نوع البيانات tuple. لاحظ أن من الطبيعي في بايثون وجود فاصلة بعد آخر عنصر في صف أو قائمة على عكس بعض لغات البرمجة الأخرى. جرب الدالة type() في المثال الآتي لترى الفرق الذي يحدثه استخدام الفاصلة بعد القيمة عمليًا: >>> type(('hello',)) <class 'tuple'> >>> type(('hello')) <class 'str'> يمكنك استخدام الصفوف في برنامجك لتقول لمن يقرأه من المبرمجين أنك لا تنوي لهذه السلسلة من القيم أن تتغير؛ أي لو أردت قيمًا متسلسلة مرتبة لا تتغير فاستخدم الصفوف. ميزة أخرى من مزايا الصفوف بدلًا من القوائم هي أن بايثون تستطيع تطبيق بعض التحسينات الداخلية لمعالجة الصفوف معالجةً أسرع من القوائم، وذلك لعلمها أنها قيم متسلسلة غير قابلة للتعديل. تبديل أنواع التسلسلات باستخدام الدوال list() و tuple() كما تعيد الدالة str(42) القيمة '42' وهو التمثيل النصي للرقم 42؛ تعيد الدالتان list() و tuple() نسخة القائمة والصف من القيم الممررة إليها. جرب المثال الآتي ولاحظ كيف أن نوع القيمة المعادة مختلف عن نوع القيمة الممررة: >>> tuple(['cat', 'dog', 5]) ('cat', 'dog', 5) >>> list(('cat', 'dog', 5)) ['cat', 'dog', 5] >>> list('hello') ['h', 'e', 'l', 'l', 'o'] قد يفيدك تحويل صف إلى قائمة إن احتجت إلى نسخة قابلة للتعديل من قيمة ذاك الصف. المرجعيات References كما رأيت سابقًا، «تُخزِّن» المتغيرات قيم السلاسل النصية والأعداد، لكن هذا تبسيط لما تقوم به بايثون فعلًا. فتقنيًا تخزن المتغيرات مرجعيةً أو إشارةً إلى مكان تخزين قيمة المتغير في ذاكرة الحاسوب: >>> spam = 42 >>> cheese = spam >>> spam = 100 >>> spam 100 >>> cheese 42 فعندما تسند القيمة 42 إلى المتغير spam فأنت تنشِئ القيمة 42 في ذاكرة الحاسوب ثم تخزِّن مرجعيةً reference إليها في المتغير spam، وعندما تنسخ القيمة في spam وتسندها إلى المتغير cheese فأنت فعليًا تنسخ المرجعية إلى القيمة 42 في ذاكرة الحاسوب وليس القيمة نفسها، أي أن كلا المتغيرين spam و cheese يشيران إلى القيمة 42 نفسها في ذاكرة الحاسوب. ثم حينما تغيّر قيمة المتغير spam إلى 100 فأنت تنشِئ قيمةً جديدةً وهي 100 ثم تخزن مرجعيةً إليها في المتغير spam، وهذا لا يؤثر على القيمة الموجودة في المتغير cheese. تذكر أن القيم العددية من أنواع البيانات غير القابلة للتعديل، أي أن تغيير قيمة spam سيؤدي إلى تغيير المرجعية التي تشير إليها في الذاكرة. لكن لا تعمل القوائم بهذه الطريقة، وذلك لأن القوائم من أنواع البيانات القابلة للتعديل. هذا المثال يسهِّل فهم الآلية السابقة والفروق بين القوائم وغيرها من أنواع البيانات: ➊ >>> spam = [0, 1, 2, 3, 4, 5] ➋ >>> cheese = spam # ستنسخ المرجعية وليست القائمة ➌ >>> cheese[1] = 'Hello!' # وهذا ما يغير قيمة عنصر القائمة >>> spam [0, 'Hello!', 2, 3, 4, 5] >>> cheese # يشير المتغير إلى القائمة نفسها [0, 'Hello!', 2, 3, 4, 5] قد يبدو الناتج السابق غريبًا بالنسبة إليك، فأنت تعدل فيه على القائمة cheese لكن التغييرات حدثت على المتغير cheese و spam معًا! عند إنشائك للقائمة ➊ فأنت تخزن مرجعيةً إليها في المتغير spam، وفي السطر التالي ➋ نسخت المرجعية الموجودة في spam إلى cheese وليس القائمة نفسها. وهذا يعني أن القيم المخزنة في المتغيرين spam و cheese تشير إلى القائمة نفسها. لاحظ أن هنالك قائمة واحدة لأن القائمة لم تنسَخ بحد ذاتها بل نُسِخَت المرجعية إليها؛ لذا حينما تعدل أحد عناصر القائمة cheese ➌ فأنت تعدل نفس القائمة التي يُشار إليها عبر المتغير spam. تذكر أن المتغيرات تشبه الصناديق التي تحتوي على قيم، والرسومات التوضيحية التي رأيتها في هذا الفصل لحد الآن ليست دقيقة تمامًا لأن من غير الممكن احتواء قائمة داخل صندوق، بل تحتوي الصناديق على إشارات لتلك القوائم، وتلك الإشارات تملك معرفات ID تستعملها بايثون داخليًا، ولتصحيح تخيلنا للمتغيرات والقوائم فانظر إلى الشكل الآتي: الشكل 4: تخزن مرجعية إلى قائمة في المتغيرات، وليست القائمة نفسها في الشكل الآتي 5 سننسخ المرجعية الموجودة في spam إلى cheese، لاحظ تخزين قيمة المرجعية في cheese وليس القائمة. لاحظ كيف يشير كلا المتغيرين إلى القائمة نفسها: الشكل 5: إسناد قيمة متغير إلى آخر ينسخ المرجعية وليس القائمة وحينما تعدل القائمة التي يشير إليها المتغير cheese فأنت تعدل القائمة التي يشير إليها spam أيضًا، لأنهما يشيران إلى القائمة نفسها، يمكنك ملاحظة ذلك في الشكل الموالي: الشكل 6: تغيير عنصر في قائمة يشار إليها من متغيرين مختلفين صحيحٌ أن بايثون تخزن مرجعيات في المتغيرات، لكن من الشائع أن يقول المطورون أن «المتغيرات تحتوي على قيم» وليس «المتغيرات تحتوي على مرجعيات تشير إلى قيم». المعرفات والدالة id() قد تتساءل لماذا يطبق السلوك السابق الغريب الذي ناقشناه في القسم السابق على القوائم القابلة للتعديل ولا يحدث على القيم غير القابلة للتعديل كالأعداد أو السلاسل النصية. يمكننا استخدام الدالة ()id لفهم ذلك، فكل القيم في بايثون لها معرف خاص بها يمكن الحصول عليه باستخدام الدالة id(): >>> id('Howdy') # ستختلف القيمة المعادة في حاسوبك 44491136 عندما تشغل بايثون العبارة البرمجية id('Howdy') فهي تنشِئ السلسلة النصية 'Howdy' في ذاكرة حاسوبك، ويعاد عنوان الذاكرة الرقمي الذي خُزِّنَت السلسلة النصية فيه عبر الدالة id()، وتختار بايثون العنوان اعتمادًا على أي بايتات تتوافر في ذاكرة حاسوبك في وقت التنفيذ، لذا ستختلف القيمة في كل مرة تشغل فيها الشيفرة. وككل السلاسل النصية، السلسلة 'Howdy' غير قابلة للتعديل، وإذا حاولت «تعديل» قيمة السلسلة النصية الموجودة في متغير، فستُنشَأ سلسلة نصية جديدة في مكان آخر في الذاكرة ثم سيشير المتغير إلى السلسلة النصية الجديدة. جرب المثال الآتي ولاحظ تغيير المعرف الذي يشير إليه المتغير olive: >>> olive = 'Hello' >>> id(olive) 44491136 >>> olive += ' world!' # سلسلة نصية جديدة >>> id(olive) # يشير المتغير إلى سلسلة نصية مختلفة 44609712 لكن يمكن تعديل القوائم لأنها من أنواع البيانات القابلة للتعديل. فالتابع append() لا ينشِئ قائمة جديدة حين تنفيذه، بل يعدل القائمة الموجودة، ونسمي هذا السلوك «بالتعديل في المكان» in-place: >>> eggs = ['cat', 'dog'] # إنشاء قائمة جديدة >>> id(eggs) 35152584 >>> eggs.append('moose') # يضيف التابع القيم مباشرة >>> id(eggs) # يشير المتغير إلى نفس القائمة السابقة 35152584 >>> eggs = ['bat', 'rat', 'cow'] # إنشاء قائمة جديدة لها معرف مختلف >>> id(eggs) # يشير المتغير إلى قائمة مختلفة كليًا 44409800 إذا أشار متغيران أو أكثر إلى القائمة نفسها (كما في المثال في القسم السابق) ثم تغيرت قيمة القائمة، فستحدث التغييرات على كلا المتغيرات لأنهما يشيران إلى القائمة نفسها. التوابع append() و extend() و remove() و sort() و reverse() وغيرها من توابع القوائم ستعمل القوائم في مكانها. جامع القمامة التلقائي في Python (أي Garbage Collector) يحذف أي قيم لا يشار إليها من المتغيرات لكي يُفرِّغ الذاكرة، وهذا رائع لأن الإدارة اليدوية للذاكرة في لغات البرمجة الأخرى هي سبب رئيسي للعلل البرمجية. تمرير المرجعيات من المهم فهم المرجعيات لاستيعاب كيف تمرر الوسائط إلى الدوال. حين استدعاء دالة ما، فإن القيم الممررة كوسائط arguments تنسخ إلى المعاملات parameters، وبالنسبة إلى القوائم (والقواميس التي سنتعرف عليها في المقال القادم) هذا يعني أن المرجعية التي تشير إلى القائمة ستنسخ من الوسيط إلى المعامل، ولكي تعي آثار ذلك جرب المثال الآتي باسم passingReference.py: def eggs(someParameter): someParameter.append('Hello') spam = [1, 2, 3] eggs(spam) print(spam) لاحظ أنه حين استدعاء eggs() فلن تستعمل القيمة المعادة من الدالة لإسناد قيمة جديدة إلى المتغير spam، بل ستعدل المتغير spam في مكانه مباشرةً؛ وسيخرج الناتج الآتي: [1, 2, 3, 'Hello'] وصحيحٌ أن قيمة spam نسخت إلى someParameter لكن ما نسخ فعليًا هو المرجعية إلى نفس القائمة، ولهذا سيؤدي استدعاء التابع append('Hello') إلى تعديل القائمة خارج الدالة. أبقِ هذا السلوك في ذهنك أثناء كتابة الشيفرات، فلو نسيت كيف تتعامل بايثون مع القوائم والقواميس فستقع في أخطاء وعلل كان من السهل تفاديها. الدالة copy() و deepcopy() في الوحدة copy صحيحٌ أن تمرير المرجعيات للإشارة إلى القوائم والقواميس يسهل التعامل معها، لكن إن كانت لدينا دالة تغير القائمة أو القاموس الممرر إليها وكنّا لا نريد إجراء تلك التعديلات على القائمة أو القاموس الأصليين، فحينها يمكننا الاستفادة من الوحدة التي توفرها بايثون باسم copy التي توفر الدالتين copy() و deepcopy(). أول دالة منهما copy.copy() تنشِئ نسخةً طبق الأصل من قيمة قابلة للتعديل كقوائم أو القواميس: >>> import copy >>> spam = ['A', 'B', 'C', 'D'] >>> id(spam) 44684232 >>> cheese = copy.copy(spam) >>> id(cheese) # قائمة مختلفة بمعرف مختلف 44685832 >>> cheese[1] = 42 >>> spam ['A', 'B', 'C', 'D'] >>> cheese ['A', 42, 'C', 'D'] يشير المتغيران spam و cheese إلى قوائم مختلفة، ولهذا السبب سنجد أن القائمة المشار إليها عبر المتغير cheese هي من تغيرت حينما ضبطنا العنصر ذا الفهرس 1 إلى القيمة 42. لاحظ في الشكل التالي أن أرقام المعرفات ID مختلفة لكلا المتغيرين، لأن كل واحد منهما يشير إلى قائمة مختلفة. الشكل 7: نسخ القائمة عبر copy() ينشِئ قائمة جديدة يمكن تعديلها بشكل مستقل عن القائمة الأصلية إذا كانت لديك قائمة ترغب بنسخ محتوياتها أيضًا فاستعمال الدالة copy.deepcopy() بدلًا من copy.copy(). ستنسخ الدالة deepcopy() القوائم الداخلية أيضًا. برنامج قصير: لعبة الحياة لعبة الحياة لكونواي Conway’s Game of Life هي مثال عن خلايا ذاتية السلوك cellular automata: مجموعة من القوائم التي تحكم سلوك حقل مؤلف من خلايا منفصلة. عمليًا هذه طريقة لإنشاء أشكال متحركة جميلة، يمكنك أن ترسل كل خطوة على ورقة رسم بياني، وتمثل المربعات في ورقة الرسم الخلايا. المربع الممتلئ هو خلية «حية»، بينما المربع الفارغ هو خلية «ميتة». تموت أي خلية حية لها أقل من اثنتين من الجيران الأحياء. أي خلية حية لها اثنتين أو ثلاثة جيران من الخلايا الحية تعيش إلى الجيل القادم. تموت أي خلية حية لها أكثر من ثلاثة جيران من الخلايا الحية. أي خلية ميتة تصبح حية عندما يصبح حولها بالضبط ثلاثة من الخلايا الأحياء. تموت أي خلية أخرى أو تبقى ميتة في الجيل القادم. يمكنك النظر إلى تمثيل لتقدم أجيل لعبة الحياة في الشكل الآتي: الشكل 8: أربع خطوات أو أجيال في لعبة الحياة صحيح أن القواعد بسيطة نسبيًا، لكن قد تحدث بعض السلوكيات المثيرة، فيمكن أن تتحرك الأنماط في لعبة الحياة أو أن تتكاثر، أو حتى تحاكي عمل المعالجات المركزية CPUs. لكن في أساس كل هذه الأنماط المعقدة برنامج بسيط. يمكننا استخدام قائمة تحتوي على قوائم داخلها لتمثيل الحقل ثنائي الأبعاد، وتمثل القوائم الداخلية عمودًا من المربعات، وتخزن القيمة '#' للخلايا الحية، والقيمة ' ' للخلايا الميتة. اكتب المثال الآتي في ملف باسم conway.py، ولا مشكلة إن لم تفهم كل ما هو مذكور فيه، كل ما عليك هو إدخاله ومحاولة فهم التعليقات والشروحات: # لعبة الحياة import random, time, copy WIDTH = 60 HEIGHT = 20 # إنشاء قوائم الخلايا nextCells = [] for x in range(WIDTH): column = [] # إنشاء عمود جديد for y in range(HEIGHT): if random.randint(0, 1) == 0: column.append('#') # إضافة خلية حية else: column.append(' ') # إضافة خلية ميتة nextCells.append(column) # nextCells هي قائمة تتألف من قوائم للأعمدة while True: # حلقة البرنامج الرئيسية print('\n\n\n\n\n') # فصل الخطوة أو الجيل القادم بأسطر فارغة currentCells = copy.deepcopy(nextCells) # طباعة الخلايا الحالية على الشاشة for y in range(HEIGHT): for x in range(WIDTH): print(currentCells[x][y], end='') # طباعة # أو فراغ print() # طباعة سطر جديد في نهاية السطر # حساب الخطوة أو الجيل القادم اعتمادًا على القيم الحالية للخلايا for x in range(WIDTH): for y in range(HEIGHT): # الوصول إلى إحداثيات الخلايا المجاورة # `% WIDTH` يضمن أن leftCoord سيكون بين 0 و WIDTH - 1 leftCoord = (x - 1) % WIDTH rightCoord = (x + 1) % WIDTH aboveCoord = (y - 1) % HEIGHT belowCoord = (y + 1) % HEIGHT # إحصاء عدد الخلايا المجاورة numNeighbors = 0 if currentCells[leftCoord][aboveCoord] == '#': numNeighbors += 1 # الخلية في الركن العلوي الأيسر حية if currentCells[x][aboveCoord] == '#': numNeighbors += 1 # الخلية في الأعلى حية if currentCells[rightCoord][aboveCoord] == '#': numNeighbors += 1 # الخلية في الركن العلوي الأيمن حية if currentCells[leftCoord][y] == '#': numNeighbors += 1 # الخلية على اليسار حية if currentCells[rightCoord][y] == '#': numNeighbors += 1 #الخلية على اليمين حية if currentCells[leftCoord][belowCoord] == '#': numNeighbors += 1 # الخلية في الركن السفلي الأيسر حية if currentCells[x][belowCoord] == '#': numNeighbors += 1 # الخلية في الأسفل حية if currentCells[rightCoord][belowCoord] == '#': numNeighbors += 1 # الخلية في الركن السفلي الأيمن حية # ضبط قيمة القيمة اعتمادًا على قواعد لعبة الحياة if currentCells[x][y] == '#' and (numNeighbors == 2 or numNeighbors == 3): # الخلايا التي لها خلايا جارة حية عددها 2 أو 3 nextCells[x][y] = '#' elif currentCells[x][y] == ' ' and numNeighbors == 3: # الخلايا الميتة التي لها 3 خلايا جارة حية nextCells[x][y] = '#' else: # كل ما بقي يكون ميتًا أو سيمين nextCells[x][y] = ' ' time.sleep(1) # التوقف لمدة ثانية لتجنب تأثير الوموض المزعج لنلقي نظرةً على الشيفرة سطرًا بسطر بدءًا من الأعلى: # لعبة الحياة import random, time, copy WIDTH = 60 HEIGHT = 20 استوردنا بدايةً الوحدات التي تحتوي على الدوال التي سنحتاج إليها، تحديدًا الدوال random.randint() و time.sleep() و copy.deepcopy(). # إنشاء قوائم الخلايا nextCells = [] for x in range(WIDTH): column = [] # إنشاء عمود جديد for y in range(HEIGHT): if random.randint(0, 1) == 0: column.append('#') # إضافة خلية حية else: column.append(' ') # إضافة خلية ميتة nextCells.append(column) # nextCells هي قائمة تتألف من قوائم للأعمدة أول خطوة من إنشاء خلايا ذاتية السلوك هي خطوة (أو «جيل») عشوائية تمامًا. سنحتاج إلى إنشاء قائمة تضم قوائم لتخزين السلاسل النصية '#' و ' ' التي تمثل الخلايا الحية والميتة، وسيدل مكانها في قائمة القوائم على مكانها في الشاشة، فكل قائمة داخلية تمثل عمودًا من الخلايا، واستدعاؤنا للدالة random.randint(0, 1) سيعطي الخلية احتمال 50% أن تكون حية و 50% أن تكون ميتة. سنضع قائمة القوائم في متغير اسمه nextCells، لأن أول خطوة ستجريها في حلقة البرنامج الرئيسية هي نسخ nextCells إلى currentCells. ستبدأ إحداثيات محور السينات X من 0 في الأعلى وستزداد نحو اليمين، بينما ستبدأ إحداثيات محور العينات Y من 0 أيضًا في الأعلى وستزداد نحو الأسفل، أي أن nextCells[0][0] ستمثل الخلية في الركن العلوي الأيسر من الشاشة، بينما nextCells[1][0] ستمثل الخلية التي على يمينها، و nextCells[0][1] ستمثل الخلية التي تدنوها. while True: # حلقة البرنامج الرئيسية print('\n\n\n\n\n') # فصل الخطوة أو الجيل القادم بأسطر فارغة currentCells = copy.deepcopy(nextCells) كل دورة من حلقة تكرار البرنامج الرئيسية ستمثل خطوة أو جيلًا من الخلايا ذاتية السلوك التي لدينا. وفي كل دورة سننسخ قيمة nextCells إلى currentCells ثم نطبع currentCells على الشاشة، ثم سنستخدم الخلايا الموجودة في currentCells لحساب الخلايا التي ستكون في nextCells. # طباعة الخلايا الحالية على الشاشة for y in range(HEIGHT): for x in range(WIDTH): print(currentCells[x][y], end='') # طباعة # أو فراغ print() # طباعة سطر جديد في نهاية السطر حلقات for المتشعبة تعني أننا سنطبع سطرًا كاملًا من الخلايا على الشاشة، ثم يكون متبوعًا بسطر فارغ، ثم نكرر العملية لكل سطر في nextCells. # حساب الخطوة أو الجيل القادم اعتمادًا على القيم الحالية للخلايا for x in range(WIDTH): for y in range(HEIGHT): # الوصول إلى إحداثيات الخلايا المجاورة # `% WIDTH` يضمن أن leftCoord سيكون بين 0 و WIDTH - 1 leftCoord = (x - 1) % WIDTH rightCoord = (x + 1) % WIDTH aboveCoord = (y - 1) % HEIGHT belowCoord = (y + 1) % HEIGHT ثم سنسنعمل حلقتي for متشعبتين لحساب كل خلية للخطوة أو الجيل القادم، ولمّا كانت حالة الخلية إن كانت ستحيى أم ستموت في الجيل القادم معتمدةً على جاراتها من الخلايا، فعلينا أولًا حساب فهرس الخلايا التي على يسارها ويمينها وأعلاها وأدناها. عامل باقي القسمة % يجري عملية «التفاف للسطر»، فالجار الأيسر لخلية موجودة في العمود الأيسر سيكون 0 - 1 أو -1، والالتفاف العمود إلى فهرس العمود الأيمن 59 فسنحسب (0 - 1) % WIDTH، ولأن قيمة WIDTH هي 60 فستكون نتيجة التعبير هي 59. يمكن فعل المثل بالنسبة إلى الخلايا الجارة التي تعلو وتدنو وعلى يمين الخلية الحالية. # إحصاء عدد الخلايا المجاورة numNeighbors = 0 if currentCells[leftCoord][aboveCoord] == '#': numNeighbors += 1 # الخلية في الركن العلوي الأيسر حية if currentCells[x][aboveCoord] == '#': numNeighbors += 1 # الخلية في الأعلى حية if currentCells[rightCoord][aboveCoord] == '#': numNeighbors += 1 # الخلية في الركن العلوي الأيمن حية if currentCells[leftCoord][y] == '#': numNeighbors += 1 # الخلية على اليسار حية if currentCells[rightCoord][y] == '#': numNeighbors += 1 #الخلية على اليمين حية if currentCells[leftCoord][belowCoord] == '#': numNeighbors += 1 # الخلية في الركن السفلي الأيسر حية if currentCells[x][belowCoord] == '#': numNeighbors += 1 # الخلية في الأسفل حية if currentCells[rightCoord][belowCoord] == '#': numNeighbors += 1 # الخلية في الركن السفلي الأيمن حية لتقرير إن كانت الخلية الموجودة في nextCells[x][y] ستكون حيةً أو ميتةً فنحتاج إلى إحصاء عدد الخلايا الحية المجاورة للخلية currentCells[x][y]، والسلسلة السابقة من تعابير if الشرطية تتحقق من الجارات الثمانية المجاورة للخلية، وتضيف القيمة 1 للمتغير numNeighbors لكل خلية حية. # ضبط قيمة القيمة اعتمادًا على قواعد لعبة الحياة if currentCells[x][y] == '#' and (numNeighbors == 2 or numNeighbors == 3): # الخلايا التي لها خلايا جارة حية عددها 2 أو 3 nextCells[x][y] = '#' elif currentCells[x][y] == ' ' and numNeighbors == 3: # الخلايا الميتة التي لها 3 خلايا جارة حية nextCells[x][y] = '#' else: # كل ما بقي يكون ميتًا أو سيمين nextCells[x][y] = ' ' time.sleep(1) # التوقف لمدة ثانية لتجنب تأثير الوموض المزعج ثم بمعرفتنا لعدد الخلايا الجارة الحية للخلية currentCells[x][y]، فيمكننا ضبط قيمة nextCells[x][y] إلى '#' أو ' '. وبعد المرور على جميع إحداثيات x و y فسيتوقف التنفيذ لبرهة باستدعاء time.sleep(1) ثم سيكمل تنفيذ البرنامج في بداية حلقة التكرار مجددًا. هنالك عدد من الأنماط للخلايا لها أسماء مثل «الطائرة الشراعية» أو «الطائرة ذات المروحة» أو «سفينة الفضاء الثقيلة». نمط «الطائرة الشراعية» هو النمط الذي رأيته في الشكل 8 وهو «يتحرك» قطريًا كل أربع خطوات. يمكنك إنشاء «طائرة شراعية» بتبديل السطر الآتي في برنامج conway.py: if random.randint(0, 1) == 0: إلى هذا السطر: if (x, y) in ((1, 0), (2, 1), (0, 2), (1, 2), (2, 2)): الخلاصة القوائم هي نوع بيانات مفيد يسمح لك بكتابة شيفرات تتعامل مع قيم متعددة مخزنة في متغير واحد. سنرى برامج في مقالات هذه السلسلة كان من المستحيل برمجتها دون الاعتماد على القوائم. القوائم هي نوع من أنواع البيانات المتسلسلة القابلة للتعديل. أي أن محتوياتها قد تتعدل برمجيًا. بينما تكون الصفوف tuple والسلاسل النصية من أنواع البيانات المتسلسلة لكنها غير قابلة للتعديل. يمكن إعادة كتابة قيمة متغير يحتوي على سلسلة نصية أو صف بقيمة أخرى، لكن هذا لا يكافئ تعديل قيمة السلسلة النصية أو الصف في مكانها، مثلما تفعل التوابع append() أو remove() على القوائم. لا تخزن المتغيرات قيم القوائم مباشرةً فيها، بل هي تشير إلى تلك القوائم، ومن المهم استيعاب هذه النقطة حين نسخ المتغيرات أو تمرير القوائم كوسائط إلى الدوال. ولأن القيمة التي ستنسخ هي مرجعية إلى القائمة وليست القائمة نفسها، فأي تعديل يجرى على القائمة سيؤثر عليها في كامل البرنامج؛ لكننا نستطيع استخدام الدالة copy() أو deepcopy() لنسخ القوائم ثم إجراء تعديلات عليها لا تؤثر على القائمة الأصلية. مشاريع تدريبية لكي تتدرب، اكتب برامج لتنفيذ المهام الآتية. افصل بفاصلة لنقل لدينا القائمة الآتية: spam = ['apples', 'bananas', 'tofu', 'cats'] اكتب دالة تأخذ قائمةً كمعامل وتعيد سلسلةً نصيةً فيها كل عناصر تلك القائمة مفصولٌ بينها بفاصلة ثم فراغ، وأضف الكلمة and قبل آخر عنصر. فلو كانت لدينا القائمة السابقة فستعيد الدالة القيمة 'apples, bananas, tofu, and cats'؛ لكن يجب أن تعمل دالتك مع جميع القيم. تذكر أن تجرب الدالة على قائمة فارغة []. سلسلة رمي القطع النقدية سنجري تجربة بسيطة. إذا رمينا قطعة نقدية مئة مرة، وكتبنا الحرف H لكل رأس والحرف T لكل نقش، فسيكون لدينا قائمة تشبه T T T T H H H H T T. إذا طلبنا من كائن بشري (أهلًا بك أخي البشري ? ) أن يكتب عشوائيًا نتائج مئة رمية لقطعة نقد، فستحصل على شيء يشبه H T H T H H T H T T والذي يبدو عشوائيًا إذا نظر كائن بشري إليه، لكنه لا يمثل سلسلة عشوائية رياضيًا. فلن يكتب الكائن البشري سلسلة من ستة رؤوس أو ستة نقوش متتالية، مع أن ذلك ممكن رياضيًا لو كانت عملية رمي القطع النقدية عشوائيًا فعليًا. فمن المتوقع أن تكون التوقعات العشوائية للكائنات البشرية تعيسةً (آسف أخي البشري، لكنها الحقيقة ? ). اكتب برنامجًا يعرف كم مرة ظهرت سلسلة من ستة رؤوس أو ستة نقوش في قائمة مولدة عشوائيًا. سيقسم برنامجك هذه التجربة إلى قسمين: القسم الأول سيولد قائمة عشوائية من الرؤوس والنقوش، والقسم الثاني سيتحقق من سلسلةً متتاليةً من الروؤس أو النقوش موجودة في تلك القائمة. أجرِ هذه التجربة 10,000 مرة لكي يكون تعرف النسبة التي تحتوي فيها قائمة الرؤوس والنقوش على ستة رؤوس متتالية أو ستة نقوش متتالية. أذكرك أن الدالة random.randint(0, 1) ستعيد القيمة 0 بنسبة 50% والقيمة 1 بنسبة 50%. يمكنك أن تستفيد من القالب الآتي: import random numberOfStreaks = 0 for experimentNumber in range(10000): # الشيفرة التي ستولد قائمة من 100 قيمة عشوائية لعملية رمي القطعة النقدية # الشيفرة التي ستتحقق من ظهور 6 رؤوس أو 6 نقوش متتالية print('Chance of streak: %s%%' % (numberOfStreaks / 100)) تذكر أن الرقم الناتج تقريبي عملي، لكن حجم العينة (عشرة آلاف) مناسب؛ لكن إذا كنت تعرف بعض المبادئ الأساسية في الاحتمالات والإحصاء الرياضي فستعرف الإجابة الدقيقة دون الحاجة إلى كتابة البرنامج السابق، لكن من الشائع أن تكون معرفة المبرمجين بالرياضيات تعيسة (على عكس المتوقع). صورة حرفية لنقل أن لدينا قائمة تضم قوائم أخرى، وكل قيمة في القوائم الداخلية هي حرف واحد كما يلي: grid = [['.', '.', '.', '.', '.', '.'], ['.', 'O', 'O', '.', '.', '.'], ['O', 'O', 'O', 'O', '.', '.'], ['O', 'O', 'O', 'O', 'O', '.'], ['.', 'O', 'O', 'O', 'O', 'O'], ['O', 'O', 'O', 'O', 'O', '.'], ['O', 'O', 'O', 'O', '.', '.'], ['.', 'O', 'O', '.', '.', '.'], ['.', '.', '.', '.', '.', '.']] تخيل أن العنصر grid[x][y] هو المحرف الموجود في الإحداثيات x و y «للصورة» التي سنرسمها عبر الأحرف. مبدأ الإحداثيات (0, 0) هو الركن العلوي الأيسر، وستزداد إحداثيات x بالذهاب نحو اليمين، وإحداثيات y نحو الأسفل. انسخ الشبكة السابقة واكتب شيفرة لطباعة الشكل الآتي منها: ..OO.OO.. .OOOOOOO. .OOOOOOO. ..OOOOO.. ...OOO... ....O.... تلميحة: ستحتاج إلى حلقة تكرار داخل حلقة تكرار، لكي تطبع grid[0][0] ثم grid[1][0] ثم grid[2][0] وهلم جرًا إلى أن تصل إلى grid[8][0]؛ ثم ستنتهي من أول صف وتطبع سطرًا جديدًا، ثم تطبع grid[0][1] ثم grid[1][1] ثم grid[2][1] …إلخ. وآخر عنصر سيطبعه برنامجك هو grid[8][5]. تذكر أن تمرر الوسيط المسمى end إلى الدالة print() إذا لم تكن تريد طباعة سطر جديد بعد كل استدعاء للدالة print(). ترجمة -بتصرف- للفصل LISTS من كتاب Automate the Boring Stuff with Python. اقرأ أيضًا المقال السابق: الدوال في لغة بايثون Python المقال السابق: بنى التحكم في لغة بايثون Python أنواع البيانات والعمليات الأساسية في لغة بايثون تعلم لغة بايثون النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون
-
تعرفت في المقالات السابقة على الدوال print() و input() و len()، وأصبحت تعرف بتوفير بايثون عدّة دوال مضمنة في اللغة built-in functions مثلها، لكنك تستطيع كتابة دوال خاصة بك أيضًا. يمكنك القول أن الدالة هي برنامج صغير داخل برنامجك، ولكي نفهم سويةً كيف تعمل الدوال فنحتاج إلى إنشاء واحدة. أدخل الشيفرة الآتية في المحرر لديك وسم الملف باسم helloFunc.py: ➊ def hello(): ➋ print('Howdy!') print('Howdy!!!') print('Hello there.') ➌ hello() hello() hello() أول سطر هو عبارة def التي تعرف دالةً باسم hello()، والشيفرة التي تلي عبارة def هي جسم الدالة ➋، الذي يحتوي على الشيفرة التي ستنفذ حين استدعاء call الدالة، وليس حين تعريف الدالة. الأسطر الثلاثة التي تحتوي على hello() ➌ هي استدعاءات الدالة، وعملية استدعاء الدالة هي كتابة اسمها متبوعًا بقوسين، وقد تمرر إليها بعض الوسائط arguments بين القوسين. حينما يصل تنفيذ البرنامج إلى استدعاء هذه الدوال، فسينتقل التنفيذ إلى أول سطر في الدالة ويبدأ بتنفيذ الشيفرات الموجودة فيها حتى يصل إلى نهايتها، وحينها يعود التنفيذ إلى السطر البرمجي الذي استدعى الدالة، ثم يكمل البرنامج عمله كالمعتاد. ولأننا استدعينا الدالة hello() ثلاث مرات، فإن الشيفرة الموجودة داخل الدالة hello() ستنفذ ثلاث مرات، وحينما تشغِّل البرنامج السابق سيظهر لديك: Howdy! Howdy!!! Hello there. Howdy! Howdy!!! Hello there. Howdy! Howdy!!! Hello there. أحد الأغراض الأساسية من تعريف الدوال هو تجميع الشيفرات مع بعضها والتي يمكن أن تنفذ عدة مرات، فلو لم تكن لدينا الدالة السابقة فسنحتاج إلى نسخ ولصق الشيفرة عدة مرات كما يلي: print('Howdy!') print('Howdy!!!') print('Hello there.') print('Howdy!') print('Howdy!!!') print('Hello there.') print('Howdy!') print('Howdy!!!') print('Hello there.') هنالك قاعدة عامة تقول أن عليك تفادي تكرار الشيفرات نفسها قدر الإمكان، لأنك إذا قررت مستقبلًا أن تجري تغييرًا عليها -كأنك وجدت علّة وحللتها مثلًا- فعليك أن تجري هذا التغيير في كل مكان نسخت إليه تلك الدالة. وكلما زادت خبرتك البرمجة وجدتَ نفسك تقلل من تكرار الشيفرات، مما يجعل برامجك أقصر وأسهل للقراءة وأسهل في التعديل والتحديث. عبارة def مع معاملات تذكر حينما كنت تستدعي الدالة print() أو len() كنت تمرر إليها قيمًا تسمى بالوسائط arguments، وذلك بكتابتها بين القوسين. يمكنك أيضًا أن تعرف دوالك التي تقبل وسائط، وجرب هذا المثال في ملف باسم helloFunc2.py: ➊ def hello(name): ➋ print('Hello, ' + name) ➌ hello('Alice') hello('Bob') ستبدو المخرجات كما يلي حين تشغيل البرنامج السابق: Hello, Alice Hello, Bob لاحظ أن تعريفنا للدالة hello() في هذا المثال يحتوي على معامل باسم name ➊. المعاملات parameters هي المتغيرات التي تحتوي على قيمة الوسائط arguments. أي حينما نستدعي دالةً مع وسائط arguments فإن قيمة تلك الوسائط ستكون مخزنة في المعاملات parameters. حين استدعاء الدالة hello() لأول مرة سنمرر إليها القيمة 'Alice' ➌ وسينتقل التنفيذ إلى داخل الدالة، وستُضبَط قيمة المعامل name إلى القيمة 'Alice' تلقائيًا، ومن ثم ستطبع هذه القيمة باستخدام الدالة print() ➋. أحد الأمور التي من المهم تذكرها حول المعاملات هي أن القيمة المخزنة فيها ستنسى حين إنتهاء تنفيذ الدالة، فلو كتبت print(name) بعد استدعاء hello('Bob') في البرنامج السابق، فسيظهر لك الخطأ NameError لعدم وجود متغير باسم name، وذلك لأن برنامجنا سيحذف المتغير name بعد إنتهاء استدعاء الدالة hello('Bob') ولذا سنشير في print(name) إلى المتغير name الذي لن يكون موجودًا. هذا يشبه كثيرًا كيف تحذف قيمة المتغيرات في برامجنا السابقة من الذاكرة حين انتهاء تنفيذها. وسنتحدث عن سبب حدوث ذلك تفصيلًا لاحقًا في هذا المقال حينما نتتحدث عن النطاق المحلي local scope. التعريف والاستدعاء والتمرير والوسائط والمعاملات! كثرت علينا المصطلحات الجديدة كالتعريف define والاستدعاء call والتمرير pass والوسائط arguments والمعاملات parameters. لننظر سويةً إلى الشيفرة الآتية لمراجعتها: ➊ def sayHello(name): print('Hello, ' + name) ➋ sayHello('Abdullatif') تعريف الدالة يعني إنشاءها، وكما في عبارة الإسناد spam = 42 التي تنشِئ المتغير spam سنستعمل العبارة def لتعريف الدالة sayHello() ➊. السطر الذي فيه sayHello('Abdullatif') ➋ يستدعي الدالة التي أنشأناها، مما ينقل تنفيذ البرنامج إلى بداية الشيفرة الموجودة داخل الدالة، وسطر الاستدعاء السابق يمرر السلسلة النصية 'Abdullatif' إلى الدالة، والقيمة التي تمرر إلى الدالة تسمى وسيطًا، وسيُسند الوسيط 'Abdullatif' إلى المتغير المحلي المسمى name، وتسمى المتغيرات التي تحمل قيمة الوسائط الممررة إلى الدالة بالمعاملات. من السهل الخلط بين المصطلحات السابقة، لكن حاول أن تستوعبها جيدًا لكي تفهم بقية المقال بسهولة. القيم المعادة وعبارة return عندما تستدعي الدالة len() وتمرر إليها وسيطًا مثل 'Hello' فستكون القيمة الناتجة من الدالة هي الرقم 5، الذي يمثل طول السلسلة النصية التي مررتها إليها. يمكننا القول عمومًا أن الناتج إحدى الدوال يسمى بالقيمة المعادة return value من تلك الدالة. حينما تنشِئ دالةً باستخدام العبارة def فيمكنك أن تحدد ما هي القيمة المعادة من الدالة باستخدام العبارة return. تتألف عبارة return من: الكلمة المحجوزة return قيمة أو تعبير برمجي يجب أن تعيدها الدالة حين استخدام تعبير برمجي مع عبارة return فستكون القيمة المعادة هي ناتج ذاك التعبير. فمثلًا البرنامج الآتي يعرف دالةً تعيد سلسلةً نصيةً مختلفةً اعتمادًا على الرقم المُمرَّر إليها كوسيط. احفظ الشيفرة الآتية في ملف باسم magic8Ball.py: ➊ import random ➋ def getAnswer(answerNumber): ➌ if answerNumber == 1: return 'It is certain' elif answerNumber == 2: return 'It is decidedly so' elif answerNumber == 3: return 'Yes' elif answerNumber == 4: return 'Reply hazy try again' elif answerNumber == 5: return 'Ask again later' elif answerNumber == 6: return 'Concentrate and ask again' elif answerNumber == 7: return 'My reply is no' elif answerNumber == 8: return 'Outlook not so good' elif answerNumber == 9: return 'Very doubtful' ➍ r = random.randint(1, 9) ➎ fortune = getAnswer(r) ➏ print(fortune) حين يبدأ تنفيذ البرنامج السابق فستسورد بايثون الوحدة random ➊، ثم ستعرف الدالة getAnswer() ➋، ولأننا عرفنا الدالة ولم نستدعها فلن تنفذ الشيفرة الموجودة داخلها، بل سينتقل التنفيذ مباشرةً إلى استدعاء الدالة random.randint() التي مررنا إليها وسيطين هما 1 و 9 ➍ وسيعاد منها عدد عشوائي بين 1 و 9 (بما في ذلك الرقمين 1 و 9) وتخزن هذه القيمة في المتغير r. تستدعى الدالة getAnswer() مع تمرير الوسيط r ➎، وينتقل التنفيذ إلى بداية الدالة getAnswer() ➌، وستخزن قيمة الوسيط r في المعامل answerNumber، ثم اعتمادًا على القيمة الموجودة في answerNumber فستعيد الدالة إحدى السلاسل النصية المعرفة مسبقًا، ومن ثم سيعود التنفيذ إلى النقطة التي استدعيت فيها الدالة getAnswer() ➎ وستسند السلسلة النصية المعادة إلى المتغير ذي الاسم fortune، الذي سيمرر بدوره إلى الدالة print() ➏ ويطبع على الشاشة. لاحظ أنك تستطيع تمرير القيم المعادة من الدوال كوسيط مباشرةً إلى دوال أخرى، لذا يمكنك أن تختصر الأسطر الثلاثة الآتية: r = random.randint(1, 9) fortune = getAnswer(r) print(fortune) إلى السطر المكافئ: print(getAnswer(random.randint(1, 9))) تذكر أن التعابير البرمجية تتألف من قيم وعوامل، ويمكن استخدام استدعاءات الدوال في تعبير برمجي لأن الاستدعاء سيؤول إلى قيمة معادة من تلك الدالة. القيمة None هنالك قيمة في بايثون تسمى None وهي تمثل عدم وجود قيمة. والقيمة None هي القيمة الوحيدة لنوع البيانات NoneType، وقد تسمي لغات البرمجة الأخرى هذه القيمة بالاسم null أو nil أو undefined. وكما في القيم المنطقية True و False فيجب أن نكتب None بحرف N كبير. يمكن أن تفيد هذه القيمة-التي-ليس-لها-قيمة حين الحاجة إلى استعمال قيمة لا تمثل شيئًا، فإحدى استخدامات None مثلًا هي القيمة المعادة من الدالة print() التي تطبع نصًا على الشاشة، لكنها لا تعيد أي قيمة على عكس دوال أخرى مثل len() أو input()، ولأن من المفترض أن يكون هنالك قيمة ناتجة لجميع استدعاءات الدوال في بايثون فاستدعاء print() سيعيد القيمة None. لنجرب السطرين الآتيين في الصدفة التفاعلية لنرى ذلك: >>> spam = print('Hello!') Hello! >>> None == spam True تضيف بايثون العبارة return None وراء الكواليس لأي دالة لا يكون لها عبارة return محددة، وهذا ما يشبه كيف تحتوي حلقات التكرار while و for على عبارة continue ضمنية في نهايتها. ستعاد القيمة None أيضًا إن استخدام عبارة return دون قيمة، أي كتبت الكلمة المفتاحية return كما هي. وسطاء الكلمات المفتاحية والدالة print() تعرف أغلبية الوسائط بموضعها حين استدعاء الدالة، فمثلًا الاستدعاء random.randint(1, 10) مختلف عن الاستدعاء random.randint(10, 1)، فحينما نستدعي الدالة random.randint(1, 10) فستعيد لنا رقمًا صحيحًا بين 1 و 10 لأن أول وسيط هو الحد الأدنى من المجال والوسيط الثاني هو الحد الأقصى، بينما يسبب استدعاء random.randint(10, 1) خطأً. لكن بدلًا من تعريف قيم الوسائط عبر موضعها، يمكن أن تعرف وسطاء الكلمات المفتاحية keyword arguments بوضع كلمة مفتاحية قبلها حين استدعاء الدالة، وتستخدم وسطاء الكلمات المفتاحية عادةً للمعاملات الاختيارية optional parameters. فمثلًا تمتلك الدالة print() معاملين اختياريين هما end و sep لضبط ما الذي سيطبع بعد نهاية طبع الوسائط الممررة إليها وما الذي سيطبع بين تلك الوسائط على التوالي. إذا شغلنا برنامجًا يحتوي على الشيفرة الآتية: print('Hello') print('World') فسيكون الناتج كما يلي: Hello World لاحظ أن السلسلتين النصيتين المطبوعتين مفصولتان بسطر وذلك لأن الدالة print() تضيف تلقائيًا محرف السطر الجديد newline character في نهاية السلسلة النصية التي تمرر إليها كوسيط. إلا أننا نستطيع ضبط قيمة الوسيط end لتغيير محرف السطر الجديد إلى سلسلة نصية مختلفة، فمثلًا إذا كتبت الشيفرة الآتية: print('Hello', end='') print('World') فسيكون الناتج: HelloWorld سيطبع الناتج في سطر وحيد لعدم وجود محرف السطر الجديد بعد السلسلة النصية 'Hello' لأننا مررنا سلسلة نصية فارغة، وهذا ما يفيدك إن أردت تعطيل الإضافة التلقائية للسطر الجديد في نهاية كل استدعاء للدالة print(). إذا مررت عدة سلاسل نصية إلى الدالة print() فستفصل الدالة بينها تلقائيًا بفراغ واحد. جرب إدخال السطر الآتي في الصدفة التفاعلية: >>> print('cats', 'dogs', 'mice') cats dogs mice يمكنك تبديل السلسلة النصية التي تفصل بين الوسائط التي ستطبع باستخدام الوسيط sep وتمرير سلسلة نصية مختلفة إليه، جرّب ذلك في الصدفة التفاعلية: >>> print('cats', 'dogs', 'mice', sep=',') cats,dogs,mice يمكنك إضافة وسائط مفتاحية في الدوال التي تكتبها أيضًا، لكن عليك أن تتعلم أولًا عن القوائم list والقواميس dictionary التي سنشرحها في مقالات لاحقة. لكن كل ما عليك معرفته الآن هو أن بعض الدوال لها معاملات اختيارية ويكون لها مفاتيح يمكن الوصول إليها لضبط قيمتها حين استدعاء الدالة. مكدس الاستدعاء Call Stack تخيل أنك تدردش مع أحد أصدقائك، فستتحدث عن صديقك محمد، ثم تتذكر قصة عن زميلك في العمل عبد الحميد، ثم تتذكر شيئًا عن ابن عمك جميل، وحينما تنتهي قصتك عن جميل تعود إلى حديثك عن عبد الحميد، ثم تعود وتتحدث عن محمد، وبعد ذلك تتذكر أخاك بشر، وتقص قصة عن بشر، ثم تعود وتكمل القصة الأصلية لمحمد. تتبع محادثتك بنيةً شبيهةً بالمكدس stack، كما في الشكل الموالي، التي يكون فيها الموضوع الحالي في أعلى المكدس. مكدس القصص في دردشتك. وكما في محادثتك السابقة، عملية استدعاء دالة لا تؤدي إلى نقل التنفيذ إلى بداية الدالة التي جرى استدعاؤها، بل تتذكر بايثون ما هو السطر الذي استدعى تلك الدالة لكي تستطيع توفير القيمة المعادة من تلك الدالة إليه. وإذا استدعت تلك الدالة دوالًا أخرى فستعاد القيم الناتجة عن تلك الدوال إلى أماكن استدعائها الأصلية أولًا. لنفهم ما يحدث بالتفصيل سنجرب المثال الآتي بعد حفظه في ملف باسم abcdCallStack.py: def a(): print('a() starts') ➊ b() ➋ d() print('a() returns') def b(): print('b() starts') ➌ c() print('b() returns') def c(): ➍ print('c() starts') print('c() returns') def d(): print('d() starts') print('d() returns') ➎ a() سينتج البرنامج ما يلي حين تشغيله: a() starts b() starts c() starts c() returns b() returns d() starts d() returns a() returns أعرف أن الفقرة الآتية متداخلة، لكن حاول أن تركز معي فيها: حين استدعاء a() ➎ فستستدعي b() ➊ التي بدورها ستستدعي c() ➌. والدالة c() لا تستدعي غيرها بل تعرض العبارة c() starts ➍ و c() returns قبل أن يعود التنفيذ إلى السطر الذي استدعاها في b() ➌. بعد أن يعود التنفيذ إلى الشيفرة في b() التي استدعت c() فستنتهي الدالة b() وتطبع b() returns ثم يعود التنفيذ إلى السطر الذي استدعى b() في a() ➊. سيستمر التنفيذ في الدالة a() وستستدعى الدالة d()، والتي تشبه الدالة c() في كونها لا تستدعي دالةً غيرها بل تطبع d() starts و d() returns قبل أن تعود إلى السطر الذي استدعاها في a() ثم سيكمل التنفيذ من هناك، وسيطبع آخر سطر من a() العبارة a() returns قبل أن ينتهي تنفيذ الدالة a() ونصل إلى نهاية البرنامج. مكدس الاستدعاء call stack هو الآلية التي تستعملها بايثون لتذكر أين سيعود التنفيذ بعد انتهاء تنفيذ استدعاء كل دالة. لا يخزن مكدس الاستدعاء في متغير في برنامجك وإنما تتولى بايثون أمره خلف الكواليس. فحينما يستدعي برنامجك دالةً فستنشِئ بايثون كائن إطار frame object فوق مكدس الاستدعاء، وتخزن كائنات الإطار frame objects رقم السطر الذي استدعى الدالة لكي تعرف بايثون أين يجب أن تعيد القيمة الناتجة من ذاك الاستدعاء. إذا استدعيت دالة أخرى فستضع بايثون كائن إطار آخر في المكدس فوق السابق. بعد إعادة استدعاء الدالة فستحذف بايثون كائن الإطار من أعلى المكدس وتكمل التنفيذ من السطر المخزن في ذاك الكائن. لاحظ أن كائنات الإطار تضاف وتحذف من أعلى المكدس وليس من أي مكان آخر. يوضح الشكل الموالي حالة مكدس الاستدعاء في البرنامج abcdCallStack.py حين استدعاء والعودة من كل دالة: كائنات الإطار لمكدس الاستدعاء في كل مرحلة من مراحل تنفيذ البرنامج abcdCallStack.py. يمثل أعلى مكدس الاستدعاء الدالة التي يجري تنفيذها حاليًا، وحينما يكون المكدس فارغًا فسيكون التنفيذ في متن البرنامج وخارج جميع الدوال. لا حاجة إلى تعلم المزيد حول مكدس الاستدعاء لكي تكتب برامجك، لأنه يدخل في تفاصيل تقنية عميقة لا داعي لها حاليًا. من الكافي أن تفهم أن الدوال ستعيد القيم إلى السطر الذي استدعيت فيه؛ لكني آثرت شرح مكدس الاستدعاء هنا لأن فهمه سيسهل استيعاب مفاهيم المجالات العامة والمحلية التي سنشرحها في القسم الآتي. المجالات العامة والمحلية تكون المعاملات parameters والمتغيرات الموجودة داخل إحدى الدوال ضمن مجال محلي local scope. أما المتغيرات التي تسند قيمتها خارج جميع الدوال تكون موجودة في المجال العام global scope. وبالتالي يسمى المتغير الموجود في مجال محلي بالمتغير المحلي local variable، بينما يسمى المتغير الموجود في المجال العام بالمتغير العام global variable؛ ويجب أن يكون المتغير عامًا أو محليًا، ولا يمكنه أن يكون كلاهما معًا. يمكنك تخيل المجالات scopes على أنها حاوية للمتغيرات؛ فحينما ينتهي وجود أحد المجالات المحلية فستحذف جميع المتغيرات الموجودة فيه. لاحظ وجود مجال عام وحيد الذي سينُشَأ حينما يبدأ تنفيذ برنامجك وينتهي بإنتهاء تنفيذه. يمكنك التأكد من حذف المتغيرات في المجال العام بعد إنتهاء تنفيذ البرنامج بتجربة الوصول إلى المتغيرات من برنامج آخر. يُنشَأ المجال المحلي في كل مرة تستدعى فيها إحدى الدوال. فتكون جميع المتغيرات المسندة ضمن الدالة موجودة في المجال المحلي. وحين إعادة قيمة return من الدالة فسينتهي وجود المجال المحلي وستحذف قيمة تلك المتغيرات؛ ولن يتذكر برنامج قيمة المتغيرات المخزنة من الاستدعاء السابق للدالة. تخزن المتغيرات المحلية أيضًا في كانئات الإطار frame objects في مكدس الاستدعاء call stack. يهمنا معرفة المجال المستخدم لعدة أسباب: لا يمكن للشيفرات الموجودة في المجال العام أن تستعمل أي متغيرات محلية لكن يمكن للشيفرات في المجال المحلي الوصول إلى المتغيرات العامة الشيفرة في المجال المحلي لإحدى الدوال لا تستطيع استخدام أي متغيرات موجودة في المجال المحلي لدالة أخرى يمكنك استخدام الاسم نفسه لمتغيرات مختلفة على أن تكون موجودة في مجالات مختلفة. أي يمكن أن يسمى متغير محلي بالاسم spam مثلًا ويكون هنالك متغير عام بالاسم spam أيضًا. السبب في امتلاك بايثون لمجالات مختلفة بدل من جعل جميع المتغيرات عامة هو أن بعض المتغيرات تعدل من شيفرة معينة ضمن دالة ما، وتتفاعل هذه الدالة مع بقية البرنامج عبر المعاملات parameters الممررة إليها وعبر القيمة المعادة منها؛ وهذا ما يقلل عدد الأسطر البرمجية التي تتعامل مع متغير ما وتسبب مشكلة برمجية، فإذا كان يحتوي برنامجك على متغيرات عامة فقط وحصل خطأ بسبب ضبط أحد المتغيرات إلى قيمة خطأ فسيصعب كثيرًا تتبع المشكلة في برنامجك، فقد يكون عدد الأسطر البرمجية بالمئات أو الآلاف التي قد تستطيع تعديل قيمة هذا المتغير؛ أما لو كانت العلة البرمجية بسبب قيمة خطأ لمتغير محلي فستعرف تحديدًا ما هي الأسطر البرمجية المسؤولة عن ضبط تلك القيمة وستحل المشكلة بسهولة وسرعة. لا مشكلة في استخدام المتغيرات العامة في البرامج القصيرة سهولتها، لكنها من غير المستحسن الاعتماد على المتغيرات العامة حينما تكبر برامجك. لا يمكن استخدام المتغيرات المحلية في المجال العام أمعن النظر في البرنامج الآتي الذي سيتسبب بخطأ حين محاولة تشغيله: def spam(): ➊ eggs = 31337 spam() print(eggs) إذا جربت هذا البرنامج فسيبدو الناتج كما يلي: Traceback (most recent call last): File "C:/test1.py", line 4, in <module> print(eggs) NameError: name 'eggs' is not defined سيحدث الخطأ بسبب وجود المتغير eggs داخل المجال المحلي المنشأ من الدالة spam() ➊. فبعد انتهاء تنفيذ الدالة spam فسيحذف المجال المحلي ولن يبقى هنالك أي متغير باسم eggs، وحينما يحاول البرنامج تشغيل السطر print(eggs) فستعطيك بايثون خطأً تقول فيه أن المتغير eggs غير معرف، وهذا منطقي إذا فكرت مليًا بالأمر؛ فحينما يكون تنفيذ البرنامج في المجال العام فلا توجد أي مجالات محلية ولن تكون هنالك أي متغيرات محلية، وبالتالي لا يمكننا استخدام سوى المتغيرات العامة في المجال العام. لا يمكن استخدام المتغيرات المحلية في مجالات محلية أخرى ينشأ مجال محلي جديد في كل مرة تستدعى فيها إحدى الدوال، بما في ذلك حين استدعاء دالة ضمن دالة أخرى. انظر إلى المثال الآتي: def spam(): ➊ eggs = 99 ➋ olive() ➌ print(eggs) def olive(): steak = 101 ➍ eggs = 0 ➎ spam() حينما يبدأ تشغيل البرنامج فستستدعى الدالة spam() ➎ وسينشأ مجال محلي، وسيضبط المتغير المحلي eggs ➊ إلى 99، ثم ستستدعى الدالة olive() ➋، ثم سينشأ مجال محلي جديد؛ فمن الممكن أن تكون عدة مجالات محلية موجودة جنبًا إلى جنب. وسنضبط قيمة المتغير المحلي Steak إلى 101، وسننشِئ المتغير المحلي eggs -المختلف كليًا عن المتغير الذي يحمل نفس الاسم في المجال المحلي للدالة spam()- ونضبط قيمته إلى 0 ➍. حين إعادة الدالة olive() فسيحذف المجال المحلي المنشأ بسبب استدعائها، بما في ذلك المتغير eggs الخاص بها. وسيكمل تنفيذ البرنامج في الدالة spam() ليطبع لنا قيمة المتغير eggs ➌؛ ولأن المجال المحلي الخاص بالدالة spam() ما يزال موجودًا فستكون قيمة eggs هي 99 كما ضبطناها سابقًا في ذلك المجال. خلاصة الكلام السابق كله هو أن المتغيرات المحلية الموجودة في إحدى الدوال مفصولة تمامًا عن المتغيرات المحلية في دالة أخرى. يمكن قراءة المتغيرات العامة من مجال محلي أمعن النظر في المثال الآتي: def spam(): print(eggs) eggs = 42 spam() print(eggs) حينما حاولنا طباعة المتغير eggs ضمن الدالة spam() بحثت بايثون عن متغير أو معامل باسم eggs في الدالة spam() لكنها لم تجد، فحينها ستعدّه إشارةً إلى المتغير العام eggs، ولهذا سيطبع البرنامج السابق القيمة 42 حين تنفيذه. المتغيرات المحلية والعامة التي تحمل الاسم نفسه من المقبول تمامًا من الناحية التقنية في بايثون استخدام نفس الاسم لمتغير في المجال العام وآخر في المجال المحلي؛ لكن لتسهيل مقروئية الشيفرة فحاول تجنب فعل ذلك. لترى ما سيحدث فجرب الشيفرة الآتية: def spam(): ➊ eggs = 'spam local' print(eggs) # 'spam local' def olive(): ➋ eggs = 'olive local' print(eggs) # 'olive local' spam() print(eggs) # 'olive local' ➌ eggs = 'global' olive() print(eggs) # 'global' سيظهر الناتج الآتي حينما تجرب تشغيل البرنامج السابق: olive local spam local olive local global هنالك ثلاثة متغيرات مختلفة في البرنامج، لكنها كلها مسماة eggs، وهي كما يلي: ➊ متغير باسم eggs موجود في المجال المحلي للدالة spam(). ➋ متغير باسم eggs موجود في المجال المحلي للدالة olive(). ➌ متغير باسم eggs موجود في المجال العام. ولأن هذه المتغيرات المختلفة لها نفس الاسم فسيكون من العسير تتبع أيها يستعمل الآن؛ لذا تجنب استخدام نفس الاسم لأكثر من متغير. العبارة البرمجية global إذا أردت تعديل قيمة متغير عام ضمن دالة، فعليك استخدام العبارة البرمجية global. فإذا كان لديك سطر يشبه global eggs في بداية إحدى الدوال فهذا سيخبر بايثون أن «المتغير eggs في هذه الدالة يشير إلى متغير عام، ولا حاجة إلى إ،شاء متغير محلي بهذا الاسم». فمثلًا جرب الشيفرة الآتية: def spam(): ➊ global eggs ➋ eggs = 'spam' eggs = 'global' spam() print(eggs) استدعاء الدالة print() سيؤدي إلى إظهار الناتج الآتي: spam ولأننا صرحنا أن المتغير eggs هو عام global في بداية الدالة spam() ➊، فحينما نضبط eggs إلى 'spam' ➋ فستجرى عملية الإسناد إلى المتغير eggs العام، ولن ينشأ أي متغير محلي. هنالك أربع قواعد لمعرفة إن كان المتغير في المجال المحلي أم العام: إذا كان المتغير مستخدمًا في المجال العام، أي خارج جميع الدوال، فسيكون متغيرًا عامًا دومًا. إذا كانت هنالك عبارة global في إحدى الدوال، فسيكون المتغير عامًا في تلك الدالة. خلاف ذلك إذا استخدم المتغير في عبارة الإسناد داخل دالة ما، فسيكون متغيرًا محليًا. لكن إن لم يستخدم ذاك المتغير في عبارة إسناد داخل الدالة فسيكون متغيرًا عامًا. لتأخذ فكرة أفضل عن هذه القواعد فاكتب البرنامج الآتي في محرر الشيفرات واحفظه وجربه: def spam(): ➊ global eggs eggs = 'spam' # this is the global def olive(): ➋ eggs = 'olive' # this is a local def Steak(): ➌ print(eggs) # this is the global eggs = 42 # this is the global spam() print(eggs) سيكون المتغير eggs في الدالة spam() عامًا لوجود العبارة global في بداية الدالة ➊، وسيكون المتغير eggs محليًا في الدالة olive() لاستعماله في عبارة إسناد في تلك الدالة ➋، وسيكون eggs عامًا في steak() ➌ لعدم استخدامه في عبارة إسناد أو العبارة global. إذا شغلت البرنامج السابق فستكون النتيجة هي: spam كقاعدة عامة: سيكون المتغير في دالةٍ ما إما عامًا أو محليًا، ولا يمكن استخدام متغير محلي في دالة باسم eggs ثم استخدام متغير عام بنفس الاسم لاحقًا في الدالة ذاتها. إذا حاولت استخدام متغير محلي ضمن دالة قبل أن تسند قيمةً له كما في البرنامج الآتي، فستظهر لك رسالة خطأ: def spam(): print(eggs) # ERROR! ➊ eggs = 'spam local' ➋ eggs = 'global' spam() ستظهر رسالة الخطأ الآتية إذا حاولت تجربة البرنامج السابق: Traceback (most recent call last): File "C:/sameNameError.py", line 6, in <module> spam() File "C:/sameNameError.py", line 2, in spam print(eggs) # ERROR! UnboundLocalError: local variable 'eggs' referenced before assignment يظهر الخطأ لأن بايثون سترى عبارة إسناد للمتغير eggs ضمن الدالة spam() ➊ وبالتالي ستعد المتغير على أنه محلي، لكننا نحاول طباعة قيمة eggs قبل إسناد أي قيمة له، أي أن المتغير المحلي eggs غير موجود، فسيظهر الخطأ ولن تستعمل بايثون المتغير العام eggs ➋. تعامل مع الدوال على أنها «صناديق سوداء» عادةً كل ما تحتاج إليه لاستخدام دالة هو معرفة ما هي المدخلات (أي ما هي المعاملات التي تأخذها) وما هي المخرجات؛ فلا حاجة إلى أن تثقل على نفسك بمعرفة كيف تعمل شيفرة تلك الدالة. فحاول أن تنظر إلى الدوال نظرة شاملة عالية المستوى، وبإمكانك معاملتها على أنها «صندوق أسود». هذه الفكرة أساسية في البرمجة الحديثة، وسترى عدة وحدات في المقالات اللاحقة من هذه السلسلة فيها دوال مكتوبة من مبرمجين آخرين، وصحيح أنك تستطيع النظر إلى الشيفرة المصدرية لها لكن لا حاجة إلى أن تفهم كيف تعمل لكي تستعملها؛ ولأن من المستحسن كتابة الدوال دون أن تتعامل مع المتغيرات العامة فلا تقلق حول تفاعل الدوال مع المتغيرات الموجودة في المجال العام لبرنامجك. التعامل مع الاستثناءات حينما يحدث خطأ -أو بتعبير أدق «استثناء» exception- في برنامجك فهذا يعني توقف عملية التنفيذ كلها. ولا تريد أن يحدث ذلك عمليًا في البرامج الحقيقية، وإنما تريد أن يحس برنامجك بوجود الأخطاء ويتعامل معها ثم يكمل تنفيذه بسلام. فالبرنامج الآتي يتسبب بخطأ القسمة على صفر. جربه: def spam(divideBy): return 42 / divideBy print(spam(2)) print(spam(12)) print(spam(0)) print(spam(1)) عرفنا الدالة spam() ومررنا إليها وسيطًا بقيم مختلفة وطبعنا قيمة قسمة العدد 42 على القيمة الممررة. هذا هو ناتج تنفيذ الشيفرة السابقة: 21.0 3.5 Traceback (most recent call last): File "C:/zeroDivide.py", line 6, in <module> print(spam(0)) File "C:/zeroDivide.py", line 2, in spam return 42 / divideBy ZeroDivisionError: division by zero يظهر الاستثناء ZeroDivisionError حينما نقسم عددًا على صفر. ويظهر لنا رقم السطر الذي يسبب هذا الاستثناء، وستعرف منه أن العبارة return في الدالة spam() هي من تسبب الخطأ. يمكن التعامل مع الاستثناءات باستخدام العبارتين try و except. إذ نضع الشيفرة التي قد تسبب خطأ أو استثناءً ضمن قسم العبارة try، وسينتقل تنفيذ البرنامج إلى بداية القسم الذي يلي العبارة except في حال حدوث استثناء. يمكنك وضع الشيفرة التي قد تسبب بخطأ القسمة على الصفر ضمن كتلة try واستخدام كتلة except للتعامل مع حدوث الخطأ: def spam(divideBy): try: return 42 / divideBy except ZeroDivisionError: print('Error: Invalid argument.') print(spam(2)) print(spam(12)) print(spam(0)) print(spam(1)) حينما تتسبب الشيفرة الموجودة ضمن try باستثناء، فسينتقل تنفيذ البرنامج مباشرةً إلى الشيفرة الموجودة في except، وبعد تنفيذ تلك الشيفرة فسيكمل تنفيذ البرنامج بشكل طبيعي. ستكون نتيجة تنفيذ الشيفرة السابقة هي: 21.0 3.5 Error: Invalid argument. None 42.0 لاحظ أن أية أخطاء تحدث أثناء استدعاءات الدوال ضمن كتلة try فستعالج أيضًا. جرب البرنامج الآتي التي يستدعي الدالة spam() ضمن كتلة try: def spam(divideBy): return 42 / divideBy try: print(spam(2)) print(spam(12)) print(spam(0)) print(spam(1)) except ZeroDivisionError: print('Error: Invalid argument.') سيكون الناتج كما يلي: 21.0 3.5 Error: Invalid argument. سبب عدم تنفيذ print(spam(1)) هو انتقال التنفيذ إلى الشيفرة الموجودة في كتلة except مباشرةً، ولن تعود لإكمال بقية كتلة try، بل ستكمل تنفيذ بقية البرنامج كالمعتاد. برنامج قصير لرسم زكزاك لنستعمل المفاهيم البرمجية التي تعلمناها حتى الآن لإنشاء برنامج حركي بسيط. سينشِئ هذا البرنامج شكل زكزاك إلى أن يوقفه المستخدم بالضغط على زر Stop في محرر Mu أو بالضغط على Ctrl+C. سيبدو ناتج البرنامج بعد تنفيذه كما يلي: ******** ******** ******** ******** ******** ******** ******** ******** ******** اكتب الشيفرة الآتية واحفظها في ملف باسم zigzag.py: import time, sys indent = 0 # كم فراغًا نضع كمسافة بادئة indentIncreasing = True # هل ستزيد المسافة البادئة أم لا try: while True: # حلقة تكرار البرنامج الأساسية print(' ' * indent, end='') print('********') time.sleep(0.1) # توقف لعُشر ثانية if indentIncreasing: # زيادة المسافة البادئة indent = indent + 1 if indent == 20: # تغيير الاتجاه indentIncreasing = False else: # إنقاص عدد الفراغات indent = indent - 1 if indent == 0: # تغيير الاتجاه indentIncreasing = True except KeyboardInterrupt: sys.exit() لننظر إلى الشيفرة سطرًا بسطر بدءًا من الأعلى. import time, sys indent = 0 # كم فراغًا نضع كمسافة بادئة indentIncreasing = True # هل ستزيد المسافة البادئة أم لا في البداية علينا أن نستورد الوحدتين time و sys، وسيستخدم برنامجنا متغيرين اثنين: المتغير indent الذي يتتبع كم فراغًا يجب أن نضع كمسافة بادئة قبل النجوم الثمانية، و indentIncreasing الذي يحتوي على قيمة منطقية بوليانية لتحدد إذا كانت المسافة البادئة ستزيد أم تنقص. try: while True: # حلقة تكرار البرنامج الأساسية print(' ' * indent, end='') print('********') time.sleep(0.1) # توقف لعُشر ثانية ثم وضعنا بقية البرنامج داخل عبارة try؛ فحينما يضغط المستخدم على Ctrl+C أثناء تشغيل برنامج بايثون فستطلق الاستثناء KeyboardInterrupt، وإذا لم تكن هنالك عبارة try-except لمعالجة الاستثناء فسينهار البرنامج وتظهر رسالة خطأ قبيحة. لتفادي ذلك سنعالج الاستثناء KeyboardInterrupt بأنفسنا باستدعاء الدالة sys.exit() (هذه الشيفرة موجودة بعد نهاية كتلة try). حلقة التكرار اللانهائية while True: ستكرر التعليمات الموجودة في برنامجنا للأبد، واستخدمنا التعبير ' ' * indent لطباعة العدد الصحيح من المسافات البادئة، لكننا لا نريد أن ننتقل إلى سطر جديد بعد تلك الفراغات فنمرر الوسيط end='' إلى الدالة print(). الاستدعاء الثاني للدالة print() سيطبع لنا 8 نجوم. لم نشرح الدالة time.sleep() بعد، لكن يكفي القول أنها توقف تشغيل البرنامج مؤقتًا لعُشر ثانية 0.1. if indentIncreasing: # زيادة المسافة البادئة indent = indent + 1 if indent == 20: # تغيير الاتجاه indentIncreasing = False ثم سنعدل مقدار المسافات البادئة للمرة القادمة التي تنفذ فيها حلقة while. فإذا كان indentIncreasing هو True فسنضيف واحد إلى indent. لكن حينما تصل المسافة البادئة إلى 20 فنرغب بتقليل المسافة البادئة: else: # إنقاص عدد الفراغات indent = indent - 1 if indent == 0: # تغيير الاتجاه indentIncreasing = True أما إذا كانت indentIncreasing هي False فسننقص واحد من المتغير indent، وحينما تصل قيمته إلى 0 فسنحتاج إلى زيادة المسافات البادئة مجددًا، وفي كلتا الحالتين سيعود تنفيذ البرنامج إلى بداية حلقة التكرار لطباعة النجوم مجددًا. except KeyboardInterrupt: sys.exit() إذا ضغط المستخدم على Ctrl+C في أي مرحلة من مراحل تنفيذ حلقة التكرار الموجودة داخل كتلة try فسيطلق الاستثناء KeyboardInterrrupt ثم يعالج في عبارة except، سيستمر تنفيذ البرنامج داخل كتلة expect الذي سيشغل الدالة sys.exit() لإنهاء البرنامج. وعلى الرغم من أن حلقة التكرار لانهائية، لكننا نوفر طريقة آمنة لإنهاء تشغيل التطبيق من المستخدم. الخلاصة الداوال هي طريقة أساسية لتجزئة شيفراتك إلى مجموعات، ولأن المتغيرات داخل الدوال تكون في مجال محلي خاص بها فلا تؤثر الشيفرات الموجودة في إحدى الدوال على الأخرى، وبالتالي تقل الشيفرات المسؤولة عن تغيير قيمة أحد المتغيرات وبالتالي تسهل عملية تنقيح البرنامج ومعرفة الأخطاء. الدوال هي أداة رائعة لتنظيم شيفراتك، ويمكنك أن تفكر فيها على أنها صناديق سوداء: فهي تقبل المدخلات على شكل معاملات وتخرج النتائج على شكل قيم معادة، والشيفرات داخلها لا تؤثر على بقية الدوال. تعلمنا استخدام العبارتين try و except التي تتولى معالجة الاستثناءات، فكان حدوث أي خطأ في البرنامج سيؤدي إلى انهياره كما رأينا في المقالات السابقة، لكن بتعلمنا لمعالجة الاستثناءات أصبح بإمكاننا بناء تطبيقات تتعامل مع الأخطاء الشائعة دون مشاكل. والآن بعد أن تعلمت الدوال في هذا المقال، ما رأيك أن تجرب ما تعلمته في التطبيق العملي الموالي؟ وطبعًا، لا تنسَ مشاركتنا نتائج تطبيقك في التعليقات: اكتب دالةً باسم collatz() التي تقبل معاملًا واحدًا اسمه number، إذا كان number زوجيًا فستطبع الدالة number // 2 ثم تعيد تلك القيمة، وإذا كان العدد number فرديًا فستطبع وتعيد ناتج 3 * number + 1. ثم اكتب برنامجًا يسمح للمستخدم بإدخال رقم صحيح واستمر باستدعاء الدالة collatz() على ذاك الرقم حتى تعيد الدالة القيمة 1. (ستعمل هذه الدالة لجميع الأعداد الصحيحة، وستكون النتيجة دومًا 1! لا يعرف علماء الرياضيات تحديدًا لماذا، لكن برنامجك هو تطبيق عملي على معضلة كولاتز، حتى أن بعضهم يطلق عليها «أبسط معضلة رياضية مستحيلة»). تذكر أن تحول القيمة المعادة من input() إلى رقم صحيح عبر الدالة int()، وإلا فستعدها بايثون على أنها سلسلة نصية. تلميحة: يكون العدد number زوجيًا إذا كان باقي القسمة على 2 هو 0 أي number % 2 == 0 وفرديًا إذا كان number % 2 == 1. يجب أن يبدو شكل تنفيذ البرنامج كما يلي: Enter number: 3 10 5 16 8 4 2 1 ترجمة -وبتصرف- للفصل Functions من كتاب Automate the boring stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: بنى التحكم في لغة بايثون Python أنواع البيانات والعمليات الأساسية في لغة بايثون تعلم لغة بايثون النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون
-
أصبحت تعرف أن هنالك تعليمات وأن البرامج تتألف من سلسلة من التعليمات، لكن قوة البرمجة الحقيقية لا تقع في تنفيذ مجموعة من التعليمات واحدةً تلو الأخرى وكأنك تتبضع قائمة التسوق. فاعتمادًا على نتيجة بعض التعبيرات البرمجية يستطيع البرنامج أن يتخطى تنفيذ مجموعة من التعليمات أو تكررها أو يختار بعضها لتنفيذه. من النادر أن تجد برامج تنفذ من أول سطر حتى آخر سطر بالترتيب، لذا تكون هنالك حاجة إلى بنى التحكم flow control لتقرير ما هي التعليمات البرمجية التي ستنفذ ووفق أي شروط. يمكن بسهولة تحويل بنى التحكم البرمجية إلى رموز في مخططات التدفق flowcharts التي نرسمها، لذا سأوفر لك نسخة منها تحاكي الشيفرات التي نكبها. الشكل الآتي يظهر مخططًا تدفقيًا يساعد في اتخاذ القرار عمّا سنفعله إذا كان الجو ماطرًا. تتبع الأسهم من البداية إلى النهاية: مخطط تدفقي يخبرك ما تفعل إن كانت السماء تمطر من الشائع أن يكون في المخططات التدفقية أكثر من طريق من البداية إلى النهاية، والأمر سيان بالنسبة إلى شيفرات تطبيقات الحاسوب. تُمثَّل نقاط التفرع في المخططات التدفقية باستخدام شكل المعيّن diamond بينما تُمثَّل بقية الخطوات بمستطيلات عادية، وتكون البداية والنهاية على شكل مستطيل بحواف مدورة. قبل أن تتعلم عن عبارات بنى التحكم، فعليك أن تعرف كيفية تمثيل خيارات «نعم» و «لا»، بعدها ستحتاج إلى فهم كيفية كتابة نقاط التفرع بلغة بايثون؛ ولهذا الغرض سنحتاج إلى تعلم القيم المنطقية أو البوليانية Boolean وعوامل المقارنة والعوامل المنطقية. القيم المنطقية Boolean يكون لأنواع البيانات التي تعلمناها سابقًا من سلاسل نصية وأرقام صحيحة وأرقام ذات فاصلة، عدد لا متناهي من القيم التي يمكن أن تأخذها. لكن للقيم المنطقية أو البوليانية تملك نوعين من القيم فقط: True و False (نقول عنها أنها قيم بوليانية Boolean نسبةً إلى العالم الرياضي جورج بولي)، وحين إدخالها في شيفرة بايثون فستلاحظ عدم وجود علامات الاقتباس التي تحيط بالسلاسل النصية، وتبدأ دومًا بالحرف الكبير T أو F وتكون بقية الكلمة بأحرف صغيرة. أدخل ما يلي في الصدفة التفاعلية، لاحظ الأخطاء التي ستظهر لك في بعض التعليمات: ➊ >>> spam = True >>> spam True ➋ >>> true Traceback (most recent call last): File "<pyshell#2>", line 1, in <module> true NameError: name 'true' is not defined ➌ >>> True = 2 + 2 SyntaxError: can't assign to keyword وكأي قيمة أخرى، يمكن للقيم المنطقية أن تستعمل في التعابير البرمجية ويمكن أن تخزن في المتغيرات ➊، وإن لم تستعمل حالة الأحرف الصحيحة ➋ أو جربت استخدام True أو False لأسماء المتغيرات ➌ فستظهر لك رسالة خطأ. عوامل المقارنة تقارن عوامل المقارنة comparison operators بين قيمتين وتكون النتيجة هي قيمة منطقية بوليانية واحدة، الجدول الآتي يستعرض عوامل المقارنة: العامل المعنى == يساوي != لا يساوي < أصغر > أكبر <= أصغر أو يساوي >= أكبر أو يساوي الجدول 2: عوامل المقارنة تكون نتيجة استخدام هذه العوامل هي True أو False اعتمادًا على القيم التي تعطيها لها. لنجرب الآن بعض تلك العوامل ولنبدأ بالعاملين == و !=: >>> 42 == 42 True >>> 42 == 99 False >>> 2 != 3 True >>> 2 != 2 False وكما قد تتوقع، ستكون نتيجة عامل «يساوي» == هي True حينما تساوت القيمتان على يمينه ويساره، وعامل «لا يساوي» ستكون نتيجته True حينما تختلف القيمتان على يمينه ويساره. يمكننا استخدام العاملين == و != على أي نوع من أنواع البيانات: >>> 'hello' == 'hello' True >>> 'hello' == 'Hello' False >>> 'dog' != 'cat' True >>> True == True True >>> True != False True >>> 42 == 42.0 True ➊ >>> 42 == '42' False لاحظ أن القيم العددية سواءً كانت صحيحة int أو ذات فاصلة عائمة float لا تتساوي مع القيمة النصية. فالتعبير 42 == '42' ➊ نتيجته هي Flase لأن بايثون تعدّ الرقم 42 مختلفًا عن السلسلة النصية '42'. أما المعاملات > و < و <= و >= في لا تعمل إلا مع القيم العددية: >>> 42 < 100 True >>> 42 > 100 False >>> 42 < 42 False >>> eggCount = 42 ➊ >>> eggCount <= 42 True >>> myAge = 29 ➋ >>> myAge >= 10 True الفرق بين عامل = و == قد تلاحظ أن عامل المساواة == يحتوي على إشارتي يساوي، بينما عامل الإسناد فيه إشارة يساوي واحدة. ومن السهل الخلط بينهما بالنسبة إلى المبتدئين، لذا تذكر أن: عامل المساواة == يتأكد إن كانت القيمتان عن يمينه ويساره متساويتين. عامل الإسناد = يضع التي على اليمين في المتغير الذي على اليسار. قد يساعدك في التذكر أن عامل المساواة == فيه حرفان، مثل معامل عدم المساواة != تمامًا. ستستخدم عوامل المقارنة كثيرًا لمقارنة قيمة أحد المتغيرات مع قيمة أخرى، كما في eggCount <= 42 ➊ و myAge >= 10 ➋، فلو كنتَ تعرف قيمة المتغير كيف ستكون قبل أن تشغل برنامجك فلا حاجة إلى المقارنة كلها، فليس من المنطقي أن تكتب 'dog' != 'cat' في شيفرتك بل تكتب True مباشرةً. سترى أمثلة كثيرة عن ذلك أثناء تعلمك لبنى التحكم. العوامل المنطقية البوليانية تستخدم العوامل المنطقية الثلاثة and و or و not لمقارنة القيم المنطقية، وكما في عوامل المقارنة ستكون نتيجة هذه التعابير هي قيمة منطقية، ولنبدأ شرح هذه العوامل بالتفصيل بدءًا من العامل and و or. العوامل المنطقية الثنائية يعمل العاملان and و or على قيمتين أو تعبيرين منطقيين، لهذا يسميان بالعوامل المنطقية الثنائية binary Boolean operators. ينتج العامل nad القيمة True إذا كانت كلا القيمتان المنطقيتان تساوي True، وإلا فالنتيجة هي False. أدخل التعابير البرمجية الآتية في الصدفة التفاعلية لترى أثر هذا العامل عمليًا: >>> True and True True >>> True and False False جداول الحقيقة truth table هو جدول في الجبر المنطقي البولياني يوضح ناتج كل شكل من أشكال التعابير المنطقية. جدول الحقيقة الآتي يوضح ناتج كل عملية ممكنة مع العامل and: التعبير النتيجة True and True True True and False False False and True False False and False False الجدول 2: جدول الحقيقة للعامل and وفي المقابل تكون نتيجة العامل or هي True إذا كان أحد القيمتين المنطقيتين يساوي True، أما إذا كانت كلتاهما Flase فنتيجة التعبير هي False: >>> False or True True >>> False or False False يمكنك أن تعرف ناتج كل تعبير ممكن مع العامل or من جدول الحقيقة الخاص به. التعبير النتيجة True or True True True or False True False or True True False or False False الجدول 3: جدول الحقيقة للعامل or العامل not وعلى النقيض من العاملين and و or، يستخدم العامل not على قيمة أو تعبير منطقي واحد، ولهذا هو عامل أحادي unary operator. ما يفعله العامل not هو عكس القيمة المنطقية الحالية: >>> not True False ➊ >>> not not not not True True وكما في نفي النفي في حديثنا العادي، يمكننا أن نجعل العامل not متشعبًا ➊، لكن لا يوجد سبب لفعل ذلك في البرامج العملية. الجدول الآتي هو جدول الحقيقة للعامل not: التعبير النتيجة not True False not False True الجدول 4: جدول الحقيقة للعامل not المزج بين العوامل المنطقية وعوامل المقارنة لما كانت نتيجة استخدام عوامل المقارنة هي قيمة منطقية، فيمكننا استخدامها في تعابير برمجية مع العوامل المنطقية. تذكر أن العوامل المنطقية and و or و not هي عوامل منطقية لأنها تعمل على القيم المنطقية True و False؛ بينما التعابير التي تحتوي على عوامل مقارنة مثل 4 < 5 ليست قيمًا منطقية بحد ذاتها لكنها تعابير تُنتِج قيمًا منطقية. جرب إدخال بعض التعابير المنطقية التي فيها عوامل مقارنة في الصدفة التفاعلية: >>> (4 < 5) and (5 < 6) True >>> (4 < 5) and (9 < 6) False >>> (1 == 2) or (2 == 2) True سيقدر الحاسوب قيمة التعبير على اليسار أولًا، ثم قيمة التعبير الذي على اليمين، ثم بعد أن يعرف ما هي القيمة المنطقية لكلٍ منها فسيقدر قيمة التعبير البرمجي كاملًا ويخرج قيمة منطقية وحيدة. يمكنك أن تتخيل أن عملية تقدير قيمة التعبير البرمجي (4 < 5) and (5 < 6) تشبه ما يلي: يمكنك أن تستعمل أكثر من عامل منطقي في تعبير واحد، بالإضافة إلى عوامل المقارنة: >>> 2 + 2 == 4 and not 2 + 2 == 5 and 2 * 2 == 2 + 2 True سيكون للعوامل المنطقية ترتيب لتقدير القيمة كما في العوامل الرياضية، فبعد تقدير قيمة العمليات الحسابية وعمليات المقارنة، ستعمل بايثون على عوامل not أولًا، ثم عوامل and ثم عوامل or. عناصر بنى التحكم تبدأ عبارات بنى التحكم بجزء يسمى عادةً بالشرط condition ويكون متبوعًا دومًا بكتلة برمجية اشتراطية clause، وقبل أن نتعلم عن بنى التحكم في بايثون فسنشرح ما هو الشرط وما هي الكتلة البرمجية. الشروط تعدّ جميع التعابير المنطقية البوليانية التي رأيتها حتى الآن شروطًا conditions، فالشروط هي تعابير برمجية، لكنها تطلق في سياق الحديث عن بنى التحكم. يكون للشروط دومًا نتيجة منطقية إما True أو False، وتقرر بنى التحكم ما الذي يجب فعله اعتمادًا على الشرط إن كان True أو False، وجميع بنى التحكم تقريبًا تستخدم الشروط. الكتل البرمجة تجمَِع أسطر الشيفرات البرمجية في بايثون على شكل كتل blocks، ويمكنك أن تعرف متى تبدأ الكتلة ومتى تنتهي من المسافة البادئة indent للأسطر البرمجية. هنالك ثلاث قواعد للكتل البرمجية في بايثون: تبدأ الكتلة البرمجية حين زيادة المسافة البادئة. يمكن أن تحتوي الكتل البرمجية على كتل أخرى. تنتهي الكتل حينما تقل المسافة البادئة إلى الصفر أو إلى المسافة البادئة للكتلة الأب. من الأسهل فهم الكتل البرمجية بالنظر إلى بعض الشيفرات التي لها مسافة بادئة، لذا لنعثر على الكتل البرمجية في البرنامج البسيط الآتي: name = ‘Ahmed’ password = 'swordfish' if name == 'Ahmed': ➊ print('Hello, Ahmed’) if password == 'swordfish': ➋ print('Access granted.') else: ➌ print('Wrong password.') تبدأ أول كتلة من الشيفرات ➊ في السطر print('Hello, Ahmed’) وتضم إليها جميع الأسطر البرمجية التي تليها، وداخل هذه الكتلة هنالك كتلة أخرى ➋ التي تحتوي سطرًا واحدًا داخلها فيه print('Access granted.')، أما الكتلة الثالثة ➌ والأخيرة ففيها print('Wrong password.'). تنفيذ البرنامج تبدأ بايثون بتنفيذ مثالنا السابق hello.py من أول البرنامج حتى نهايته سطرًا بسطر، وعملية تنفيذ البرنامج (التي تسمى program execution أو اختصارًا execution) هي اصطلاح يشير إلى التعليمة البرمجية التي يجري تنفيذها حاليًا، فلو كانت شيفرة برنامجك مطبوعةً على ورقة وتشير بإصبعك إلى السطر الذي يجري تنفيذه حاليًا فتشير إصبعك هنا إلى خط سير تنفيذ البرنامج. لا تنفذ جميع البرامج من الأعلى إلى الأسفل، فلو كنت تستعمل إصبعك لتتبع خط سير أحد البرامج التي فيها بنى تحكم فسترى أنك تنتقل من مكانٍ إلى آخر في الشيفرة المصدرية، وأنك قد تتخطى أجزاء من الشيفرة كليًا. عبارات بنى التحكم حان الوقت الآن لنتحدث عن أهم جزء من بنى التحكم: عبارات بنى التحكم نفسها. تمثل العبارات في مخططات التدفق -مثل التي رأيتها في صورة "مخطط تدفقي يخبرك ما تفعل إن كانت السماء تمطر" على شكل معيّن، وتشير إلى القرارات التي يتخذها برنامجك. عبارة if أكثر نوع شائع من بنى التحكم هو العبارة الشرطية if، ففيها ستُنفَّذ الكتلة البرمجية التي تلي if إذا كان الشرط محققًا أي True، وسيتخطاها البرنامج إن لم يكن الشرط محققًا أي False. فإذا أردنا أن نقرأ عبارة if البرمجة باللغة العربية فسنقول «إذا كان الشرط محققًا، فنفذ الكتلة البرمجية الآتية». تتألف عبارة if في بايثون مما يلي: الكلمة المفتاحية if الشرط، وهو التعبير الذي تكون نتيجته هي True أو False نقطتان رأسيتان : كتلة برمجية تلي ما سبق، تكون فيها الأسطر مسبوقة بمسافة بادئة لنقل مثلًا أنك تريد كتابة شيفرة تتحقق إن كان اسم المستخدم هو Ahmed، وعلى فرض أننا قد أسندنا سابقًا قيمةً ما إلى المتغير name: if name == 'Ahmed': print('Hi, Ahmed.') تنتهي جميع عبارات بنى التحكم بنقطتين رأسيتين : متبوعة بكتلة برمجية، والكتلة البرمجية في مثالنا هي التي تحتوي على print('Hi, Ahmed.'). يوضح الشكل الآتي المخطط التدفقي للشيفرة السابقة: عبارات else يمكن اختياريًا أن يأتي بعد كتلة if العبارة else، وتنفذ كتلة else في حال كان شرط عبارة if غير محقق False. أي بالعربية يمكننا أن نقول «إذا كان الشرط محققًا، فنفذ الكتلة البرمجية الآتية، وإلا فنفذ هذه الكتلة». لا تملك عبارة else شرطًا، وتتألف else مما يلي: الكلمة المفتاحية else نقطتان رأسيتان : كتلة برمجية تلي ما سبق، تكون فيها الأسطر مسبوقة بمسافة بادئة وبالعودة إلى مثالنا السابق للترحيب بالمستخدم، فسنضيف العبارة else لطباعة تحية مختلفة لأي شخص ليس اسمه أحمد: if name == 'Ahmed': print('Hi, Ahmed.') else: print('Hello, stranger.') يبين المخطط التدفقي للبرنامج السابق عبارات elif تعرفنا سابقًا على عبارة if و else التي يجب أن تنفذ إحداهما، لكن ماذا لو كنّا نريد وجود أكثر من احتمال أو أكثر من شرط؟ تعمل العبارة elif كأنها «وإلا إذا كان كذا» else if، وتأتي بعد عبارة if أو elif أخرى. توفر عبارة elif شرطًا بديلًا يمكن التحقق إن كان محققًا إن كانت الشروط التي تسبقه غير محققة. تتألف عبارة elif في بايثون مما يلي: الكلمة المفتاحية elif الشرط، وهو التعبير الذي تكون نتيجته هي True أو False نقطتان رأسيتان : كتلة برمجية تلي ما سبق، تكون فيها الأسطر مسبوقة بمسافة بادئة لنضف عبارة elif إلى برنامجنا الذي نتحقق فيه من اسم المستخدم: if name == 'Ahmed': print('Hi, Ahmed.') elif age < 12: print('You are not Ahmed, kiddo.') سنتحقق هذه المرة من عمر المستخدم، فإن كان عمره أقل من 12 فسيقول له «أنت لست أحمد يا غلام!». يمكننا تمثيل البرنامج بمخطط التدفق الآتي: مخطط التدفق للعبارة elif ستفَّذ الكتلة البرمجية التي تلي elif إن كان عمر المستخدم age < 12 وكان الشرط name == 'Ahmed' غير محقق False. لكن إن كان كلا الشرطين غير محقق فسيتجاوز البرنامج تنفيذ الكتلتين البرمجيتين، وليس من الضروري أي ينفذ أحد الكتل البرمجية، فقد تنفذ عبارة واحدة أو لا تنفذ أي عبارة. بعد أن يتحقق شرط إحدى العبارات الشرطية فسيتجاوز البرنامج بقية عبارات elif كلها. فعلى سبيل المثال أنشِئ الملف vampire.py وضع فيه الشيفرة الآتية: age = 3000 if name == Ahmed: print('Hi, Ahmed.') elif age < 12: print('You are not Ahmed, kiddo.') elif age > 2000: print('Unlike you, Ahmed is not an vampire.') elif age > 100: print('You are not Ahmed, grannie.') أضفنا هنا عبارتَي elif لبرنامج الترحيب بالمستخدم، وأضفنا جوابين مختلفين بناءً على العمر age. يظهر المخطط التدفقي الآتي سير عمل البرنامج: المخطط التدفقي لعبارات elif متعددة في برنامج vampire.py. لترتيب عبارات elif أهمية، ولتجربة أهمية ترتيبها فلنحاول إضافة علّة لبرنامجنا. تذكر أن البرنامج سيتخطى عبارات elif بعد تحقيق شرط إحداها، لذا إذا غيرنا ترتيب الشيفرة إلى ما يلي وحفظناه باسم vampire2.py: name = 'Abdullatif' age = 3000 if name == 'Ahmed': print('Hi, Ahmed.') elif age < 12: print('You are not Ahmed, kiddo.') ➊ elif age > 100: print('You are not Ahmed, grannie.') elif age > 2000: print('Unlike you, Ahmed is not an undead, vampire.') إذا جربنا البرنامج وكانت قيمة المتغير age تساوي 3000 مثلًا، فقد تتوقع أن برنامج سيطبع العبارة التي تقول أن عمره أكثر من 2000 سنة، لكن ولمّا كانت الشرط age > 100 محققًا True (وذلك لأن 3000 أكبر من 100 بالفعل) ➊، فستعرض عبارة الترحيب بالعجوز وستخطى تنفيذ البرنامج جميع عبارات elif الأخرى. لذا من المهم الانتباه إلى ترتيب عبارات elif. المخطط التدفقي الآتي يظهر سير تنفيذ الشيفرة السابقة. لاحظ كيف جرى تبديل المعين الذي يحتوي على age > 100 و age > 2000. المخطط التدفقي لتنفيذ برنامج vampire2.py، لاحظ أن الفرع الذي عليه إشارة × لا ينفذ أبدًا، لكنه إذا كان العمر age أكبر من 2000 فهو بكل تأكيد أكبر من 100 أيضًا. يمكنك أن تضع عبارة else بعد آخر عبارة elif اختياريًا، وفي هذه الحالة سنضمن تنفيذ كتلة برمجية واحدة فقط لا غير، أي في حال كانت جميع شروط if و elif هي False فستنفَّذ كتلة else. لنعد كتابة مثال الترحيب بالمستخدم لنستعمل فيه if و elif و else: name = 'Abdullatif' age = 3000 if name == 'Ahmed': print('Hi, Ahmed.') elif age < 12: print('You are not Ahmed, kiddo.') else: print('You are neither Ahmed nor a little kid.') يظهر الشكل الآتي المخطط التدفقي للمثال السابق، الذي سنسميه littleKid.py. المخطط التدفقي لبرنامج liitleKid.py. يمكننا وصف هذا النمط من بنى التحكم باللغة العربية: «إذا كان أول شرط محققًا فافعل كذا، وإلا إن كان الشرط الثاني محققًا فافعل كذا، وإلا فافعل كذا». من المهم أن تنتبه إلى ترتيب عبارات if وelif و else حين استخدامها لكي تتجنب العلل المنطقية في برامجك. وتذكر أن هنالك عبارة if وحيدة فقط لا غير، وأي عبارات elif يجب أن تأتي بعدها؛ وإذا أردت ضمان تنفيذ إحدى الكتل البرمجية فأنهِ بنى التحكم بعبارة else. حلقة التكرار while يمكنك أن تعيد تنفيذ إحدى الكتل البرمجية مرارًا وتكرارًا باستخدام عبارة while. وستُنفَّذ الشيفرة الموجودة في الكتلة التي تلي شرط while طالما كان الشرط محققًا True. تتألف عبارة while في بايثون مما يلي: الكلمة المفتاحية while الشرط، وهو التعبير الذي تكون نتيجته هي True أو False نقطتان رأسيتان : كتلة برمجية تلي ما سبق، تكون فيها الأسطر مسبوقة بمسافة بادئة يمكنك ملاحظة أن عبارة while شبيهة جدًا بعبارة if، والفرق بينهما هو في السلوك. فبعد الانتهاء من الكتلة التي تلي شرط if سيكمل البرنامج تنفيذ الشيفرة التي تليها، لكن في نهاية كتلة while فسيعود تنفيذ البرنامج إلى بداية عبارة while. نسمي عبارة while عادةً «حلقة التكرار while» أو «حلقة while». للنظر إلى الفرق بين العبارة الشرطية if وحلقة while لهما نفس الشرط وتفعلان نفس الفعل في الكتلة التي تليهما. هذه هي الشيفرة التي نستعمل العبارة الشرطية if فيها: spam = 0 if spam < 5: print('Hello, world.') spam = spam + 1 وهذه هي الشيفرة التي نستعمل الحلقة while فيها: spam = 0 while spam < 5: print('Hello, world.') spam = spam + 1 تشبه هذه التعابير بعضها بعضًا، إذ تتحقق if و while من أن قيمة المتغير spam أصغر من 5، ثم تعرض رسالةً ترحيبيةً وتزيد قيمة المتغير spam بمقدار 1. لكن حينما تشغل البرنامجين السابقين فستجد نتيجةً مختلفةً لكلٍ منهما، ففي البرنامج الذي فيه if سترى عبارة الترحيب مرة واحدة، بينما تكرر العبارة الترحيبية 5 مرات في برنامج while! لننظر إلى المخططين التدفقيين للبرنامجين السابقين في الشكلين المواليين لنفهم ما حدث: المخطط التدفقي للبرنامج الذي يحتوي على العبارة الشرطية if. المخطط التدفقي للبرنامج الذي يحتوي على العبارة الشرطية while. تتحقق العبارة الشرطية if من الشرط وتطبع "Hello, World" مرةً واحدةً بعد تحقق الشرط، ثم ينتهي تنفيذ البرنامج. بينما البرنامج الذي فيه حلقة while فسينفذ 5 مرات لأن قيمة العدد spam تزيد مرة في نهاية كل حلقة تكرار، وهذا يعني أن الحلقة ستنفذ 5 مرات قبل أن يصبح الشرط spam < 5 غير محقق False. ستجري عملية التحقق من شرط حلقة while في بداية كل دورة iteration، أي في بداية كل تنفيذ لحلقة while؛ وإذا كان الشرط محققًا True فستنفذ الكتلة، ثم بعد ذلك يعاد التحقق من الشرط مجددًا. لاحظ أنه إذا كان شرط حلقة while غير محقق False من أول مرة فلن تنفَّذ الحلقة أبدًا وسيتخطاها البرنامج. حلقة while المزعجة هذا برنامج بسيط يطلب منك باستمرار أن تدخل "your name" في سطر الأوامر حين سؤاله عن اسمك. أنشِئ ملفًا جديدًا من File ثم New وأدخل الشيفرة الآتية واحفظها في الملف yourName.py: ➊ name = '' ➋ while name != 'your name': print('Please type your name.') ➌ name = input() ➍ print('Thank you!') يضبط البرنامجُ المتغيرَ name إلى سلسلة نصية فارغة ➊، وبهذا يكون الشرط name != 'your name' محققًا True وبالتالي سيبدأ تنفيذ حلقة while ➋. سيسأل البرنامجُ المستخدمَ عن اسمه، ثم يأخذ المدخلات ويخزنها في المتغير name ➌، ولمّا كنّا قد وصلنا إلى نهاية حلقة التكرار فسنعود إلى بداية حلقة while ونتحقق من الشرط، وإن لم يكن الاسم name مساويًا إلى السلسلة النصية 'your name' فيسكون الشرط محققًا True وسيعاد تنفيذ حلقة while مجددًا. لكن حينما يدخل المستخدم الكلمتين your name فسيصبح شرط حلقة while غير محقق False، وبالتالي بدلًا من إعادة تكرار الحلقة فسيكمل برنامجنا تنفيذ بقية البرنامج ➍. المخطط التدفقي في الشكل الآتي يوضح آلية عمل البرنامج yourName.py: المخطط التدفقي للبرنامج yourName.py. لنجرب الآن البرنامج، بالضغط على زر F5 لتشغيله، ثم كتابة أي عبارة سوى "your name" عدة مرات قبل أن نرضخ للضغط الذي يمارسه البرنامج تجاهنا ونكتب "your name". Please type your name. Abdullatif Please type your name. Hsoub Please type your name. %#@#%*(^&!!! Please type your name. your name Thank you! إذا لم ندخل "your name" أبدًا فلن يصبح شرط while غير محقق False وبالتالي سيستمر تنفيذ البرنامج إلى الأبد. وفي حالة برنامجنا كانت لدينا الدالة input() التي تمكننا من إكمال سير البرنامج حينما ندخل العبارة الصحيحة وبالتالي تتغير حالة الشرط، لكن قد لا يتغير الشرط في بعض البرامج مما يسبب مشكلة، لذا هنالك حاجة لتعلم طريقة للخروج من حلقة while. العبارة break هنالك طريقة نجعل فيها برنامجنا يخرج من حلقة while قبل انتهاء تنفيذها. فإذا وصل التنفيذ إلى عبارة break فسيخرج البرنامج من حلقة while مباشرةً. وتتألف عبارة break في بايثون من الكلمة المحجوزة break فقط. أليس ذلك بسيطًا؟ لنكتب برنامجًا يشبه البرنامج السابق لكنه يستخدم العبارة break للخروج من حلقة التكرار، أدخِل الشيفرة الآتية واحفظها في ملف باسم yourName2.py: ➊ while True: print('Please type your name.') ➋ name = input() ➌ if name == 'your name': ➍ break ➎ print('Thank you!') ننشِئ في أول سطر ➊ حلقة تكرار لا نهائية، وذلك بجعل شرط حلقة while محققًا دومًا True، وبالتأكيد إذا وضعنا True شرطًا لحلقة while فستكون قيمته هي True دومًا. بعد أن يبدأ تنفيذ حلقة التكرار فلن يخرج البرنامج منها إلا إذا استخدام عبارة break، لاحظ أن حلقات التكرار اللانهائية التي لا ينتهي تنفيذها أبدًا هي علّة منطقية في البرامج. وكما في المثال السابق، سيطلب البرنامج من المستخدم أن يدخل your name ➋، وسنتحقق إن كان المتغير name يساوي 'your name' باستخدام البنية الشرطية if ➌، وإذا كان الشرط محققًا True فستنفذ العبارة break ➍، وسينتقل التنفيذ إلى خارج الحلقة وستطبع رسالة الشكر ➎. إذا لم يكن شرط العبارة if محققًا فهذا يؤدي إلى دورة جديدة لحلقة while، وسيتحقق البرنامج من شرط تنفيذ حلقة while ➊، ولمّا كان الشرط محققًا True دومًا، فستنفذ حلقة التكرار مجددًا وتسأل المستخدم أن يدخل your name. المخطط التدفقي في الشكل التالي يوضح آلية عمل البرنامج yourName2.py: المخطط التدفقي للبرنامج yourName2.py مع حلقة تكرار لا نهائية، لاحظ أن المسار × لا ينفذ منطقيًا أبدًا لأن شرط الحلقة هو True دومًا. هل وقعت في حلقة تكرار لا نهائية؟ إذا شغلت تطبيقًا يحتوي على علة تؤدي إلى حلقة تكرار لا نهائية، فاضغط على Ctrl+C أو أعد تشغيل الصدفة من Shell ثم Restart Shell؛ مما يرسل إشارة KeyboardInterrupt إلى برنامجك تؤدي إلى إيقاف تشغيله مباشرةً. يمكنك التجربة بإنشاء برنامج بسيط باسم infiniteLoop.py: while True: print('Hello, world!') سيطبع البرنامج السابق العبارة Hello, World! إلى اللانهاية لأن شرط حلقة while محقق True دومًا. قد تستفيد من استخدام الاختصار Ctrl+C لإنهاء تنفيذ البرامج حتى دون أن تكون عالقًا في حلقة تكرار لا نهائية. عبارة continue وكما في عبارة break، نستعمل العبارة continue داخل حلقات التكرار، وحينما يصل التنفيذ إلى عبارة continue فسينتقل تنفيذ البرنامج إلى بداية حلقة التكرار مباشرةً ويعيد التحقق من شرط الحلقة، أي نفس ما يحدث حين الوصول إلى نهاية دورة حلقة التكرار. لنستخدم continue لكتابة برنامج يسأل عن اسم المستخدم وكلمة المرور، أدخل ما يلي في ملف جديد واحفظه باسم swordfish.py: while True: print('Who are you?') name = input() ➊ if name != 'Abdullatif': ➋ continue print('Hello, Abdullatif. What is the password? (It is a fish.)') ➌ password = input() if password == 'swordfish': ➍ break ➎ print('Access granted.') إذا أدخل المستخدم أي اسم باستثناء Abdullatif ➊ فستنقل العبارة continue ➋ التنفيذ إلى بداية حلقة التكرار، وحين إعادة التحقق من شرط الدخول إلى الحلقة فسيكون محققًا دومًا لأنه True. بعد أن يتجاوز المستخدم الشرط الموجود في if فسنسأله عن كلمة المرور ➌، وإذا أدخل كلمة المرور swordfish فستنفذ عبارة break ➍ وبالتالي نخرج من حلقة التكرار كليًا وستطبع العبارة Access granted ➎، وإذا لم تكن كلمة المرور صحيحةً فسنصل إلى نهاية دورة حلقة التكرار ثم نعود إلى بدايتها ونتحقق من الشرط مجددًا الذي هو True دومًا… المخطط التدفقي في الشكل الآتي يوضح آلية عمل البرنامج swordfish.py: المخطط التدفقي للبرنامج swordfish.py، لاحظ أن المسار × لا ينفذ منطقيًا أبدًا لأن شرط الحلقة هو True دومًا. شغل البرنامج السابق وجرب بعض المدخلات، ولن يسألك البرنامج عن كلمة المرور حتى تدعي أنك Abdullatif، وسيطبع لك رسالة أن الوصول مسموح لك إن أدخلت كلمة المرور الصحيحة: Who are you? I'm fine, thanks. Who are you? Who are you? Abdullatif Hello, Abdullatif. What is the password? (It is a fish.) Mary Who are you? Abdullatif Hello, Abdullatif. What is the password? (It is a fish.) swordfish Access granted. القيم التي تكافئ True والقيم التي تكافئ False ستعدّ الشروط في بايثون بعض القيم في أنواع البيانات المختلفة على أنها مكافئة للقيمة True وأخرى للقيمة False. فلو استخدمنا القيم 0 و 0.0 و '' (سلسلة نصية فارغة) في الشروط فستكافئ False، بينما ستكافئ أي قيمة أخرى True. ألقِ نظرةً هنا: name = '' ➊ while not name: print('Enter your name:') name = input() print('How many guests will you have?') numOfGuests = int(input()) ➋ if numOfGuests: ➌ print('Be sure to have enough room for all your guests.') print('Done') يبدأ البرنامج بتهيئة المتغير name مع قيمة نصية فارغة، وبالتالي سيكون شرط حلقة while محققًا True ➊، وسيتحقق أيضًا إذا أدخل المستخدم سلسلة نصية فارغة أثناء سؤاله عن الاسم name (بالضغط مباشرةً على زر Enter دون كتابة شيء). سيبدأ تنفيذ الحلقة بطلب إدخال الاسم وعدد الضيوف، وإذا كان عدد الضيوف numOfGuests ليس صفرًا 0 ➋ فسيكون الشرط محققًا True وسيطبع البرنامج تذكيرًا للمستخدم ➌. كان بإمكاننا كتابة name != '' بدلًا من not name، و numOfGuests != 0 بدلًا من numOfGuests، لكن استخدام القيم التي تكافئ True أو False في شروط سيجعل مقروئية شيفرتك أفضل. حلقات تكرار for والدالة range() ستعمل حلقة while لطالما كان الشرط محققًا True (ومن هنا أتى اسمها while)، لكن ماذا لو أردنا تنفيذ كتلة من الشيفرات لعدد محدد من المرات؟ يمكننا فعل ذلك عبر حلقة التكرار for والدالة range(). ستبدو عبارة for كالآتي for i in range(5): وستتضمن ما يلي: الكلمة المحجوزة for اسم المتغير الكلمة المحجوزة in استدعاء للدالة range() مع تمرير 3 أعداد صحيحة كحد أقصى إليها نقطتان رأسيتان : كتلة برمجية تلي ما سبق، تكون فيها الأسطر مسبوقة بمسافة بادئة لننشئ برنامجًا ولنسمه fiveTimes.py الذي يساعدنا على معاينة حلقة for عمليًا: print('My name is') for i in range(5): print('Hani Five Times (' + str(i) + ')') سترى أن الكتلة البرمجية لحلقة التكرار for تنفذ 5 مرات، وستكون قيمة المتغير i في أول مرة تعمل فيها الحلقة هو 0، وستطبع print() العبارة Hani Five Times (0) ثم بعد أن تنتهي بايثون من تنفيذ أول دورة في حلقة التكرار فسيعود التنفيذ إلى بداية الحلقة وستزيد قيمة المتغير i بمقدار 1، ولهذا سيؤدي استدعاء الدالة range(5) إلى حدوث 5 تكرارات داخل حلقة for، إذ سيبدأ المتغير i من القيمة 0 ثم 1 ثم 2 ثم 3 ثم 4، وعمومًا ستزداد قيمة المتغير إلى أن تصل إلى الرقم الذي مررناه إلى الدالة range() لكن دون تضمينه في النتائج. الشكل الآتي يوضح المخطط التدفقي لهذا البرنامج: إذا شغلت البرنامج فسترى العبارة Hani Five Times متبوعةً بقيمة المتغير i في دورة حلقة for الحالية: My name is Hani Five Times (0) Hani Five Times (1) Hani Five Times (2) Hani Five Times (3) Hani Five Times (4) ملاحظة: يمكنك استخدام break و continue داخل حلقات for أيضًا، وستؤدي العبارة continue إلى تخطي الدورة الحالية لحلقة التكرار والانتقال إلى الدورة الآتية، كما لو أن تنفيذ البرنامج وصل إلى نهاية دورة حلقة for الحالية ثم عاد إلى بداية الحلقة. يمكنك أن تستعمل العبارتين break و continue داخل حلقات while و for فقط، وإذا حاولت أن تجرب استخدامها في مكانٍ آخر فستعطيك بايثون رسالة خطأ. لنأخذ قصة عالم الرياضيات الشهير كارل فريدريش غاوس (قد تعرفه باسم غاوس أو Gauss)، أراد مدرسه حينما كان صغيرًا أن يعطيه وظيفة صعبة وطلب منه جمع الأرقام من 0 إلى 100، وخطرت ببال الفتى غاوس طريقة ذكية للإجابة عن ذاك السؤال بثوانٍ معدودة، لكن لنكتب الآن برنامج بايثون يحسب لنا الناتج ويستعمل الحلقة for: ➊ total = 0 ➋ for num in range(101): ➌ total = total + num ➍ print(total) يفترض أن يكون الناتج 5,050. نضبط قيمة المتغير total إلى 0 ➊ في بداية البرنامج، ثم نبدأ حلقة for ➋ التي ننفذ فيها total = total + num ➌ مئة مرة، وبعد أن تنتهي دورات التكرار المئة فسنكون قد جمعنا الأعداد الصحيحة من 0 إلى 100 في المتغير total، ثم نطبق قيمة total إلى الشاشة ➍. سيعمل برنامجنا بأجزاء من الثانية ويخبرنا بالناتج النهائي. (اكتشف غاوس حينما أعطاه المدرس هذه الوظيفة حلها بطريقة ذكية: هنالك خمسون زوجًا من الأرقام التي يكون مجموعها 101 مثل 1 + 100 و 2 + 99 و 3 + 98 وهلمَّ جرًا، حتى يصل إلى 50 + 51. ولمّا كان جداء 50 × 101 هو 5,050 فسيكون مجموع جميع الأرقام من 0 إلى 100 هو 5,050. يا له من فتى ذكي!) كتابة حلقة while تكافئ حلقة for يمكنك عمليًا أن تكتب حلقة while لتكافئ في عملها حلقة for، وستجد أن كتابة حلقات for مختصرة أكثر. لنعد كتابة المثال iveTimes.py ليستعمل الحلقة while: print('My name is') i = 0 while i < 5: print('Hani Five Times (' + str(i) + ')') i = i + 1 إذا شغلت البرنامج فمن المفترض أن يكون الناتج مماثلًا تمامًا للمثال fiveTimes.py الذي يستعمل حلقة for. اعلم أنك تستطيع كتابة أمور كثيرة في البرمجة بأكثر من طريقة، لكن عليك اختيار الأداة الأنسب لأداء المهمة التي تريدها. وسائط الدالة range(): البداية والنهاية والخطوة يمكن أن تستدعى بعض الدوال مع عدّة وسائط arguments مفصولة بفاصلة، والدالة range() هي إحداها. يسمح لك تمرير وسائط للدالة range() أن تغير سلوكها، فمثلًا يمكنك تحديد الرقم الذي يجب أن تبدأ منه الدالة range(): for i in range(12, 16): print(i) يمثِّل أول وسيط من أين يجب أن تبدأ حلقة for، ويمثل الوسيط الثاني أين يجب أن تتوقف (دون تضمين هذا الرقم): 12 13 14 15 يمكننا أيضًا استدعاء الدالة range() مع ثلاثة وسائط، ويكون أول وسيطين هما البداية والنهاية، أما الوسيط الثالث فسيكون «الخطوة» step، الذي يشير إلى مقدار زيادة قيمة المتغير عند كل دورة: for i in range(0, 10, 2): print(i) أي أن استدعاء range(0, 10, 2) سيعد من الصفر إلى الثمانية وبخطوة 2: 0 2 4 6 8 الدالة range() مرنة ويمكنك استخدامها لتوليد أي سلسلة أرقام، فمثلًا يمكنك استخدام رقم سالب كوسيط لقيمة الخطوة مما يؤدي إلى العد عكسيًا تنازليًا: for i in range(5, -1, -1): print(i) ستنتج حلقة for السابقة الناتج الآتي: 5 4 3 2 1 0 نجد أن المجال range(5, -1, -1) مع حلقة التكرار for سيؤدي إلى طباعة 5 أعداد تنازليًا من 5 إلى 0. استيراد الوحدات يمكن لجميع برامج بايثون أن تستدعي مجموعةً من الدوال الأساسية نسميها الدوال المضمنة في اللغة أو «الدوال المضمنة» built-in functions، والتي تتضمن الدوال التي تعرفت عليها سابقًا مثل print() و input() و len(). تأتي بايثون أيضًا مع مجموعة من الوحدات الأساسية modules التي نسميها بالمكتبة القياسية standard library. تتألف كل وحدة من عدد برامج بايثون التي فيها مجموعة من الدوال التي يمكنك استخدامها في برامجك. فمثلًا تحتوي الوحدة math على مجموعة من الدوال المتعلقة بالعمليات الرياضية، بينما تضم الوحدة random مجموعة من الدوال التي تجري عمليات على الأرقام المولدة عشوائيًا، وهكذا. قبل أن نستخدم الدوال الموجودة في إحدى تلك الوحدات في برامجنا، يجب علينا أولًا استيراد تلك الوحدة باستخدام العبارة import. وتتألف عبارة import مما يلي: الكلمة المحجوزة import اسم الوحدة التي نريد استيرادها واختياريًا أسماء وحدات أخرى نريد استيرادها على أن نفصل بينها بفاصلة بعد أن تستورد إحدى الوحدات فيمكنك أن تستعمل جميع الدوال الرائعة الموجودة فيها، ولنضرب مثالًا الوحدة random التي تمنحنا وصولًا إلى الدالة random.randint() حين استيرادها. احفظ الشيفرة الآتية في ملف باسم printRandom.py: import random for i in range(5): print(random.randint(1, 10)) سيطبع البرنامج السابق ناتجًا يشبه الناتج الآتي حين تشغيله: 4 1 8 4 1 ستكون نتيجة استدعاء الدالة random.randint() هي عدد صحيح عشوائي يقع بين العددين الذين مررتهما إلى الدالة كوسيطين. ولمّا كانت الدالة randint() موجودةً في الوحدة random، فعليك أن تكتب الكلمة random أولًا قبل اسم الدالة لتخبر بايثون أنك تريد استخدام الدالة الموجودة في الوحدة random، ونفصل بين اسم الوحدة واسم الدالة بنقطة. هذا مثال لعبارة استيراد تستورد أربع وحدات في آنٍ واحد: import random, sys, os, math يمكننا الآن استخدام أي دالة موجودة في الوحدات الأربع السابقة، وسنتعلم المزيد عن تلك الوحدات لاحقًا في هذه السلسلة. عبارة from import هنالك شكل بديل لعبارة import يتضمن الكلمة المحجوزة from، والتي نتبعها باسم الوحدة، ثم الكلمة المحجوزة import ثم نجمة *؛ فمثلًا from random import *. وحين استيراد الوحدات بهذا الشكل فلا حاجة إلى وضع السابقة random. قبل أسماء الدوال التي نريد استدعاءها؛ لكن في المقابل من الأفضل كتابة الاسم الكامل للدالة مع السابقة التي تشير إلى اسم الوحدة لزيادة مقروئية الشيفرة البرمجية. إنهاء تنفيذ البرنامج حينما نشاء باستخدام الدالة sys.exit() آخر مفهوم من مفاهيم بنى التحكم التي سنشرحها في هذا المقال هو آلية إنهاء تنفيذ البرنامج. ينتهي تنفيذ البرامج دومًا حينما يصل التنفيذ إلى نهاية الملف، لكن يمكننا إنهاء تنفيذ البرنامج قبل آخر تعليمة برمجية باستخدام الدالة sys.exit(). ولمّا كانت هذه الدالة جزءًا من الوحدة sys فعلينا أن نستورد تلك الوحدة في بداية البرنامج قبل استخدامها. افتح محرر الشيفرات واكتب ما يلي واحفظه exitExample.py: import sys while True: print('Type exit to exit.') response = input() if response == 'exit': sys.exit() print('You typed ' + response + '.') شغل البرنامج السابق وستجد أنك دخلت في حلقة تكرار لا نهائية دون وجود عبارة break داخلها، والطريقة الوحيدة لكي ينتهي تنفيذ البرنامج هي الوصول إلى استدعاء الدالة sys.exit()، ولأن قيمة المتغير response تساوي ما يدخله المستخدم عبر input()، فإذا أدخلت exit فستتحقق العبارة الشرطية if وسينتهي تنفيذ البرنامج. برنامج قصير: احزر الرقم جميع الأمثلة السابقة كانت بسيطة جدًا لكنها مناسبة لاستيعاب المفاهيم البرمجية الأساسية، لكن لنبني الآن برنامجًا متكاملًا نوظِّف فيه المعلومات التي تعلمناها. سنكتب في هذا القسم لعبة «احزر الرقم»، والتي ستبدو كما يلي حين تشغيلها: I am thinking of a number between 1 and 20. Take a guess. 10 Your guess is too low. Take a guess. 15 Your guess is too low. Take a guess. 17 Your guess is too high. Take a guess. 16 Good job! You guessed my number in 4 guesses! افتح محرر الشيفرات وأدخل الشيفرة الآتية واحفظها في ملف باسم guessTheNumber.py: # This is a guess the number game. import random secretNumber = random.randint(1, 20) print('I am thinking of a number between 1 and 20.') # Ask the player to guess 6 times. for guessesTaken in range(1, 7): print('Take a guess.') guess = int(input()) if guess < secretNumber: print('Your guess is too low.') elif guess > secretNumber: print('Your guess is too high.') else: break # This condition is the correct guess! if guess == secretNumber: print('Good job! You guessed my number in ' + str(guessesTaken) + ' guesses!') else: print('Nope. The number I was thinking of was ' + str(secretNumber)) لنقسم البرنامج إلى أقسام صغيرة ونناقشها كلًا على حدة، بدءًا من أعلى الملف: # This is a guess the number game. import random secretNumber = random.randint(1, 20) يبدأ البرنامج بتعليق في أول سطر فيه يشرح ماذا يفعل البرنامج، ثم يستورد الوحدة random لكي نستطيع استخدام الدالة random.randint() لتوليد رقم ليحزره اللاعب، وسنخزن الرقم العشوائي الناتج في المتغير secretNumber. print('I am thinking of a number between 1 and 20.') # Ask the player to guess 6 times. for guessesTaken in range(1, 7): print('Take a guess.') guess = int(input()) سيخبر البرنامج اللاعب أنه قد «فكر» في رقم سري ويملك اللاعب ست محاولات ليحزره، وستكون الشيفرة التي تسمح للاعب بإدخال رقم والتحقق منه موجودة في حلقة for التي ستنفذ ست مرات كحد أقصى. أول ما تفعله حلقة التكرار هو طلب كتابة تخمين للرقم السري عبر input()، ولأن الدالة input() تعيد سلسلةً نصية وليس رقمًا صحيحًا فنمرر الناتج إلى الدالة int()، وثم يخزن الرقم الذي أدخله المستخدم في المتغير guess. if guess < secretNumber: print('Your guess is too low.') elif guess > secretNumber: print('Your guess is too high.') تتحقق الشروط السابقة إن كان تخمين المستخدم أصغر أو أكبر من الرقم السري، وفي كلتي الحالتين سيوفر البرنامج تلميحًا لللاعب. else: break # This condition is the correct guess! إذا لم يكن تخمين اللاعب أصغر ولا أكبر من الرقم السري فهذا يعني أنه يساويه، وفي هذه الحالة سيخرج البرنامج من حلقة for عبر break. if guess == secretNumber: print('Good job! You guessed my number in ' + str(guessesTaken) + ' guesses!') else: print('Nope. The number I was thinking of was ' + str(secretNumber)) بعد نهاية حلقة for سيتحقق البرنامج عبر عبارة if…else أن اللاعب قد خمَّن الرقم السري الصحيح، ويعرض رسالة مناسبة لكل حالة. تذكر أن الشيفرات التي تقع بعد حلقة التكرار ستنفذ بعد انتهاء تنفيذ حلقة التكرار سواءً بإكمالها 6 مرات وعدم تخمين اللاعب للرقم الصحيح، أو بالخروج منها عبر break حين تخمين الرقم السري الصحيح. سيعرض برنامجنا رسالتين نصيتين فيهما متغير يحمل قيمةً عدديةً صحيحةً guessesTaken و secretNumber، ولأننا نحاول ضم عدد صحيح إلى سلسلة نصية فيجب أن نمرر الرقم إلى الدالة str() أولًا، ثم نضم السلاسل النصية عبر العامل + لتمريرها إلى الدالة print() لطباعتها على الشاشة. برنامج قصير: حجرة ورقة مقص لنستخدم المفاهيم البرمجية التي تعلمناها لإنشاء لعبة «حجرة ورقة مقص» الشهيرة. سيكون ناتج تشغيل اللعبة كما يلي: ROCK, PAPER, SCISSORS 0 Wins, 0 Losses, 0 Ties Enter your move: (r)ock (p)aper (s)cissors or (q)uit p PAPER versus... PAPER It is a tie! 0 Wins, 1 Losses, 1 Ties Enter your move: (r)ock (p)aper (s)cissors or (q)uit s SCISSORS versus... PAPER You win! 1 Wins, 1 Losses, 1 Ties Enter your move: (r)ock (p)aper (s)cissors or (q)uit q افتح نافذةً جديدةً في محرر الشيفرات وأدخل الشيفرة الآتية واحفظها في ملف باسم rpsGame.py: import random, sys print('ROCK, PAPER, SCISSORS') ستتبّع هذه المتغيرات عدد مرات الربح والخسارة والتعادل wins = 0 losses = 0 ties = 0 while True: # دورة اللعبة print('%s Wins, %s Losses, %s Ties' % (wins, losses, ties)) while True: # مدخلات المستخدم print('Enter your move: (r)ock (p)aper (s)cissors or (q)uit') playerMove = input() if playerMove == 'q': sys.exit() # الخروج من البرنامج if playerMove == 'r' or playerMove == 'p' or playerMove == 's': break # الخروج من حلقة مدخلات المستخدم print('Type one of r, p, s, or q.') # عرض ما اختاره اللاعب if playerMove == 'r': print('ROCK versus...') elif playerMove == 'p': print('PAPER versus...') elif playerMove == 's': print('SCISSORS versus...') # عرض ما اختاره الحاسوب randomNumber = random.randint(1, 3) if randomNumber == 1: computerMove = 'r' print('ROCK') elif randomNumber == 2: computerMove = 'p' print('PAPER') elif randomNumber == 3: computerMove = 's' print('SCISSORS') # عرض النتيجة الربح/الخسارة/التعادل if playerMove == computerMove: print('It is a tie!') ties = ties + 1 elif playerMove == 'r' and computerMove == 's': print('You win!') wins = wins + 1 elif playerMove == 'p' and computerMove == 'r': print('You win!') wins = wins + 1 elif playerMove == 's' and computerMove == 'p': print('You win!') wins = wins + 1 elif playerMove == 'r' and computerMove == 'p': print('You lose!') losses = losses + 1 elif playerMove == 'p' and computerMove == 's': print('You lose!') losses = losses + 1 elif playerMove == 's' and computerMove == 'r': print('You lose!') losses = losses + 1 لنمعن النظر إلى الشيفرة من بدايتها: import random, sys print('ROCK, PAPER, SCISSORS') # ستتبّع هذه المتغيرات عدد مرات الربح والخسارة والتعادل wins = 0 losses = 0 ties = 0 سنستورد في البداية الوحدتين random و sys لكي نستطيع استدعاء الدالتين random.randint() و sys.exit()، ثم سنهيئ ثلاثة متغيرات لكي نتتبع عدد مرات ربح أو خسارة أو تعادل اللاعب. while True: # دورة اللعبة print('%s Wins, %s Losses, %s Ties' % (wins, losses, ties)) while True: # مدخلات المستخدم print('Enter your move: (r)ock (p)aper (s)cissors or (q)uit') playerMove = input() if playerMove == 'q': sys.exit() # الخروج من البرنامج if playerMove == 'r' or playerMove == 'p' or playerMove == 's': break # الخروج من حلقة مدخلات المستخدم print('Type one of r, p, s, or q.') يستعمل برنامجنا حلقة while داخل حلقة while، أول حلقة هي حلقة اللعبة الأساسية، وتمثل دورًا من لعبة «حجرة ورقة مقص» في كل مرة تنفذ فيها تلك الحلقة. أما حلقة التكرار الثانية فهي تطلب من اللاعب مدخلات، وستبقى تعمل حتى يدخل المستخدم r أو p أو s أو q التي ترمز إلى حجرة rock وورقة paper ومقص scissors على التوالي وبالترتيب، أما q فتعني أن اللاعب يريد إنهاء اللعبة quit، وفي تلك الحالة ستستدعى الدالة sys.exit() وسنتهي تنفيذ البرنامج. إذا أدخل المستخدم r أو p أو s فسنخرج من حلقة التكرار الثانية عبر break، وإلا فسيعود التنفيذ إلى بداية حلقة التكرار ويذكر البرنامج اللاعب أن عليه إدخال r أو p أو s أو q. # عرض ما اختاره اللاعب if playerMove == 'r': print('ROCK versus...') elif playerMove == 'p': print('PAPER versus...') elif playerMove == 's': print('SCISSORS versus...') يظهر البرنامج هنا ما اختاره اللاعب. # عرض ما اختاره الحاسوب randomNumber = random.randint(1, 3) if randomNumber == 1: computerMove = 'r' print('ROCK') elif randomNumber == 2: computerMove = 'p' print('PAPER') elif randomNumber == 3: computerMove = 's' print('SCISSORS') وهنا يظهر ما اختاره الحاسوب عشوائيًا. ولمّا كانت الدالة random.randint() تعيد رقمًا عشوائيًا، فسنحتاج إلى تحويل الرقم الصحيح المخزن في المتغير randomNumber إلى حجرة أو ورقة أو مقص عبر البنية الشرطية if و elif، وبالتالي سيخزن البرنامج ما اختاره الحاسوب في المتغير computerMove ثم يطبع رسالة نصية فيها الحركة المختارة. # عرض النتيجة الربح/الخسارة/التعادل if playerMove == computerMove: print('It is a tie!') ties = ties + 1 elif playerMove == 'r' and computerMove == 's': print('You win!') wins = wins + 1 elif playerMove == 'p' and computerMove == 'r': print('You win!') wins = wins + 1 elif playerMove == 's' and computerMove == 'p': print('You win!') wins = wins + 1 elif playerMove == 'r' and computerMove == 'p': print('You lose!') losses = losses + 1 elif playerMove == 'p' and computerMove == 's': print('You lose!') losses = losses + 1 elif playerMove == 's' and computerMove == 'r': print('You lose!') losses = losses + 1 وفي النهاية، سيوازن البرنامج بين السلسلتين النصيتين الموجودتين في المتغيرين playerMove و computerMove ويظهر الناتج على الشاشة، وسيزيد قيمة أحد المتغيرات wins أو losses أو ties بما يناسب الحالة. بعد أن يصل تنفيذ البرنامج إلى النهاية فسيعود إلى بداية حلقة التكرار الرئيسية ونلعب دورًا جديدًا من اللعبة. الخلاصة بعد أن تعلمنا كيفية كتابة شروط أو عبارات تكون نتيجتها True أو False، أصبح بإمكاننا كتابة برامج تستطيع اتخاذ قرارات تحدد ما هي الشيفرات التي سينفذها البرنامج وأيها سيتخطاها. أصبح تستطيع تنفيذ الشيفرات مرارًا وتكرارًا عبر حلقات التكرار، وستستفيد من العبارتين break و continue للخروج من حلقة التكرار أو العودة إلى بدايتها مباشرةً. تسمح لنا بنى التحكم بكتابة برامج أكثر ذكاءً، وسنتعلم كتابة شيفرات أفضل عبر نوع جديد من بنى التحكم الذي سنتعرف عليه في مقال قادم، ألا وهو الدوال functions. والآن هل تستطيع تجربة الآتي؟ كتابة شيفرة تطبع Hello إذا كانت القيمة 1 مخزنة في المتغير spam، و World إذا كانت القيمة 2 في المتغير spam، و Greetings فيما عدا ذلك. كتابة برنامج قصير يطبع الأرقام من 1 إلى 10 باستخدام حلقة for. ثم اكتب برنامجًا مكافئًا له يطبع الأرقام من 1 إلى 10 باستخدام حلقة while. ترجمة -وبتصرف- للفصل Flow Control من كتاب Automate the boring stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: أساسيات لغة بايثون Python مصطلحات بايثون البرمجية أنواع البيانات والعمليات الأساسية في لغة بايثون تعلم لغة بايثون النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون
-
تمتلك لغة بايثون طيفًا واسعًا من البنى البرمجية، والدوال القياسية، وميزات رائعة لبيئات التطوير التفاعلية؛ لكن تجاهل أغلب ما ذكرته آنفًا وابدأ بتعلم ما تحتاج إليه لكتابة برامجك. لكنك ستحتاج إلى تعلم المفاهيم الأساسية للبرمجة قبل أن تفعل أي شيء، وقد تظن أن هذه المفاهيم تافهة أو مملة لكنها أساسية لتتحكم بحاسوبك كما تشاء. في هذا المقال أمثلة عديدة أنصحك أن تكتبها في الصدفة التفاعلية التي تسمى أيضًا REPL (اختصار للعبارة Read-Evaluate-Print Loop أي حلقة قراءة-تقدير القيمة-طباعة)، التي تسمح لك بتنفيذ تعليمات بايثون كل تعليمة على حدة مباشرةً وتظهر لك الناتج. الصدفة التفاعلية رائعة لتتعلم التعليمات الأساسية في بايثون، لذا أؤكد عليك أن تجربها أثناء قراءتك لهذا المقال وبالتالي ستتذكر المفاهيم المشروحة فيه تذكرًا أفضل لأنك تراها عمليًا أمامك بدل قراءتها فحسب. إدخال التعابير البرمجية في الصدفة التفاعلية يمكنك تشغيل الصدفة التفاعلية بفتح محرر Mu، الذي يفترض أنك ثبتته وفق التعليمات في المقال السابق، افتح قائمة ابدأ في ويندوز أو مجلد التطبيقات في ماك وشغِّل محرر Mu، ثم اضغط على زر New ثم احفظ الملف الفارغ باسم مثل blank.py، وحينا تحاول تشغيل الملف باستخدام الزر Run أو الضغط على زر F5 في لوحة مفاتيحك فستفتح لك الصدفة التفاعلية في الجزء السفلي من نافذة المحرر، وسترى المحث <<< أمامك. أدخل 2 + 2 في سطر الأوامر لكي تجري عملية جمع بسيطة، يجب أن تبدو الطرفية التفاعلية كما يلي: >>> 2 + 2 4 >>> نسمي 2 + 2 في بايثون بالتعبير البرمجي expression، وهي أبسط أنواع التعليمات البرمجية في اللغة، وتتألف التعابير البرمجية عادةً من قيم (مثل 2) وعوامل (operators مثل +)، وتقدر قيمتها evaluate إلى قيمة واحدة؛ وهذا يعني أنك تستطيع استخدام التعابير في أي مكان في شيفرات بايثون تستطيع فيه استخدام قيمة ما. تقدر قيمة التعبير 2 + 2 في المثال السابق إلى قيمة واحدة هي 4، واعلم أن قيمةً واحدةً لا عامل فيها مثل 4 تُعَد تعبيرًا برمجيًا في بايثون أيضًا، كما هو واضح هنا: >>> 2 2 لا ضير من الأخطاء سيفشل تشغيل البرامج التي تحتوي على شيفرات لا يفهمها الحاسوب، مما يؤدي إلى إنهيار البرنامج وظهور رسالة خطأ، ورسائل الخطأ لا تسبب مشاكل في حاسوبك، لذا لا تخف من ارتكاب الأخطاء في الشيفرات البرمجية حين تجربتك، وانهيار البرنامج crash يعني أن البرنامج توقف عن التنفيذ بشكل غير متوقع. إذا أردت أن تتعرف على المزيد من المعلومات حول الخطأ الذي ظهر لك، فعليك البحث في الإنترنت عنه، أو العودة إلى توثيق لغة بايثون. يمكنك استخدام عوامل عديدة في تعابير بايثون، فالجدول الآتي يستعرض جميع العوامل الحسابية في بايثون. العامل العملية مثال الناتج ** القوة (أو الأس) 2 ** 3 8 % باقي القسمة 22 % 8 6 // عامل قسمة الأعداد الصحيحة 22 // 8 2 / القسمة 22 / 8 2.75 * الضرب 3 * 5 15 - الطرح 5 - 2 3 + الجمع 2 + 2 4 جدول العوامل الرياضية من أعلاها إلى أدناها أولوية وأسبقية ترتيب أولوية العمليات في بايثون (تسمى أيضًا «أسبقية») مشابهة لأولويتها في الرياضيات، فتقدر قيمة المعامل ** أولًا، ثم تأتي المعاملات * و / و // و % بالترتيب حسب التعبير من اليسار إلى اليمين، ثم يأتي المعاملان + و - بالترتيب أيضًا من اليسار إلى اليمين. يمكنك استخدام الأقواس () لتغيير الترتيب إن احتجت إلى ذلك. لا تلعب المسافات الفارغة أي معنى بين العوامل في بايثون (عدا المسافة البادئة في أول السطر) لكن من المتعارف عليه استخدام فراغ واحد بينها. أدخل التعابير الآتية في الصدفة التفاعلية: >>> 2 + 3 * 6 20 >>> (2 + 3) * 6 30 >>> 48565878 * 578453 28093077826734 >>> 2 ** 8 256 >>> 23 / 7 3.2857142857142856 >>> 23 // 7 3 >>> 23 % 7 2 >>> 2 + 2 4 >>> (5 - 1) * ((7 + 1) / (3 - 1)) 16.0 كل ما عليك فعله هو كتابة التعبير البرمجي، وستتولى بايثون العمليات الحسابية وتعيد لك قيمةً واحدةً هي الناتج، كما هو موضح في الرسم الآتي: اعلم أن قواعد وضع العوامل والقيم مع بعضها بعضًا لتشكيل التعابير البرمجية من أساس لغة بايثون، ومثَلها كمثل القواعد النحوية التي تساعدنا في التواصل: هذه الجملة صحيحة قاعديًا في اللغة العربية. جملة صحيحة في اللغة العربية قاعديًا. لاحظ أن الجملة الثانية صعبة الفهم لأنها لا تتبع القواعد الأساسية لبنية الجملة العربية، وبالمثل إذا أدخلت تعليمة بايثون غير صحيحة فلن تتمكن بايثون من فهمها وسيظهر خطأ SyntaxError كما هو ظاهر هنا: >>> 5 + File "<stdin>", line 1 5 + ^ SyntaxError: invalid syntax >>> 42 + 5 + * 2 File "<stdin>", line 1 42 + 5 + * 2 ^ SyntaxError: invalid syntax يمكنك دومًا اختبار صحة تعليمة ما بإدخالها في الصدقة التفاعلية، وأؤكد لك أنها لن تسبب ضررًا بحاسوبك، فأسوأ ما يمكن هو ظهور رسالة خطأ. صدقني حينما أخبرك أن المطورين المحترفين يرون رسائل الخطأ في كل يوم. أنواع البيانات العددية والعشرية والنصية أذكرك أن التعابير هي قيم تجمعها المعاملات، وتكون نتيجتها هي قيمة واحدة دومًا. نوع البيانات data type هو تصنيف للقيم، فكل قيمة يكون لها نوع بيانات واحد فقط، وسنذكر في الجدول الآتي أكثر أنواع البيانات شيوعًا. القيمة -2 و 30 هي أعداد صحيحة integer أو اختصارًا int، والتي تشير إلى أنها أعداد كاملة دون فواصل ويمكن أن تكون موجبة أو سالبة؛ أما الأرقام مع فاصلة عشرية مثل 3.14 فهي تسمى «الأرقام ذات الفاصلة العائمة» floating-point numbers أو اختصارًا floats. انتبه إلى أن القيمة 42 هي عدد صحيح int، بينما 42.0 هي قيمة ذات فاصلة عائمة float. نوع البيانات أمثلة أرقام صحيحة -2, -1, 0, 1, 2, 3, 4, 5 أرقام ذات فاصلة عائمة -1.25, -1.0, -0.5, 0.0, 0.5, 1.0, 1.25 سلاسل نصية 'a', 'aa', 'aaa', 'Hello!', '11 cats' جدول أنواع البيانات الشائعة يمكن أن تحتوي برامج بايثون على نصوص أيضًا، وتسمى بالسلاسل النصية strings أو اختصارًا strs. احرص على إحاطة السلاسل النصية التي تضعها في برامج بعلامة اقتباس مفردة ' كما في 'مرحبًا' أو 'مع السلامة' لكي تعرف بايثون أين تبدأ السلسلة النصية وأين تنتهي. يمكنك أيضًا إنشاء سلاسل نصية فارغة بكتابة ''، سنتعلم السلاسل النصية تفصيليًا في مقال لاحق. إذا رأيت رسالة الخطأ SyntaxError: EOL while scanning string literal فمن المرجح أنك نسيت علامة الاقتباس المفردة في نهاية السلسلة النصية، كما في المثال الآتي: >>> 'Hello, world! SyntaxError: EOL while scanning string literal ضم السلاسل النصية وتكرارها قد يتغير معنى العوامل اعتمادًا على أنواع بيانات القيم التي تحيط بها، فمثلًا العامل + هو عامل الجمع حينما يحاط بقيمتين عدديتين سواءً كانتا أعدادًا صحيحةً أو ذات فاصلة عائمة؛ لكن حين استخدام العامل + بين قيمتين نصيتين فهو يلمهما على بعضهما ويضمهما ويسمى عامل string concatenation. أدخل ما يلي إلى الصدفة التفاعلية: >>> 'Hello' + 'World' 'HelloWorld' ينتج من التعبير السابق قيمة واحدة وهي سلسلة نصية تجمع النص الموجود في السلسلتين النصيتين المدخلتين؛ لكنك إذا جربت استخدام العامل + على سلسلة نصية ورقم صحيح فلن تعرف بايثون ماذا عليها أن تفعل هنا، وستظهر لك رسالة خطأ: >>> 'Hello' + 42 Traceback (most recent call last): File "<pyshell#0>", line 1, in <module> 'Hello' + 42 TypeError: can only concatenate str (not "int") to str رسالة الخطأ can only concatenate str (not "int") to str تعني أن بايثون تظن أنك تحاول ضم عدد صحيح 42 إلى سلسلة نصية 'Hello'، فإذا كنت تريد فعل ذلك فعليك أن تحول نوع البيانات العددي إلى نصي يدويًا لأن بايثون لن تفعل ذلك تلقائيًا بالنيابة عنك. سنتعلم تحويل أنواع البيانات في قسم «فهم مكونات تطبيقك الأول» حينما نتحدث عن الدوال str() و int() و float(). العامل * يضبط عددين صحيحين أو ذوي فاصلة عائلة مع بعضها بعضًا، لكن حين استخدامه مع سلسلة نصية وعدد صحيح فإنه يكرهها ويسمى عامل string replication. لترى كيف يعمل، أدخل سلسلةً نصية متبوعةً بعامل * ثم رقم في الصدفة الفاعلية: >>> 'Hello' * 5 'HelloHelloHelloHelloHello' نتيجة التعبير السابق هي قيمة نصية واحدة تُكرَّر فيها السلسلة النصية الأصلية عددًا محددًا من المرات قيمته هي قيمة العدد الصحيح. يفيدنا عامل تكرار السلاسل النصية في بعض الأحيان لكن فائدته لا تقارن بفائدة عامل الضم الذي نستعمله كثيرًا. يمكن أن يستعمل عامل * مع قيمتين عدديتين لإجراء عملية ضرب، أو مع سلسلة نصية واحدة وعدد صحيح واحد لإجراء عملية تكرار؛ وإلا فستظهر لنا بايثون رسالة خطأ كما يلي: >>> 'Hello' * 'World' Traceback (most recent call last): File "<pyshell#32>", line 1, in <module> 'Hello' * 'World' TypeError: can't multiply sequence by non-int of type 'str' >>> 'Hello' * 5.0 Traceback (most recent call last): File "<pyshell#33>", line 1, in <module> 'Hello' * 5.0 TypeError: can't multiply sequence by non-int of type 'float' من المنطقي ألا تسمح لنا بايثون بهذه التعابير، إذ لا يمكنك ضرب كلمتين ببعضهما، ومن الصعب أن تستعمل عددًا عشريًا لتكرار عبارة نصية. تخزين القيم في متغيرات المتغير variable هو ما يشبه الصندوق في ذاكرة الحاسوب الذي يسمح لك بتخزين قيمة واحدة فيه، فإذا أردت تخزين قيمة ناتج أحد التعبيرات البرمجية في برنامجك لاستعمالها لاحقًا فخزنها في متغير. عبارات الإسناد ستستخدم عبارة إسناد assignment statement لتخزين القيم في متغيرات، وتتألف عبارة الإسناد من اسم المتغير وعلامة المساواة (وتسمى أيضًا عامل الإسناد) والقيمة التي نرغب بتخزينها. إذا كتبت مثلًا عبارة الإسناد spam = 42 فهذا يعني أن المتغير الذي اسمه spam سيخزِّن القيمة 42 داخله. تخيل أن المتغير هو صندوق له لافتة أو اسم، يمكنك أن تضع القيم داخله كما في الشكل الآتي: العبارة spam = 42 تخبر البرنامج أن «المتغير spam يحتوي الآن على القيم العددية 42 داخله». على سبيل المثال، أدخِل ما يلي في الصدفة التفاعلية: ➊ >>> spam = 40 >>> spam 40 >>> eggs = 2 ➋ >>> spam + eggs 42 >>> spam + eggs + spam 82 ➌ >>> spam = spam + 2 >>> spam 42 سيُهيِّئ المتغير intialize أو يُنشَأ في أول مرة تخزن فيه قيمة ➊، وبعد ذلك يمكنك استخدامه في التعابير البرمجية مع غيره من المتغيرات والقيم ➋، وحين إسناد قيمة جديدة إلى المتغير ➌ فستنسى القيمة القديمة، ولذها السب كان ناتج قيمة المتغير spam في نهاية البرنامج هو 42 بدلًا من 40، وهذا ما يسمى بإعادة كتابة قيمة المتغير overwrite. أدخل الشيفرة الآتية في الصدقة التفاعلية لتجربة إعادة الكتابة فوق سلسلة نصية: >>> spam = 'Hello' >>> spam 'Hello' >>> spam = 'Goodbye' >>> spam 'Goodbye' وكما في الصندوق في الشكل الآتي، فإن الصندوق spam يخزن القيمة 'Hello' إلى أن تبدلها إلى 'Goodbye'. تنسى القيمة القديمة حين إسناد قيمة جديدة إلى المتغير. أسماء المتغيرات يصف الاسم الجيد للمتغير البيانات التي يحتويها، فتخيل أنك انتقلت إلى منزل جديد ووضعت لافتات على صناديقك وكتبت عليها «أشياء»، وبهذا لن تعرف ما يحتويه الصندوق إلى أن تنظر داخله. وستجد أن أغلبية أمثلة هذه السلسلة (وتوثيق لغة بايثون) تستعمل أسماء عامة للمتغيرات مثل spam و eggs و olive، لكن احرص على استخدام أسماء واضحة ودالة على محتويات المتغير في برامج لتزيد من مقروئية شيفرتك. وصحيحٌ أنك تستطيع تسمية متغيراتك بأي اسم، لكن هنالك بعض القيود التي تفرضها لغة بايثون، فالجدول الآتي يحتوي على أمثلة عن أسماء المتغيرات. يمكنك أن تسمي متغيراتك بأي اسم طالما التزمت بالشروط الثلاثة الآتية: أن يكون كلمةً واحدةً دون فراغات. أن يستعمل الأرقام والأحرف والشرطة السفلية _ فقط. ألا يبدأ برقم. أسماء متغيرات صالحة أسماء متغيرات غير صالحة current_balance current-balance لا يسمح باستخدام الشرطات currentBalance current balance لا يسمح باستخدام الفراغات account4 4account لا يمكن أن يبدأ برقم TOTAL_SUM TOTAL_$UM لا يسمح باستخدام المحارف الخاصة مثل $ hello hello' لا يسمح باستخدام محارف خاصة مثل ' جدول أسماء صالحة وغير صالحة للمتغيرات أسماء المتغيرات حساسة لحالة الأحرف، وهذا يعني أن spam و SPAM و Spam و sPaM هي أربعة متغيرات مختلفة، وصحيحٌ أن الاسم Spam صالح لتسمية المتغيرات في بايثون، لكن من المتعارف عليه أن نبدأ متغيراتنا بأحرف صغيرة. سنستخدم طريقة التسمية «سنام الجمل» camel case في أمثلة هذه السلسلة بدلًا من الشرطات السفلية، أي أن المتغيرات ستكون lookLikeThis بدلًا من looklikethis. قد يشير المبرمجون الخبراء إلى أن الدليل الرسمي لتنسيق شيفرات بايثون المسمى PEP 8 يقول أن علينا استخدام الشرطات السفلية، لكنني شخصيًا أفضل كتابة المتغيرات بطريقة سنام الجمل، وأقتبس من فقرة «A Foolish Consistency Is the Hobgoblin of Little Minds» في دليل PEP 8 نفسه: برنامجك الأول صحيحٌ أن الصدفة التفاعلية جيدة لتكتب تعليمات بايثون كل واحدةً على حدة، لكن لكتابة برامج كاملة متكاملة فعليك أن تكتب التعليمات البرمجية في محرر شيفرات. محررات الشيفرات تشبه كثيرًا محررات النصوص العادية مثل المفكرة أو TextMate لكنها تحتوي على ميزات مخصصة لتسهيل كتابة الشيفرات البرمجية. ولفتح نافذة تحرير ملف جديد في محرر Mu، اضغط على زر New في الصف العلوي من النافذة. يفترض أن تظهر نافذة فيها مؤشر كتابة ينتظر منك المدخلات، لكن هذه النافذة تختلف عن الصدفة التفاعلية التي كانت تشغل تعليمات بايثون أولًا بأول حين الضغط على زر Enter. إذ يسمح لك محرر الشيفرات بإدخال تعليمات برمجية متعددة ثم حفظ الملف وتشغيل البرنامج. يمكنك معرفة الفرق بينهما بسهولة، إذ تحتوي نافذة الصدفة التفاعلية على المحث <<< فيها، بينما لا يحتويه محرر الشيفرات. حان الوقت لكتابة أول برنامج لك! حينما تظهر لك نافذة المحرر أدخل فيها الأسطر الآتية: ➊ # يرحب البرنامج بالمستخدم ويسأله عن عمره ➋ print('Hello, world!') print('What is your name?') # اسأل عن الاسم ➌ myName = input() ➍ print('It is good to meet you, ' + myName) ➎ print('The length of your name is:') print(len(myName)) ➏ print('What is your age?') # اسأل عن العمر myAge = input() print('You will be ' + str(int(myAge) + 1) + ' in a year.') بعد أن تدخل الشيفرة المصدرية السابقة فعليك حفظها لكي لا تكتبها كل مرة تشغل فيها محرر Mu. اضغط على زر Save واكتب اسم الملف hello.py ثم اضغط على Save لحفظه. أنصحك أن تحفظ برامجك التي تكتبها بين الحين والآخر أثناء كتابتها وتطويرها، فلو حدث خلل في حاسوبك أو أغلقت محرر الشيفرات خطأً فلن تخسر ما كتبته، ويمكنك استعمال اختصار الحفظ الذي هو Ctrl+S في ويندوز ولينكس و ⌘+S في نظام ماك. بعد أن تحفظ الملف يمكنك تشغيل برنامجك. اضغط على زر F5 في لوحة المفاتيح ويجب أن يبدأ تشغيل البرنامج في الصدفة التفاعلية. تذكر أنك عليك الضغط على زر F5 من نافذة المحرر وليس من نافذة الصدفة التفاعلية. أدخل اسمك حينما يسألك البرنامج عليه، وسيبدو ناتج تنفيذ البرنامج كما يلي: Hello, world! What is your name? Abdullatif It is good to meet you, Abdullatif The length of your name is: 10 What is your age? 5 You will be 6 in a year. >>> عندما لا يبقى أي شيفرات لتنفَّذ فسينتهي برنامج بايثون terminate، أي أنه يتوقف عن العمل، ويمكننا القول بتعبير تقني أنه يخرج exit. يمكنك إغلاق محرر النصوص بالضغط على زر X في أعلى النافذة، ولإعادة فتح برنامج سابق اضغط على File ثم Open من القائمة العلوية، وستظهر نافذة اختيار الملفات التي ستختار منها الملف hello.py وتضغط على زر Open؛ يفترض أن ترى أمامك الملف hello.py الذي كتبته وحفظته سابقًا. فهم مكونات تطبيقك الأول لنأخذ جولةً سريعة على تعليمات بايثون بعد فتحك لتطبيقك الأول في محرر الشيفرات، وذلك بالنظر إلى ما يفعله كل سطر من الشيفرة. التعليقات يسمى السطر الآتي تعليقًا comment: ➊ # يرحب البرنامج بالمستخدم ويسأله عن عمره تتجاهل لغة بايثون التعليقات، ويمكنك استخدامها لكتابة ملاحظات أو لتذكير نفسك ما الذي تحاول شيفرتك فعله. تكون أي نصوص مذكورة بعد علامة المربع # جزءًا من التعليق. يضع المطورون في بعض الأحيان رمز # في بداية أحد الأسطر البرمجية لتعطيله مؤقتًا أثناء تجربة البرنامج، وهذا يسمى بتعليق الشيفرة commenting out، ويمكن أن تستفيد من هذا حينما تحاول معرفة لم لا يعمل برنامجك، ثم تزيل رمز # عندما تريد إعادة تفعيل السطر البرمجي. تتجاهل بايثون أيضًا السطر الفارغ بعد التعليق، ويمكنك إضافة الأسطر الفارغة إلى برنامجك كيفما تشاء، وهذا يسهِّل قراءة برنامجك كما لو كنت تكتب فقرات في كتاب. الدالة print() تظهر الدالة print() قيمة السلسلة النصية الموجودة بين قوسين على الشاشة: ➋ print('Hello, world!') print('What is your name?') # اسأل عن الاسم السطر print('Hello, world!') يعني «اطبع النص الموجود في السلسلة النصية 'Hello, world!'، فحين تنفيذ بايثون لهذا السطر فأنت تطلب منها أن تستدعي الدالة print() وأن تمرِّر pass قيمة السلسلة النصية إلى تلك الدالة. القيمة التي تمرر إلى استدعاء دالة function call تسمى بالوسيط argument. لاحظ أن علامات الاقتباس لا تظهر على الشاشة، فهي إشارة متى تبدأ وتنتهي السلسلة النصية، وليست جزءًا من النص. الدالة input() تنتظر الدالة input() أن يدخل المستخدم نصًا عبر لوحة المفاتيح ثم يضغط على زر Enter: ➌ myName = input() نتيجة استدعاء هذه الدالة هي سلسلة نصية تطابق ما أدخله المستخدم، ثم ستُسند السلسلة النصية الناتجة عن هذا الاستدعاء إلى المتغير myName. يمكنك أن تعدّ عملية استدعاء الدالة input() على أنها تعبير برمجي نتيجته هي قيمة السلسلة النصية التي أدخلها المستخدم، فإذا أدخل المستخدم 'Ahmed' فيمكنك أن تقول أن التعبير البرمجي أصبح أشبه بالتعبير myName = 'Ahmed'. إذا استدعيت الدالة input() وظهرت لك رسالة خطأ مثل NameError: name 'Ahmed' is not defined، فهذا يعني أنك تشغل الشيفرة عبر الإصدار الثاني من بايثون وليس الثالث. طباعة اسم المستخدم يحتوي الاستدعاء التالي للدالة print() على التعبير 'It is good to meet you, ' + myName بين القوسين: ➍ print('It is good to meet you, ' + myName) تذكر أن بايثون تقدر قيمة التعابير البرمجية وتنتهي بقيمة واحدة. فإذا احتوى المتغير myName على القيمة 'Ahmed' في السطر ➌، فستقدر قيمة التعبير السابق إلى 'It is good to meet you, Ahmed'، ثم ستمرر هذه السلسلة النصية إلى الدالة print() التي تطبعها على الشاشة. الدالة len() يمكنك أن تمرر سلسلةً نصيةً إلى الدالة len() أو متغيرًا يحتوي على سلسلةٍ نصية، وستنتج الدالة عددًا صحيحًا يمثل عدد المحارف في السلسلة النصية: ➎ print('The length of your name is:') print(len(myName)) أجرب إدخال ما يلي إلى الصدفة التفاعلية: >>> len('hello') 5 >>> len('I like to drink good coffee') 27 >>> len('') 0 وكما في الأمثلة السابقة، ستكون نتيجة len(myName) هي رقم صحيح، ثم ستمرر إلى الدالة print() لطباعتها. تذكر أن الدالة print() تسمح لك بطباعة أعداد أو سلاسل نصية، لكن لاحظ رسالة الخطأ التي تظهر حينما تحاول كتابة ما يلي في الصدفة التفاعلية: >>> print('I am ' + 29 + ' years old.') Traceback (most recent call last): File "<pyshell#6>", line 1, in <module> print('I am ' + 29 + ' years old.') TypeError: can only concatenate str (not "int") to str لا يأتي الخطأ من دالة print() بل من التعبير الذي حاولت تمريره إلى الدالة، وستحصل على رسالة الخطأ نفسها إذا كتبت التعبير البرمجي بمفرده في الصدفة التفاعلية: >>> 'I am ' + 29 + ' years old.' Traceback (most recent call last): File "<pyshell#7>", line 1, in <module> 'I am ' + 29 + ' years old.' TypeError: can only concatenate str (not "int") to str تعطي بايثون خطأً لأن العامل + يمكن أن يستعمل لجمع رقمين مع بعضهما أو ضم سلسلتين نصيتين، لكنه لا يستطيع إضافة عدد إلى سلسلة نصية لأن ذلك ليس مسموحًا به قاعديًا في بايثون. ويمكنك حل هذه المشكلة بتحويل الرقم إلى سلسلة نصية، وهذا ما سنتعلمه في القسم التالي. الدوال str() و int() و float() إذا أردت ضم عدد صحيح مثل 29 إلى سلسلة نصية لتمريره إلى الدالة print() مثلًا، فما ستحتاج إليه هو السلسلة النصية '29' التي هي النسخة النصية من العدد 29. الدالة str() تقبل أن يُمرَّر إليها عدد صحيح ثم تنتج لنا سلسلةً نصيةً تمثل هذا العدد كما يلي: >>> str(29) '29' >>> print('I am ' + str(29) + ' years old.') I am 29 years old. ولأن ناتج التعبير str(29) هو '29' فسيكون ناتج التعبير 'I am ' + str(29) + ' years old.' هو 'I am ' + '29' + ' years old.' الذي بدوره سينتج 'I am 29 years old.'، وهذه هي القيمة التي ستمرر إلى الدالة print(). الدوال str() و int() و float() ستحول القيم التي تمررها إليها إلى سلسلة نصية وعدد صحيح وعدد ذي فاصلة عائلة على التوالي وبالترتيب. جرب تحول بعض القيم في الصدفة التفاعلية وانظر ماذا سيحدث: >>> str(0) '0' >>> str(-3.14) '-3.14' >>> int('42') 42 >>> int('-99') -99 >>> int(1.25) 1 >>> int(1.99) 1 >>> float('3.14') 3.14 >>> float(10) 10.0 نستدعي في المثال السابق الدوال str() و int() و float() ونمرر إليهم مجموعةً من القيم لها أنواع بيانات مختلفة، وسنحصل على نسخة نصية أو عددية من تلك القيم. تفيد الدالة str() حينما يكون لدينا عدد صحيح أو ذو فاصلة عائمة ونريد ضمه إلى سلسلة نصية، بينما تفيد الدالة int() حينما يكون لدينا رقم مخزن على شكل سلسلة نصية ونريد إجراء بعض العمليات الحسابية عليه، فمثلًا تعيد الدالة input() سلسلةً نصيةً دومًا حتى لو أدخل المستخدم رقمًا، فجرب إدخال spam = input() في الصدفة التفاعلية وكتابة رقم ما ثم طباعة قيمة المتغير spam: >>> spam = input() 101 >>> spam '101' لاحظ أن القيمة المخزنة في المتغير spam ليست العدد 101 بل السلسلة النصية '101'، فلو أردت إجراء أي عملية رياضية على القيمة المخزنة في spam فعليك استخدام الدالة int() للحصول على عدد صحيح. سنعيد تخزين القيمة العددية للمتغير spam في المتغير spam نفسه: >>> spam = int(spam) >>> spam 101 يمكنك الآن التعامل مع المتغير spam كأي عدد صحيح: >>> spam * 10 / 5 202.0 لاحظ أنك إذا مررت قيمةً إلى الدالة int() لا يمكن أن تحول إلى عدد صحيح، فستظهر لك بايثون رسالة خطأ: >>> int('99.99') Traceback (most recent call last): File "<pyshell#18>", line 1, in <module> int('99.99') ValueError: invalid literal for int() with base 10: '99.99' >>> int('twelve') Traceback (most recent call last): File "<pyshell#19>", line 1, in <module> int('twelve') ValueError: invalid literal for int() with base 10: 'twelve' تفيد أيضًا الدالة int() بتحويل عدد عشري إلى عدد صحيح (مع تقريبه إلى أصغر عدد صحيح يمثله): >>> int(7.7) 7 >>> int(7.7) + 1 8 لقد استخدمنا الدالتين int() و str() في آخر ثلاثة أسطر من برنامجك للحصول على قيمة نوع البيانات المطلوب: ➏ print('What is your age?') # ask for their age myAge = input() print('You will be ' + str(int(myAge) + 1) + ' in a year.') مساواة النصوص والأرقام صحيحٌ أن القيمة النصية لعددٍ ما تختلف اختلافًا تامًا عن العدد الصحيح أو العدد ذي الفاصلة العائمة، لكن يمكن أن يساوي العددُ الصحيح العددَ ذا الفاصلة العائمة: >>> 42 == '42' False >>> 42 == 42.0 True >>> 42.0 == 0042.000 True تفعل بايثون ذلك لأن السلاسل النصية هي نصوص بينما الأعداد الصحيحة والأعداد ذات الفاصلة هي أعداد في نهاية المطاف. المتغير myAge يحتوي على القيمة المعادة من input()، ولمّا كانت القيمة المعادة من الدالة input() هي سلسلة نصية دومًا حتى لو أدخل المستخدم رقمًا، فعلينا استخدام int(myAge) لإعادة قيمة عددية صحيحة من السلسلة النصية الموجودة في myAge، ثم سنزيد مقدار هذا العدد بواحد في التعبير int(myAge) +1. سنمرر بعد ذلك نتيجة التعبير السابق إلى الدالة str() على الشكل str(int(myAge) +1)، ثم سنضم القيمة النصية للتعبير السابق مع السلسلتين النصيتين 'You will be ' و ' in a year.' وذلك للحصول على قيمة نصية واحدة، ثم ستمرر هذه القيمة النصية إلى الدالة print() لطباعتها على الشاشة. لنقل مثلًا أن المستخدم أدخل السلسلة النصية '4' قيمةً للمتغير myAge عبر الدالة input(). ستحول السلسلة النصية '4' إلى عدد صحيح 4، ثم سيضاف 1 إليها فتصبح 5، ثم تحولها الدالة str() إلى سلسلة نصية مجددًا لإضافتها إلى السلاسل النصية الأخرى، وبالتالي ستنتج لدينا النسخة النهائية التي ستطبع على الشاشة، كما هو ظاهر في الشكل الآتي: الخلاصة يمكنك أن تحسب العمليات الحسابية بالآلة الحاسبة أو تضم السلاسل النصية عبر معالج النصوص في حاسوبك، ويمكنك تكرار السلاسل النصية بسهولة بنسخها ولصقها مرارًا وتكرارًا. لكن التعابير البرمجية -وما تحتويه من مكونات مثل العوامل والقيم واستدعاءات الدوال- هي اللبنة الأساسية لبناء البرامج، وبعد أن تتعلم هذه العناصر الأساسية فستتمكن من إعطاء أوامر لبايثون لتنفذ لك أمورًا معقدة على مجموعة كبير من البيانات. من المهم أن تتذكر من هذا المقال أنواع العوامل المختلفة (+ و - و * و / و // و % و ** للعوامل الحسابية، و + و * للسلاسل النصية) وأنواع البيانات المختلفة (الأعداد الصحيحة integers والأعداد ذات الفاصلة العائمة floating-point والسلاسل النصية string). شرحنا أيضًا بعض الدوال، مثل print() و input() التي تتعامل مع النصوص البسيطة لطباعتها على الشاشة أو لإدخالها من لوحة المفاتيح، واستعملنا الدالة len() للحصول على القيمة العددية لسلسلة نصية، وساعدتنا الدوال str() و int() و float() في تحويل القيم المُمرَّرة إليها إلى سلاسل نصية أو أعداد صحيحة أو أعداد ذات فاصلة على التوالي. سنتعلم في المقال القادم كيف نخبر بايثون متى تنفذ الشيفرة ومتى تتجاوزها بذكاء، وكيفية تكرار جزء من الشيفرة اعتمادًا على شرط محدد، وهذا ما نسميه «بنى التحكم» مما يسمح لنا بكتابة برامج تتصرف تصرفات ذكية. الآن وبعد أن أخذت المعارف اللازمة من المقال، برأيك لماذا سيسبب التعبير الآتي خطأً؟ وكيف نحله؟ شاركنا ذلك في التعليقات. 'I have eaten ' + 99 + ' burritos.' ترجمة -وبتصرف- للفصل Python Basics من كتاب Automate the boring stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: تهيئة بيئة العمل في بايثون Python أساسيات البرمجة بلغة بايثون تعلم لغة بايثون مشاريع بايثون عملية تناسب المبتدئين النسخة العربية الكاملة لكتاب: البرمجة بلغة بايثون
- 1 تعليق
-
- 1
-
قبل شرح كيفية تهيئة بايثون، نود إعلامك أن هذا سيكون أول مقال من سلسلة مقالات أتمتة المهام عبر بايثون. ستشرح هذه السلسلة أساسيات البرمجة بلغة بايثون، والمهام المختلفة التي يمكن لحاسوبك أتمتتها، كما سنشاركك مشاريع عملية مع نهاية كل جزئية منه، والتي تستطيع استعمالها ودراستها ومشاركتنا تطبيقاتك لها في التعليقات. ما هي بايثون؟ بايثون هي لغة برمجة، ولغة البرمجة هي مجموعة قواعد لكتابة الشيفرة يجب الالتزام بها ليفهمها مترجم اللغة، ومترجم بايثون Python interpreter هو برمجية تقرأ الشيفرات المصدرية بلغة بايثون وتطبق التعليمات البرمجية فيها. يمكنك أن تنزل مفسر لغة بايثون مجانًا من موقع اللغة الرسمي، الذي فيه إصدارات لأنظمة تشغيل ويندوز وماك ولينكس. إذا كنتَ تتساءل من أين أتى اسم بايثون، فهو من مجموعة كوميدية بريطانية اسمها Monty Python، ولا علاقة للثعابين والأفاعي باسمها. تنزيل وتثبيت بايثون يمكنك تنزيل بايثون لنظام تشغيل ويندوز أو ماك أو لينكس مجانًا من رابط موقع اللغة، وستعمل معك جميع التطبيقات المشروحة في هذه السلسلة معك. ستجد في موقع بايثون نسختين إحداهما 64-بت والأخرى 32-بت لكل نظام تشغيل، لذا عليك اختيار الملف الصحيح لتنزله وتثبته. إذا اشتريت حاسوبك بعد عام 2007 فمن المرجح أنك تستعمل نظام 64-بت، وإلا فعليك استخدام نسخة 32-بت. إذا كنت تستعمل نظام ويندوز فنزِّل مثبت بايثون (الذي ينتهي باللاحقة .exe) وشغِّله، ثم اتبع ما يرشدك إليه المثبِّت على الشاشة. أما على نظام macOS فنزِّل ملف .pkg ثم شغله واتبع الإرشادات الموجودة على الشاشة. أما إذا كنت تستعمل توزيعة أوبنتو (أو ما يشبهها من التوزيعات المبنية على دبيان) فيمكنك تثبيت بايثون من سطر الأوامر بتنفيذ الأمر الآتي: sudo apt install python3 python3-pip idle3 تنزيل وتثبيت محرر Mu صحيحٌ أن مفسِّر بايثون هو البرنامج الذي يشغل شيفرات بايثون التي تكتبها، لكن برمجية Mu editor هي مكان إدخالك للشيفرات، بنفس الطريقة التي تكتب فيها بمعالج النصوص المفضل لديك. يمكنك تنزيل محرر Mu من موقعه. نزِّل المثبِّت الخاص بنظامك إن كنت تستعمل ويندوز أو ماك، ثم ابدأ عملية التثبيت بفتح المثبت، أما على لينكس فستحتاج إلى تثبيت محرر Mu كحزمة بايثون، وفي تلك الحالة اتبع الإرشادات الموجودة في الموقع. تشغيل محرر Mu بعد انتهاء تثبيت Mu، يمكنك تشغيله: في نظام ويندوز بالضغط على قائمة ابدأ ثم البحث عن Mu. في نظام MacOS بفتح Finder ثم Applications ثم الضغط على أيقونة mu-editor. في لينكس عليك تشغيل الطرفية Terminal ثم كتابة mu-editor. وحين تشغيلك لمحرر Mu لأول مرة فستظهر لك نافذة تخيّرك بين عدة خيارات وعليك اختيار Python 3، يمكنك في أي وقت تغيير النمط بالضغط على زر Mode في أعلى نافذة المحرر. تشغيل IDLE نستعمل في هذه السلسلة Mu كمحرر وصدفة تفاعلية interactive shell، لكنك تستطيع استخدام أي محرر تشاء لكتابة شيفرات بايثون، وتثبت بيئة التطوير والتعليم المدمجة Integrated Development and Learning Environment (اختصارًا IDLE) مع بايثون تلقائيًا، ويمكنها أن تعمل كمحرر ثانوي لك إن لم يعمل يثبت عندك محرر Mu. لنشغل IDLE: في نظام ويندوز بالضغط على قائمة ابدأ ثم البحث عن IDLE. في نظام macOS بفتح Finder ثم Applications ثم الضغط على أيقونة Python. في لينكس عليك تشغيل الطرفية Terminal ثم كتابة idle3. الصَدَفة التفاعلية Interactive Shell عندما تشغل Mu فستظهر أمامك نافذة تحرير الملف، ويمكنك فتح الصَدَفة التفاعلية بالضغط على زر REPL. الصَدَفة Shell هي برنامج يسمح لك بإعطاء تعليمات للحاسوب، كما تفعل حينما تفتح الطرفية Terminal في لينكس أو ماك، أو موجه الأوامر في ويندوز. تسمح لك الصدفة التفاعلية في بايثون بإدخال التعليمات لينفذها مفسِّر بايثون مباشرةً. تجد الصدفة التفاعلية في محرر Mu في إطار في الجزء السفلي من النافذة فيها النص الآتي: Jupyter QtConsole 4.7.7 Python 3.6.9 (default, Mar 15 2022, 13:55:28) Type 'copyright', 'credits' or 'license' for more information IPython 7.16.3 -- An enhanced Interactive Python. Type '?' for help. In [1]: إذا كنت تستعمل IDLE، فالصدفة التفاعلية هي أول ما ستراه أمامك، ويفترض أن تكون فارغة باستثناء نص يبدو كما يلي: Python 3.6.9 (default, Mar 15 2022, 13:55:28) [GCC 8.4.0] on linux Type "help", "copyright", "credits" or "license()" for more information. >>> الأسطر التي تبدأ بالعبارة In [1]: أو <<< تسمى محثًا prompt (أي أنها عبارة «تحثّ» المستخدم على كتابة أمر وراءها). أمثلة هذه السلسلة ستستخدم <<< للإشارة إلى محث الصدفة التفاعلية لأنه أكثر شيوعًا. إذا شغلت مفسر بايثون من سطر الأوامر فستجد أمامك المحث <<< أيضًا. يستخدم محرر Jupyter Notebook الشهير المحث In [1]: وشاع بسببه. لنجرب مثالًا بسيطًا، أدخل السطر الآتي في الصدفة التفاعلية بعد المحث: >>> print('Hello, world!') بعد كتابة السطر السابق فاضغط على زر Enter، ويجب أن يظهر ما يلي في الصدفة: >>> print('Hello, world!') Hello, world! لقد أعطيت الحاسوب أمرًا ونفذه لك. مبارك! تثبيت وحدات خارجية بعض البرامج التي سنكتبها ستستورد وحدات modules، وتأتي بعض هذه الوحدات مع بايثون لكن هنالك وحدات أخرى خارجية طورها مبرمجون ليسوا من فريق تطوير لغة بايثون. يوضح الملحق أ بالتفصيل كيفية استخدام البرنامج pip في ويندوز أو pip3 في ماك ولينكس لتثبيت وحدات خارجية. أين تجد المساعدة يميل المطورون إلى التعلم بالبحث في الإنترنت عن إجابات عن أسئلتهم، وهذه الطريقة مختلفة كثيرًا عمّا اعتاد كثيرون على فعله من حضور دروس لمدرس يلقي محاضرةً ويجيب عن أسئلة الطلاب. ما يميز استخدام الإنترنت للتعلم أنك ستجد مجتمعات كاملة فيها أشخاص يستطيعون الإجابة عن أسئلتك، بل ومن المرجح أن تكون أسئلتك مجاب عنها مسبقًا وتنتظرك الإجابات عنها لتقرأها. إذا واجهت رسالة خطأ أو حدث خطبٌ ما أثناء محاولتك تشغيل شيفرتك، فلن تكون أول شخص يواجه هذه المشكلة، وأؤكد لك أن العثور على حلها أسهل مما تظن بكثير. لنسبب خطأً عمدًا في برنامجنا للتجربة: أدخل '42' + 3 في الصدفة التفاعلية، لا حاجة إلى شرح ما الذي تفعله هذه الشيفرة الآن لأننا سنتعلمها لاحقًا، لكن سيظهر لك الخطأ الآتي: >>> '42' + 3 ➊ Traceback (most recent call last): File "<pyshell#0>", line 1, in <module> '42' + 3 ➋ TypeError: Can't convert 'int' object to str implicitly >>> رسالة الخطأ ➋ تظهر لأن بايثون لا تستطيع فهم التعليمة التي أعطيتها إياها، والجزء الأول ➊ من رسالة الخطأ يبين لنا التعليمة الخطأ ورقم سطرها وما المشكلة التي واجهتها بايثون معها. إذا لم تكن متأكدًا من معنى رسالة الخطأ فابحث عنها في الويب. أدخل "TypeError: Can’t convert ‘int’ object to str implicitly" (بما فيها علامات الاقتباس) في محرك البحث المفضل لديك وستجد عشرات الصفحات التي توضح لك ما هذا الخطأ وما مسبباته كما في الصورة الآتية: الشكل 2: نتائج بحث جوجل حول رسالة خطأ برمجي ستجد أن أحدهم كان له نفس سؤالك وأتى مطور من أولي الخبرة وأجابه. اعلم أنه لا يمكن لشخص واحد أن يعرف كل شيء عن البرمجة، لذا يكون البحث عن إجابات الأسئلة التقنية جزءًا من يومك كمطور برمجيات. طرح أسئلة برمجية ذكية إذا لم تجد إجابةً عن سؤالك في الإنترنت، فيمكنك أن تجرب سؤال الخبراء في أحد المنتديات أو المواقع المخصصة مثل Stack Overflow أو مجتمع Learn Programming في Reddit. لكن أبقِ في ذهنك أن هنالك طرائق ذكية لطرح الأسئلة البرمجة لتساعد الآخرين على مساعدتك. فبدايةً احرص على قراءة قسم الأسئلة الشائعة FAQ في تلك المواقع لتعرف الطريقة الصحيحة لطرح السؤال. حين طرحك لسؤال برمجي فتذكر ما يلي: اشرح ما تحاول فعله، وليس ما فعلته. هذا يسمح لمن يريد مساعدته أن يخبرك إن كنت على الطريق الصحيح لحل المشكلة. حدد النقطة التي يحدث الخطأ عندها، هل تحدث مثلًا حين بدء تشغيل البرنامج كل مرة أم حين وقوع حدث معين؟ وما هو هذا الحدث الذي يحدث ويسبب ظهور الخطأ. انسخ والصق رسالة الخطأ كاملة على مواقع تسمح لك بمشاركة الشيفرات مع الآخرين بسرعة مثل pastebin أو gits.github أو قسم الأسئلة والأجوبة في أكاديمية حسوب، فعبرها تستطيع مشاركة نصوص طويلة دون أن تفقد تنسيق النص، وبعد ذلك ضع رابط URL التابع للشيفرة التي شاركتها في مشاركتك أو سؤالك. اشرح ما حاولت فعله لحل المشكلة، وبهذا تخبر الآخرين أنك بذلت جهدًا لمحاولة حل المشكلة بنفسك. ضع إصدار بايثون الذي تستخدمه (إذ هنالك اختلافات جوهرية بين الإصدار الثاني من مفسر بايثون والإصدار الثالث). اذكر أيضًا نوع نظام تشغيلك وإصداره. إذا حدث الخطأ بعد إجراء تعديل على شيفرتك فاذكر ما هو التعديل الذي أجريته. أرجو منك أيضًا أن تتبع آداب طرح النقاشات في الإنترنت، فاحرص على سلامة السؤال من الأخطاء الإملائية أو اللغوية، وأن تكون لغته واضحة للجميع، وإذا طرحت سؤالك بالإنكليزية فلا تضعه بأحرف كبيرة، ولا تقدم مطالب غير معقولة وغير منطقية ممن يمد إليك يد المساعدة. الخلاصة يرى أغلبية مستخدم الحاسوب أن حاسوبهم هو صندوق سحري بدل رؤيته كأداة يمكنهم استخدامها كيفما يشاؤون؛ وبتعلمك البرمجة ستصل إلى أقوى أدوات المتاحة في عالمنا الحالي وهي القدرة الحاسوبية الهائلة المتوافرة أمامك! وتذكر أن تستمتع بوقتك أثناء البرمجة وتعلمها، فالبرمجة ليست كالجراحة العصبية فلا بأس أن تخطئ وتجرب كما تشاء. نفترض في هذه السلسلة أنك لا تعرف شيئًا عن البرمجة وستتعلم فيه الكثير، لكن إن كانت لديك أسئلة خارج سياقه فتذكر أن تبحث عنها أو أن تطرحها على الخبراء بأسلوب واضح فالبحث عن الإجابات من أهم الأدوات التي عليك احترافها لتعلم البرمجة. ترجمة -وبتصرف- لمقدمة كتاب Automate the boring stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا تعلم لغة بايثون تعرف على أبرز مميزات لغة بايثون تطبيقات لغة بايثون النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون
-
مرحبًا محمود، بعد إنهائك للدورة وإجرائك للامتحان مرتين لم تستجب إلى إرشادات المدربين الذين نصحوك كيف تنجز المشاريع وكيف تتعلم بطريقة صحيحة... نلتزم في أكاديمية حسوب بما نعد طلابنا به، لذا أعدنا لك المبلغ الذي دفعته، ونرجو لك كل التوفيق في رحلتك القادمة.
- 9 اجابة
-
- 6
-
تُعَدّ Node.js بيئة تشغيل JavaScript التي تعمل من طرف الخادم، وهي مفتوحة المصدر ومتعددة المنصات cross-platform -أي تعمل على أكثر من نظام تشغيل- وهي مبنية على محرك جافاسكربت Chrome V8، وتستعمل أساسيًا لإنشاء خوادم الويب، لكنها ليست محدودةً لهذه المهمة فقط، كما أنها لاقت رواجًا بدءًا من انطلاقها في 2009، وتلعب الآن دورًا مهمًا في عالم تطوير الويب، فإذا عددنا أنّ النجوم التي يحصل عليها المشروع في GitHub معيارًا لشهرة البرمجية، فاعلم أنّ Node.js قد حصدت أكثر من 84 ألفًا من النجوم حتى الآن، ومن الجدير بالذكر أنّ Node.js مبنية على محرك جافاسكربت Chrome V8، كما تُستعمل بصورة أساسية لإنشاء خوادم الويب، لكنها ليست محدودةً لهذه المهمة فقط. هذا المقال جزء من سلسلة حول Node.js: مقدمة إلى Node.js استخدام الوضع التفاعلي والتعامل مع سطر الأوامر في Node.js دليلك الشامل إلى مدير الحزم npm في Node.js كيفية تنفيذ الدوال داخليا ضمن Node.js البرمجة غير المتزامنة في Node.js التعامل مع الطلبيات الشبكية في Node.js التعامل مع الملفات في Node.js تعرف على وحدات Node.js الأساسية أفضل ميزات Node.js تتميز Node.js بالمزايا التالية: السرعة إحدى الميزات التي تشتهر بها Node.js هي السرعة، فشيفرة JavaScript التي تعمل على Node.js اعتمادًا على اختبارات الأداء benchmark يمكن أن تكون بضعفي سرعة تنفيذ اللغات المصرَّفة compiled مثل C أو Java، وأضعاف سرعة اللغات المفسَّرة مثل بايثون أو روبي بسبب نموذج عدم الحجب non-blocking الذي تستعمله. بسيطة صدِّقنا عندما نخبرك بأن Node.js بسيطة، بل بسيطة جدًا. تستعمل JavaScript تشغِّل Node.js شيفرة JavaScript، وهذا يعني أنّ ملايين مبرمجي الواجهات الأمامية الذين يستعملون JavaScript في المتصفح سيستطيعون العمل على شيفرات من طرف الخادم ومن طرف الواجهات الأمامية باستخدام اللغة نفسها، فلا حاجة إلى تعلّم لغة جديدة كليًا، حيث ستكون جميع التعابير التي تستعملها في JavaScript متاحةً في Node.js، واطمئن إلى أنّ آخر معايير ECMAScript مستعملة في Node.js، فلا حاجة إلى انتظار المستخدِمين ليحدِّثوا متصفحاتهم، فأنت صاحب القرار بأي نسخة ECMAScript تريد استخدامها في برنامجك باختيار إصدار Node.js المناسب. محرك V8 يمكنك الاستفادة من عمل آلاف المهندسين الذين جعلوا -ويستمروا بجعل- محرك JavaScript الخاص بمتصفح Chrome سريعًا للغاية، وذلك باعتماد Node.js على محرك Chrome V8 المفتوح المصدر. منصة غير متزامنة تُعَدّ جميع الأوامر البرمجية في لغات البرمجة التقليدية حاجبة blocking افتراضيًا مثل سي C وجافا Java وبايثون وبي إتش بي PHP إلا إذا تدخّلتَ بصورة صريحة لإنشاء عمليات غير متزامنة؛ فإذا أجريت مثلًا طلبًا شبكيًا لقراءة ملف JSON، فسيتوقف التنفيذ حتى يكون الرد response جاهزًا. تسمح JavaScript بكتابة شيفرات غير متزامنة asynchronous وغير حاجبة non-blocking بطريقة سهلة جدًا باستخدام خيط thread وحيد ودوال رد النداء callback functions والبرمجة التي تعتمد على الأحداث event-driven، حيث نمرر دالة رد نداء والتي ستُستدعَى حين نتمكن من إكمال معالجة العملية وذلك في كل مرة تحدث عملية تستهلك الموارد، كما أننا لن ننتظر الانتهاء من ذلك قبل الاستمرار في تنفيذ بقية البرنامج. أُخِذت هذه الآلية من المتصفح، فلا يمكننا انتظار تحميل شيء ما عبر طلب AJAX قبل أن نكون قادرين على التعامل مع أحداث النقر على عناصر الصفحة، فيجب حدوث كل شيء في الوقت الحقيقي لتوفير تجربة جيدة للمستخدِم، مما يسمح بمعالجة آلاف الاتصالات بخادم Node.js وحيد دون الدخول في تعقيدات إدارة الخيوط threads، والتي تكون سببًا رئيسيًا للعلل في البرامج. توفِّر Node.js تعاملًا غير حاجب مع الدخل والخرج I/O، وتكون المكتبات في Node.js عمومًا مكتوبةً بمنهجية عدم الحجب، مما يجعل سلوك الحجب في Node.js استثناءً للقاعدة وليس شيئًا طبيعيًا، كما تكمل Node.js العمليات عند وصول الرد عندما تريد إجراء عملية دخل أو خرج مثل القراءة من الشبكة أو الوصول إلى قاعدة البيانات أو نظام الملفات بدلًا من حجب الخيط blocking the thread وإهدار طاقة المعالج بالانتظار. عدد هائل من المكتبات ساعد مدير الحزم npm ببنيته البسيطة النظام العام في node.js، إذ يستضيف npm ما يقرب من 500 ألف حزمة مفتوحة المصدر تستطيع استخدامها بحرّية. مثال عن تطبيق Node.js المثال الأكثر شيوعًا عن تطبيق Node.js هو خادم ويب يعرض العبارة الشهيرة Hello World: const http = require('http') const hostname = '127.0.0.1' const port = 3000 const server = http.createServer((req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end('Hello World\n') }) server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`) }) احفظ الشيفرة البسيطة السابقة في ملف باسم server.js ثم نفِّذ node server.js في الطرفية الخاصة بك وذلك من أجل تنفيذ تلك الشيفرة. تبدأ الشيفرة السابقة بتضمين وحدة http، إذ تمتلك Node.js مكتبةً قياسيةً رائعةً بما في ذلك دعم التعامل مع الشبكات؛ أما التابع createServer() الخاص بوحدة http فيُنشئ خادم HTTP جديد ويعيده، كما أنّ الخادم قد ضُبِط للاستماع إلى منفذ وعنوان شبكي محدَّدين، وعندما يجهز الخادم فستُستدعى دالة رد النداء والتي تخبرنا في هذه الحالة أنّ الخادم جاهز، وكلما استقبل الخادم طلبًا request جديدًا، فسيُطلَق الحدث request الذي يوفِّر كائنين هما الطلب أي كائن http.IncomingMessage والرد أي كائن http.ServerResponse، ويُعَدّ هذان الكائنان أساسًا للتعامل مع استدعاء HTTP. يوفِّر الكائن الأول معلومات الطلبية لكننا لم نستعمله في هذا المثال البسيط، إلا أنه يمكنك الوصول إلى ترويسات الطلب وإلى بيانات الطلب؛ أما الكائن الثاني فيعيد البيانات إلى صاحب الطلب caller، وفي هذه الحالة باستخدامنا للسطر التالي: res.statusCode = 200 ضبطنا قيمة الخاصية statusCode إلى 200 والتي تعني أنّ الرد ناجح، ثم ضبطنا ترويسة Content-Type كما يلي: res.setHeader('Content-Type', 'text/plain') ثم ننهي الطلب بإضافة المحتوى على أساس وسيط argument إلى التابع end(): res.end('Hello World\n') أدوات Node.js وأطر عملها تُعَدّ Node.js منصةً منخفضة المستوى، كما توجد آلاف المكتبات المكتوبة باستخدام Node.js لتسهيل الأمور على المطوِّرين وجعلها أكثر متعةً وسلاسةً، إذ أصبح عدد كبير من هذه المكتبات شائعًا بين المطوِّرين، وهذه قائمة غير شاملة للمكتبات التي نراها تستحق التعلم: Express: أحد أبسط وأقوى الطرق لإنشاء خادم ويب، ويكون تركيزه على البساطة وعدم الانحياز والميزات الأساسية لخادم الويب هو مفتاح نجاحه. Meteor: إطار عمل Framework قوي جدًا ومتكامل، يسمح لك ببناء التطبيقات باستخدام JavaScript ومشاركة الشيفرة بين العميل والخادم، كما أصبح الآن يتكامل بسلاسة مع مكتبات واجهة المستخدِم مثل React و Vue و Angular، في حين يمكن استخدامه أيضًا لإنشاء تطبيقات الويب. koa: بناه فريق Express نفسه ويطمح إلى أن يكون أصغر وأبسط اعتمادًا على سنوات الخبرة الطويلة للفريق، إذ بدأ هذا المشروع الجديد للحاجة إلى إنشاء تغييرات غير متوافقة مع ما سبقها دون تخريب ما أُنجِز في المشروع. Next.js: إطار عمل يستعمل التصيير rendering من طرف الخادم لتطبيقات React. Micro: خادم خفيف جدًا لإنشاء خدمات HTTP مصغرة microservices غير متزامنة. Socket.io: محرك تواصل في الوقت الحقيقي لبناء تطبيقات الشبكة. تاريخ موجز عن Node.js لننظر إلى تاريخ Node.js من عام 2009 حتى الآن. أُنشِئت لغة JavaScript في شركة Netscape على أساس أداة لتعديل صفحات الويب داخل متصفحها Netscape Navigator، كما يُعَدّ بيع خوادم الويب جزءًا من نموذج الأعمال في Netscape والتي تحتوي على بيئة باسم Netscape LiveWire التي تستطيع إنشاء صفحات آلية باستخدام JavaScript من طرف الخادم؛ أي أنّ فكرة استخدام JavaScript من طرف الخادم لم تبدأ من Node.js وإنما هي قديمة قدم JavaScript، إلا أنها لم تكن ناجحةً في تلك الفترة. أحد العوامل الرئيسية لاشتهار Node.js هو التوقيت، إذ بدأت لغة JavaScript تُعَدّ لغةً حقيقيةً، وذلك بفضل تطبيقات Web 2.0 التي أظهرت للعالم كيف تكون التجربة الحديثة للويب مثل Google Maps أو GMail، كما أنّ أداء محركات JavaScript قد ارتفع بشدة بفضل حرب المتصفحات، إذ تعمل فرق التطوير خلف كل متصفح رئيسي بجدّ كل يوم لتحسين الأداء، وكان هذا فوزًا عظيمًا لمنصة JavaScript، علمًا أنّ محرك V8 الذي تستعمله Node.js هو محرك متصفح Chrome، وبالطبع لم تكن شهرة Node.js محض صدفة أو توقيت جيد، إذ أضافت مفاهيم ثورية في كيفية البرمجة باستخدام JavaScript من طرف الخادم. عام 2009: ولدت Node.js وأُنشِئت أول نسخة من npm. عام 2010: ولد كل من Express وSocket.io. عام 2011: وصل npm إلى الإصدار 1.0 وبدأت الشركات الكبيرة بتبني Node.js مثل LinkedIn، كما ولد Hapi. عام 2012: استمرت عملية تبنّي Node.js بسرعة كبيرة. عام 2013: أُنشِئت أول منصة تدوين باستخدام Node.js: ولدت Koa. عام 2014: اشتق مشروع IO.js من Node.js بهدف إضافة دعم ES6 والتطوير بوتيرة أسرع. عام 2015: أُسست منظمة Node.js Foundation، ودمج مشروع IO.js مع Node.js مجددًا، كما أصبح npm يدعم الوحدات الخاصة private modules، وأُصدرت نسخة Node 4 (ولم تُصدَر النسخة 1 أو 2 أو 3 من قبل). عام 2016: ولد مشروع Yarn، وأُصدِرت Node 6. عام 2017: ركّز npm على الحماية أكثر، وأُصدرت Node 8، وأضاف محرك V8 وحدة HTTP/2 بنسخة تجريبية. عام 2018: أُصدرت Node 10، وأضيف دعم تجريبي لوحدات ES بلاحقة .mjs. عام 2019: أُصدرت Node 12 وNode 13. عام 2020: أُصدرت Node 14. عام 2021: الإصدار الحالي هو 16 والذي أصبح الإصدار المستقر طويل الدعم LTS حاليًا. كيفية تثبيت Node.js يمكن تثبيت Node.js بطرائق مختلفة، وسنشرح في هذا الفصل أشهر الطرائق وأسهلها لتثبيتها، كما أنّ الحزم الرسمية لجميع أنظمة التشغيل الرئيسية متوافرة على الرابط nodejs.org/en/download. إحدى الطرائق المناسبة لتثبيت Node.js هي استعمال مدير الحزم، حيث يملك كل نظام تشغيل مدير حزم خاص به، ففي نظام macOS يكون مدير الحزم Homebrew هو مدير الحزم الأساسي، ويسمح بعد تثبيته بتثبيت Node.js بسهولة، وذلك بتنفيذ الأمر التالي في سطر الأوامر داخل الطرفية، ولن نذكر بالتفصيل مدراء الحزم المتاحة للينكس أو ويندوز منعًا للإطالة، لكنها موجودة بالتفصيل في موقع nodejs.org: brew install node يُعَدّ nvm الطريق الشائع لتشغيل Node.js، إذ يسمح لك بتبديل إصدار Node.js بسهولة وتثبيت الإصدارات الجديدة لتجربتها ثم العودة إلى الإصدار القديم إذا لم يعمل كل شيء على ما يرام، فمن المفيد جدًا على سبيل المثال تجربة الشيفرة الخاصة ببرنامج على الإصدارات القديمة من Node.js، كما ننصحك بمراجعة github.com/creationix/nvm لمزيد من المعلومات حول هذا الخيار، وننصحك بصورة شخصية باستعمال المثبِّت الرسمي إذا أنت حديث العهد على Node.js ولا تريد استخدام مدير الحزم الخاص بنظام تشغيلك، لكن على أي حال، سيكون البرنامج التنفيذي node متاحًا في سطر الأوامر بعد تثبيت Node.js بأي طريقة من الطرائق السابقة. ماذا عليك معرفته في JavaScript لاستخدام Node.js إذا بدأت لتوّك مع JavaScript، فما مدى عمق المعلومات التي تلزمك لاستخدام Node.js؟ من الصعب الوصول إلى النقطة التي تكون فيها واثقًا من قدراتك البرمجية كفاية، فحين تعلّمك كتابة الشيفرات، فقد تكون محتارًا متى تنتهي شيفرة JavaScript ومتى تبدأ Node.js وبالعكس، لذا ننصحك أن تكون متمكنًا تمكنًا جيدًا من المفاهيم الأساسية في JavaScript قبل التعمق في Node.js: بنية البرنامج. التعابير. الأنواع. المتغيرات الدوال والدوال السهمية. الكلمة المحجوزة this. حلقات التكرار والمجالات scopes. المصفوفات. الفواصل المنقوطة (نعم، هذا المحرف ; ? ) الوضع الصارم. إصدارات ECMAScript مثل ES6 وES2016 وES2017. ستكون بعد تعلّمك للمفاهيم السابقة في طريقك لتصبح مطور JavaScript محترف في بيئة المتصفح وNode.js، وفيما يلي مفاهيم أساسية لفهم البرمجة غير المتزامنة asynchronous programming والتي هي جزء أساسي من Node.js: مفهوم البرمجة غير المتزامنة ورد النداء callbacks. المؤقتات Timers. الوعود Promises. الكلمتان المحجوزتان Async وAwait. التعابير المغلقة Closures. حلقات الأحداث. توجد مقالات كثيرة وكتب في أكاديمية حسوب بالإضافة إلى توثيق JavaScript في موسوعة حسوب عن جميع المواضيع السابقة. الاختلافات بين Node.js والمتصفح كيف تختلف كتابة تطبيقات JavaScript في Node.js عن البرمجة للويب داخل المتصفح؟ يستخدِم كل من المتصفح وNode.js لغة JavaScript للبرمجة؛ لكن بناء التطبيقات التي تعمل في المتصفح مختلف تمامًا عن بناء تطبيقات Node.js، فعلى الرغم من أنهما يستعملان لغة البرمجة نفسها JavaScript، إلا أنه هنالك اختلافات جوهرية تجعل الفرق بينهما كبيرًا،. يملك مطور واجهات المستخدِم الذي يكتب تطبيقات Node.js ميزةً رائعةً ألا وهي استخدامه للغة البرمجة نفسها، فمن المعروف أنه من الصعب تعلّم لغة برمجة جديدة بإتقان، لكن باستعمال لغة البرمجة نفسها لإجراء كل العمل على موقع الويب سواءً للواجهة الأمامية أو الخادم، حيث توجد أفضلية واضحة في هذه النقطة؛ إلا أنّ بيئة العمل هي التي تختلف. ستتعامل أغلب الوقت في المتصفح مع شجرة DOM أو غيرها من الواجهات البرمجية الخاصة بالمتصفح Web Platform APIs مثل ملفات الارتباط Cookies والتي لا توجد في Node.js، وبالطبع لن تكون الكائنات document وwindow وغيرها من كائنات المتصفح متوافرةً، كما لن نحصل على الواجهات البرمجية APIs التي توفرها Node.js عبر وحداتها مثل الوصول إلى نظام الملفات. يوجد اختلاف كبير آخر وهو أنك تستطيع التحكم في البيئة التي تعمل فيها Node.js ما لم تبني تطبيقًا مفتوح المصدر يمكن لأي شخص نشره في أيّ مكان، فأنت تعلم ما هي نسخة Node.js التي سيعمل عليها تطبيقك؛ فليس لديك الحرية في اختيار المتصفح الذي يستخدمه زوار موقعك بموازنة ذلك مع بيئة المتصفح، وهذا يعني أنك يمكنك أن تكتب شيفرات ES6-7-8-9 التي تدعمها نسخة Node.js عندك. ستكون ملزمًا باستخدام إصدارات JavaScript/ECMAScript القديمة على الرغم من تطوّر JavaScript بسرعة كبيرة، وذلك لأن المتصفحات أبطأ منها والمستخدِمون أبطأ بالتحديث، وصحيحٌ أنك تستطيع استخدام Babel لتحويل شيفرتك إلى نسخة موافقة لمعيار ECMAScript 5 قبل إرسالها إلى زوار موقعك، لكنك لن تحتاج إلى ذلك في Node.js. يوجد اختلاف آخر هو استخدام Node.js لنظام الوحدات CommonJS، بينما بدأنا نرى أنّ المتصفحات تستخدم معيار وحدات ES Modules؛ وهذا يعني عمليًا أنه عليك استخدام require() في Node.js، وimport في المتصفح حاليًا. محرك V8 V8 هو اسم محرك JavaScript الذي يُشغِّل متصفح Chrome، حيث يأخذ شيفرات JavaScript وينفذها أثناء التصفح عبر Chrome، إذ يوفر بيئة التشغيل اللازمة لتنفيذ شيفرات JavaScript، في حين يوفِّر المتصفح شجرة DOM وغيرها من الواجهات البرمجية للويب، كما يُعَدّ استقلال محرك JavaScript عن المتصفح الذي يستخدمه الميزة الرئيسية التي سببت بانتشار Node.js. اختار مجتمع مطوري Node.js محرك V8 في 2009، وبعد أن ذاع صيت Node.js صار محرك V8 هو المحرك الذي يشغِّل عددًا غير محصور من شيفرات JavaScript التي تعمل من طرف الخادم، وخذ بالحسبان أنّ بيئة تشغيل Node.js كبيرة جدًا ويعود الفضل إليها في أنّ محرك V8 أصبح يشغِّل تطبيقات سطح المكتب عن طريق مشاريع مثل Electron.js. محركات JavaScript الأخرى تملك المتصفحات الأخرى محركات JavaScript مختلفة منها: يملك متصفح Firefox محرك SpiderMonkey. يملك متصفح Safari محرك JavaScriptCore ويسمى Nitro أيضًا. يملك متصفح Edge محرك Chakra. تطبِّق جميع هذه المحركات معيار ECMA ES-262 والذي يسمى ECMAScript أيضًا، وهو المعيار المستخدَم في لغة JavaScript. السعي إلى الأداء الأفضل كُتِب محرك V8 بلغة C++ ويُحسَّن باستمرار، وهو محمول portable ويعمل على ماك ولينكس وويندوز وغيرها من أنظمة التشغيل، كما لن نخوض في تفاصيل محرك V8 لأنها موجودة في مواقع كثيرة مثل موقع V8 الرسمي وتتغير مع مرور الوقت، وعادةً تكون التغييرات كبيرةً، إذ يتطور محرك V8 دومًا كما في غيره في محركات JavaScript لتسريع الويب وبيئة Node.js، كما يجري سباق في عالم الويب للأداء الأفضل منذ سنوات، ونحن -المستخدِمون والمطوِّرون- نستفيد كثيرًا من هذه المنافسة لأنها تعطينا أداءً أسرع وأفضل سنةً بعد سنة. التصريف Compilation تُعَدّ لغة JavaScript عمومًا لغةً مفسَّرةً interpreted؛ لكن محركات JavaScript الحديثة لم تَعُد تفسِّر شيفرات JavaScript وحسب، وإنما تُصرِّفها compile، فقد حدث ذلك منذ عام 2009، عندما أُضيف مُصرِّف SpuderMonkey إلى متصفح Firefox 3.5، ثم اتبع الجميع هذه الفكرة، حيث تُصرَّف JavaScript داخليًا في V8 حين اللزوم بتصريف JIT (اختصارًا لـ just-in-time) لتسريع عملية التنفيذ. قد ترى أنّ ذلك مناف للمنطق، لكن تطورت لغة JavaScript من لغة تنفِّذ عادةً بضعة مئات من الأسطر إلى لغة تشغِّل تطبيقات كاملة تحتوي على آلاف أو حتى مئات الآلاف من الأسطر البرمجية التي تعمل في المتصفح وذلك منذ إطلاق Google Maps في 2004، فيمكن لتطبيقاتنا الآن أن تعمل لساعات في المتصفح بدلًا من سكربتات بسيطة للتحقق من صحة المدخلات أو إجراء أفعال بسيطة معينة، ففي هذا العالم الجديد أصبح من المنطقي تمامًا تصريف شيفرات JavaScript؛ وصحيحٌ أنها قد تأخذ بعض الوقت القليل لجهوزية شيفرة JavaScript، إلا أننا سنرى أداءً أفضل من الشيفرة المفسَّرة فقط. تشغيل تطبيق Node.js والخروج منه الطريقة التقليدية لتشغيل برنامج Node.js هي استخدام الأمر node المتاح في نظام التشغيل بعد تثبيت Node.js ثم تمرير اسم الملف الذي تريد تنفيذه إلى الأمر، فلو كان تطبيق Node.js عندك موجود في ملف باسم app.js، فيمكنك تشغيله بكتابة الأمر التالي في سطر الأوامر: node app.js لنتعلم كيف يمكننا إنهاء تطبيق Node.js بأفضل الطرائق الممكنة، حيث توجد عدة طرق لإنهائه، فعندما تشغِّل برنامج في سطر الأوامر، يمكنك أن توقفه بالضغط على Ctrl+c، لكن ما نريد الحديث عنه هو إنهاء التطبيق برمجيًا، ولنبدأ بأكثر طريقة قاسية لإنهاء التطبيق، ولنرَ لماذا لا يفترض بك استعمالها. توفِّر الوحدة الأساسية core module المسماة process تابعًا يسمح لك بالخروج برمجيًا من تطبيق Node.js وهو process.exit()، إذ سيؤدي تشغيل Node.js هذا السطر إلى إغلاق العملية process مباشرةً، وهذا يعني أنه سيتوقف أيّ رد نداء معلَّق، أو أيّ طلب شبكي لا يزال مُرسَلًا، أو أيّ وصول إلى نظام ملفات، أو عمليات تكتب إلى مجرى الخرج القياسي stdout أو الخطأ القياسي stderr توقفًا مباشرًا دون سابق إنذار، وإذا لم تجد حرجًا في ذلك، فيمكنك تمرير عدد صحيح إلى التابع ليخبر نظام التشغيل ما هي حالة الخروج exit code: process.exit(1) تكون حالة الخروج الافتراضية هي 0، والتي تعني نجاح تنفيذ البرنامج، حيث تمتلك حالات الخروج المختلفة معانٍ خاصة والتي يمكنك استخدامها في نظامك للتواصل مع بقية البرامج، كما يمكنك أيضًا ضبط قيمة الخاصية process.exitCode بحيث تُعيد Node.js حالة الخروج المضبوطة إلى هذه الخاصية عند انتهاء تنفيذ البرنامج: process.exitCode = 1 ينتهي البرنامج بسلام عند انتهاء تنفيذ جميع الشيفرات فيه في الحالة الطبيعية، وكثيرًا ما نُنشِئ خوادم باستخدام Node.js مثل خادم HTTP التالي: const express = require('express') const app = express() app.get('/', (req, res) => { res.send('Hi!') }) app.listen(3000, () => console.log('Server ready')) لن ينتهي هذا البرنامج أبدًا، فإذا استدعيت التابع process.exit()، فستنتهي جميع الطلبيات قيد التنفيذ أو المعلَّقة، وهذا ليس أمرًا جميلًا صدقًا، حيث ستحتاج في هذه الحالة إلى إرسال الإشارة SIGTERM إلى الأمر، وسنتعامل مع الأمر باستخدام معالج إشارة العملية process signal handler. const express = require('express') const app = express() app.get('/', (req, res) => { res.send('Hi!') }) app.listen(3000, () => console.log('Server ready')) process.on('SIGTERM', () => { app.close(() => { console.log('Process terminated') }) }) قد تتساءل ما هي الإشارات؟ الإشارات هي نظام تواصل داخلي في معيار POSIX، وهو إشعار يُرسَل إلى العملية لإخبارها أنّ حدثًا ما قد حدث، فالإشارة SIGKILL هي الإشارة التي تخبر العملية بالتوقف عن العمل فورًا، وهي تعمل مثل process.exit()؛ أما الإشارة SIGTERM فهي الإشارة التي تخبر العملية بأن تنتهي بلطف، وهي الإشارة التي تُرسَل من مدراء العمليات في أنظمة التشغيل، كما يمكنك إرسال هذه الإشارة من داخل البرنامج باستخدام دالة تابع آخر: process.kill(process.pid, 'SIGTERM') أو من برنامج Node.js آخر، أو من أيّ برنامج يعمل على النظام يعرف مُعرِّف العملية PID الخاص بالعملية التي تريد إنهاءها. متغيرات البيئة: الفرق بين التطوير والإنتاج متغير البيئة هو اصطلاح مُستخدَم على نطاق واسع في المكتبات الخارجية أيضًا، وسنتعلم كيفية قراءة واستخدام متغيرات البيئة في برنامج Node.js، حيث توفِّر الوحدة الأساسية process في Node.js الخاصية env التي تحتوي على جميع متغيرات البيئة المضبوطة في لحظة تشغيل العملية، وفيما يلي مثال يصل إلى متغير البيئة NODE_ENV المضبوط إلى القيمة development افتراضيًا: process.env.NODE_ENV // "development" ضبطه إلى القيمة production قبل تشغيل السكربت سيخبر Node.js أنّ هذه بيئة إنتاجية وليست تطويرية، كما يمكنك بالطريقة نفسها الوصول إلى أيّ متغيرات بيئة خاصة تضبطها. يمكن أن يكون لديك إعدادات مختلفة لبيئات الإنتاج والتطوير، حيث يفترض Node أنه يعمل دائمًا في بيئة تطوير، ولكن يمكنك إعلام Node.js بأنك تعمل في بيئة إنتاج من خلال ضبط متغير البيئة NODE_ENV=production عن طريق تنفيذ الأمر التالي: export NODE_ENV=production لكن يُفضَّل في الصدَفة shell وضعه في ملف إعداد الصدَفة مثل .bash_profile مع صدفة Bash، وذلك لأن الإعداد بخلاف ذلك لا يستمر في حالة إعادة تشغيل النظام، كما يمكنك تطبيق متغير البيئة عن طريق وضعه في بداية أمر تهيئة تطبيقك كما يلي: NODE_ENV=production node app.js يضمن ضبط البيئة على القيمة production ما يلي: الاحتفاظ بتسجيل الدخول إلى المستوى الأدنى الأساسي. إجراء مزيد من مستويات التخبئة أو التخزين المؤقت caching لتحسين الأداء. تطبِّق مكتبة القوالب Pug التي يستخدمها إطار عمل Express على سبيل المثال عملية التصريف في وضع تنقيح الأخطاء، إذا لم يُضبَط المتغير NODE_ENV على القيمة production، حيث تُصرَّف عروض Express في كل طلب في وضع التطوير، بينما تُخزَّن مؤقتًا في وضع الإنتاج، كما يوفِّر إطار Express خطّافات إعداد configuration hooks خاصة بالبيئة تُستدعَى تلقائيًا بناءً على قيمة المتغير NODE_ENV: app.configure('development', () => { //... }) app.configure('production', () => { //... }) app.configure('production', 'staging', () => { //... }) يمكنك استخدام ذلك مثلًا لضبط معالجات أخطاء مختلفة في وضع مختلف كما يلي: app.configure('development', () => { app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); }) app.configure('production', () => { app.use(express.errorHandler()) }) استضافة مشاريع Node.js يمكن استضافة تطبيقات Node.js في أماكن عديدة اعتمادًا على احتياجاتك، وسنذكر لك قائمةً بالخيارات المتاحة أمامك، وهي قائمة غير شاملة بالخيارات التي يمكنك استخدامها لنشر تطبيقك وجعله متاحًا للعامة، وسنرتِّبها من الأبسط والأقل مزايا إلى الأعقد والأقوى: أسهل الخيارات على الإطلاق: نفق محلي local tunnel. نشر التطبيقات دون أي ضبط. Glitch. Codepen. الخيارات عديمة الخوادم Serverless. المنصة على أساس خدمة PAAS. Zeit Now. Nanobox. Heroku. Microsoft Azure. Google Cloud Platform. خادم خاص افتراضي Virtual Private Server أي VPS. خادم حقيقي Bare metal. نفق محلي يمكنك نشر تطبيقك وتُخديم الطلبات من حاسوبك باستخدام نفق محلي local tunnel حتى إذا كان لديك عنوان IP ديناميكي، أو كنت تحت NAT، فهذا الخيار مناسب لإجراء بعض الاختبارات السريعة، أو تجربة المنتج أو مشاركة التطبيق مع مجموعة صغيرة من الأشخاص، وهنالك أداة رائعة لذلك متاحة لجميع المنصات اسمها ngrok، فكل ما عليك فعله لاستعمالها هو كتابة ngrok PORT، إذ إنَّ PORT هو المنفذ الذي تريد نشره على الإنترنت، وستحصل على نطاق من ngrok.io، لكن سيسمح لك الاشتراك المدفوع بالحصول على عنوان URL مخصص إضافةً إلى خيارات حماية إضافة (تذكَّر أنك تفتح جهازك إلى الإنترنت)، وهنالك خدمة أخرى يمكنك استخدامها وهي github.com/localtunnel/localtunnel. نشر التطبيقات دون أي ضبط هناك خيارات متاحة لنشر تطبيقات Node.js دون أيّ ضبط يُذكر، وسنذكر من هذه الخيارات منصة Glitch ومنصة Codepen. Glitch يُعَدّ Glitch بيئةً تسمح لك ببناء تطبيقاتك بسرعة كبيرة، ورؤيتها حيةً على النطاق الفرعي الخاص بك على glitch.com، فلا يمكنك حاليًا الحصول على نطاق مخصص وهنالك بعض المحدوديات، لكن ستبقى مع ذلك بيئةً رائعةً؛ فهي تحتوي على كامل ميزات Node.js وCDN ومكان تخزين آمن للمعلومات الحساسة، بالإضافة إلى الاستيراد والتصدير من GitHub، كما أنّ هذه الخدمة موفَّرة من الشركة التي تقف خلف FogBugz وTrello والمشاركين في إنشاء StackOverflow. Codepen منصة Codepen رائعة، فهي تسمح لك بإنشاء مشروع متعدد الملفات ونشره بنطاق مخصص. الخيارات عديمة الخوادم Serverless إحدى الطرائق لنشر تطبيقك وعدم الحاجة إلى خادم لإدارته هي استخدام إحدى الخيارات عديمة الخوادم Serverless التي هي منهجية لنشر تطبيقاتك على أساس وظائف functions، وهي ترد على نقطة نهاية شبكية network endpoint، وهذا يسمى أيضًا FAAS أي الوظيفة على أساس خدمة Function As A Service، ومن الخيارات الشائعة جدًا هي: Serverless Framework Standard Library يوفِّر كلا الخيارين طبقة تجريدية abstraction layer لنشر التطبيقات على حلول مثل AWS Lambda وغيرها من حلول FAAS المبنية على Azure أو Google Cloud. المنصة على أساس خدمة PAAS تُعَدّ PASS اختصارًا لـ Platform AS A Service أي المنصة على أساس خدمة، وتحمل عنك هذه المنصات عناء التفكير في كثير من الأمور عند نشر تطبيقك. Zeit Now يُعَدّ Zeit خيارًا مثيرًا للاهتمام، فعندما تكتب الأمر now في الطرفية، فسيتولى أمر نشر تطبيقك كله؛ وهنالك نسخة مجانية مع محدوديات ونسخة مدفوعة بميزات أكثر، حيث ستنسى أنّ هنالك خادم وكل ما عليك فعله هو نشر التطبيق. Nanobox سنحيلك إلى موقع Nanobox لتأخذ فكرةً عنه nanobox.io. Heroku يُعَدّ Heroku منصةً رائعةً، وهنالك سلسلة فيديوهات عن كيفية نشر التطبيقات عبر Heroku منها فيديو نشر تطبيق React.js ذو واجهات خلفية Node.js على منصة Heroku. Microsoft Azure خدمة Azure توفرها Microsoft Cloud، وهنالك مقالة أجنبية تفصيلية عن إنشاء تطبيق Node.js في Azure. Google Cloud Platform تُعَدّ منصة Google Cloud خيارًا رائعًا لتنظيم تطبيقاتك، ولديهم توثيق جيد عن Node.js. خادم خاص افتراضي VPS ستجد في هذا القسم الخيارات الشائعة التي قد تعرفها من قبل، وهي مرتبة من أكثرها سهولةً للمستخدِم: DigitalOcean Linode Amazon Web Services، ونذكر خصوصًا خدمة Amazon Elastic Beanstalk فهي تسهل بعضًا من تعقيدات AWS. ولمّا كانت هذه الخدمات توفِّر لك خادم لينكس فارغ يمكنك العمل عليه، فلن نوصي بمقالةٍ أو دليلٍ محدد لهذه الخدمات؛ وهذه ليست جميع الشركات التي توفر خدمات VPS، لكنها هي التي استعملناها سابقًا وننصح بها. خادم حقيقي يوجد خيار آخر هو خادم حقيقي bare metal، بحيث تثبت عليه توزيعة لينكس وتصله بالإنترنت أو يمكنك استئجار واحد شهريًا، كما في خدمة Vultr Bare Metal. ترجمة -وبتصرّف- للفصلين Introduction و Basics من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: استخدام الوضع التفاعلي والتعامل مع سطر الأوامر في Node.js بناء تطبيق Node.js باستخدام Docker إعداد تطبيق node.js لسير عمل يعتمد على الحاويات باستخدام Docker Compose تأمين تطبيق Node.js يعمل على الحاويات باستخدام Nginx و Let’s Encrypt و Compose Docker نشر تطبيق Node.js على الويب: خدمة هيروكو (Heroku) مثالًا
-
أهلًا بك، إذا كانت ميولك في تطوير الويب فلا تجعل اختصاصك يشكل عائقًا أمامك، بل عليك أن تستفيد منه. ما أنصحك به هو أمران مهمان، الأول ألا تتشتت بين المجالات فتارةً تدخل إلى تطوير الويب ثم تطوير الجوال ثم تطوير الألعاب، بل حدد ما تريد فعله على المدى المتوسط وأقدم على ذلك. الأمر الثاني هو أنك تجيد الشبكات بحكم تخصصك، فهذا يعطيك ميزةً عن مطوري الويب الآخرين، يمكنك أن تستفيد من ذلك في العمل مستقبلًا بدمج خبرتك في المجالين معًا. أرجو لك كل التوفيق.
- 37 تعليقات
-
- 1
-
- تطوير البرمجيات
- لغات البرمجة
-
(و 1 أكثر)
موسوم في:
-
سنناقش في هذا الدرس حالة المكوِّنات (component state). ما هي حالة المكون (Component State)؟ أغلبية المكونات تأخذ الخاصيات props وتصيّر العنصر، لكن تتيح المكونات الحالة أيضًا، والتي يمكن أن تستخدم لتخزين معلومات حول المكوِّن التي يمكن أن تتغير مع مرور الوقت. وعادةً يأتي التغيير نتيجةً لأحداث المستخدم أو أحداث النظام (مثل رد لمدخلات المستخدم، أو طلبية للخادم بعد مرور فترة زمنية معيّنة). بعض الأمور التي يجب أخذها بالحسبان حول حالة مكوِّنات React: إذا كان للمكوِّن حالة فيمكن ضبط الحالة الافتراضية this.setState() في الدالة البانية. تغيرات الحالة هي ما تجعلنا نعيد تصيير المكوِّن وجميع المكونات الفرعية التابعة له. يمكنك إعلام المكوِّن بتغيّر الحالة باستخدام this.setState() لضبط حالةٍ جديدة. يمكن أن تدمج تغيرات الحالة البيانات الجديدة مع البيانات القديمة التي ما تزال محتواةً في الحالة (أي this.state). عند تغيير الحالة، فستجري عملية إعادة التصيير داخليًا، ولا يفترض بك استدعاء this.render() مباشرةً أبدًا. يجب أن يحتوي كائن الحالة على القدر الأدنى من البيانات اللازم لواجهة المستخدم؛ فلا تضع البيانات المحسوبة أو مكونات React الأخرى أو الخاصيات props في كائن الحالة. العمل مع حالة المكونات يتطلب التعامل مع حالة المكونات عادةً ضبط الحالة الافتراضية، والوصول إلى الحالة الحالية، وتحديث الحالة. سننشِئ في المثال الآتي المكوِّن الذي يبيّن استخدام this.state.[STATE] وthis.setState(). إذا ضغطتَ على المكوِّن في متصفح الويب (أي الوجه المبتسم) فستجد أنَّ المكوِّن سيبدِّل بين الحالات المتاحة (أي الأوجه المبتسمة)، وبالتالي تكون هنالك ثلاثة حالات ممكنة للمكوِّن وهي مرتبطة بالواجهة الرسومية ومعتمدة على نقرات المستخدم في واجهة المستخدم: class MoodComponent extends React.Component { state = {mood: ':|'}; constructor(props){ super(props); this.changeMood = this.changeMood.bind(this) } changeMood(event,a){ const moods = [':)',':|',':(']; const current = moods.indexOf(event.target.textContent); this.setState({mood: current === 2 ? moods[0] : moods[current+1]}); } render() { return ( <span style={{fontSize:'60',border:'1px solid #333',cursor:'pointer'}} onClick={this.changeMood}> {this.state.mood} </span> ) } }; ReactDOM.render(< MoodComponent />, app); لاحظ أنَّ للمكوِّن حالةً افتراضيةً هي :|، وهي مضبوطة باستخدام state = {mood: ':|'}; وهي تستخدم في المكوِّن عند تصييره لأول مرة عبر {this.state.mood}. أضفنا مستمعًا للأحداث لتغيير الحالة، وهي هذه الحالة سيؤدي حدث النقر (onClick) على عقدة إلى استدعاء الدالة changeMood، وداخل هذه الدالة سنستخدم this.setState() للتبديل إلى الحالة التالية اعتمادًا على قيمة الحالة الحالية. بعد إجراء هذا التحديث (لاحظ أنَّ setState() ستدمج التعديلات) فسيعاد تصيير العنصر وستتغير واجهة المستخدم. بعض الأمور التي علينا إبقاؤها في ذهننا عند الحديث عن حالة مكوِّنات React: إذا كان للمكوِّن حالة فيمكن ضبط الحالة الافتراضية الخاصية state. تغيرات الحالة هي ما تجعلنا نعيد تصيير المكوِّن وجميع المكونات الفرعية التابعة له. يمكنك إعلام المكوِّن بتغيّر الحالة باستخدام this.setState() لضبط حالةٍ جديدة. هنالك طرائق أخرى (مثل forceUpdate()) ولكن لا يجدر بناء استخدامها إلا إذا أردنا دمج React مع مكتباتٍ من طرف ثالث. يمكن أن تدمج تغيرات الحالة البيانات الجديدة مع البيانات القديمة التي ما تزال محتواةً في الحالة (أي this.state). لكن هذا دمجٌ أو تحديثٌ سطحي، فلن يجرى عملية دمج عميقة. عند تغيير الحالة، فستجري عملية إعادة التصيير داخليًا، ولا يفترض بك استدعاء this.render() مباشرةً أبدًا. يجب أن يحتوي كائن الحالة على القدر الأدنى من البيانات اللازم لواجهة المستخدم؛ فلا تضع البيانات المحسوبة أو مكونات React الأخرى أو الخاصيات props في كائن الحالة. الفروقات بين الحالة والخاصيات props هنالك أرضيةٌ مشتركة بين الحالة state والخاصيات props: كلاهما كائنات JavaScript عادية. يمكن لكليهما أن يمتلك قيمًا افتراضية. يمكن الوصول إليها باستخدام this.props أو this.state، لكن لا يجوز أن نضبط قيمهما بهذه الطريقة، إذ سيكون كلاهما للقراءة فقط عند استخدام this. لكنهما يستخدمان لأغراضٍ مختلفة وبطرائق مختلفة. الخاصيات props: تُمرَّر الخاصيات إلى المكوِّنات من البنية الأعلى منها، سواءً كانت من مكوِّنٍ أب أو بداية المجال حيث صُيَّرت React من الأساس. الغرض من الخاصيات هو تمرير قيم الضبط إلى المكوِّن. تخيل أنها كالوسائط المُمرَّرة إلى دالة (وإن لم تكن تستعمل صيغة JSX فهذا ما تفعله تحديدًا). الخاصيات غير قابلة للتعديل في المكوِّن الذي يستقبلها، أي لا يمكننا تعديل الخاصيات المُمرَّرة إلى المكوِّن من داخل المكوِّن نفسه. الحالة (state): الحالة هي تمثيلٌ للبيانات الذي سيرتبط في مرحلةٍ ما مع الواجهة الرسومية. يجب أن تبدأ الحالة دومًا بقيمةٍ افتراضية، ثم ستُعدّل الحالة داخليًا في المكوِّن باستخدام setState(). يمكن تعديل الحالة باستخدام المكوِّن الذي يحتوي عليها فقط، أي أنها خاصة. لا يفترض تعديل حالة المكوِّنات الأبناء، ويجب ألا يشارك المكوِّن حالةً قابلةً للتعديل. يجب أن يحتوي كائن الحالة على القدر الأدنى من البيانات اللازم لواجهة المستخدم؛ فلا تضع البيانات المحسوبة أو مكونات React الأخرى أو الخاصيات props في كائن الحالة. إنشاء مكونات عديمة الحالة (Stateless Components) عندما يكون المكون نتيجةً لاستخدام الخاصيات props فقط دون وجود حالة state، فيمكن كتابة المكوِّن على شكل دالة صرفة مما يجعلنا نتفادى إنشاء نسخة من مكوِّن React. ففي المثال الآتي سيكون MyComponent نتيجةً لاستدعاء دالة ناتجة عن React.createElement(). var MyComponent = function(props){ return <div>Hello {props.name}</div>; }; ReactDOM.render(<MyComponent name="doug" />, app); إذا ألقينا نظرةً على شيفرة JavaScript الناتجة عن عملية تحويل JSX، فستبدو الأمور جليةً وواضحةً لنا: var MyComponent = function MyComponent(props) { return React.createElement( "div", null, "Hello ", props.name ); }; ReactDOM.render(React.createElement(MyComponent, { name: "doug" }), app); إنشاء مكوِّن React دون اشتقاق الصنف React.Component يشار إليه عادةً بمكوِّن عديم الحالة. لا يمكن تمرير خيارات الضبط إلى المكونات عديمة الحالة (مثل render أو componentWillUnmount …إلخ.)؛ لكن يمكن ضبط .propTypes و .defaultProps على الدالة. الشيفرة الآتية توضِّح مكوِّنًا عديم الحالة يستخدم .propTypes و .defaultProps: import PropTypes from 'prop-types'; var MyComponent = function(props){ return <div>Hello {props.name}</div>; }; MyComponent.defaultProps = {name:"John Doe"}; MyComponent.propTyes = {name: PropTypes.string}; ReactDOM.render(<MyComponent />, app); ملاحظات حاول أن تجعل أكبر عدد من المكونات عديمَ الحالة. ترجمة -وبتصرف- للفصل React Component State من كتاب React Enlightenment
-
سنناقش في هذا الدرس استخدام خاصيات مكوِّنات React والتي تعرف بالخاصيات props. ما هي خاصيات المكونات؟ table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } أسهل طريقة لشرح خاصيات المكوِّنات هي القول أنَّها تسلك سلوك خاصيات HTML. بعبارةٍ أخرى، توفِّر الخاصيات خيارات الضبط للمكوِّن. فمثلًا، الشيفرة الآتية فيها المكوِّن Badge الذي يتوقع إرسال الخاصية name عند تهيئة المكوِّن: class Badge extends React.Component { render() { return <div>{this.props.name}</div>; } }; class BadgeList extends React.Component { render() { return (<div> <Badge name="Bill" /> <Badge name="Tom" /> </div>); } }; ReactDOM.render(<BadgeList />, document.getElementById('app')); داخل دالة التصيير للمكوِّن وعندما نستخدم المكوِّن ستُضاف الخاصية name إلى المكوِّن بنفس الطريقة التي نضيف فيها خاصية HTML إلى عنصر HTML (أي)؛ ثم ستستخدم الخاصية name من المكوِّن Badge (عبر this.props.name) كعقدة نصية للعقدة التي ستصيَّر عبر المكون Badge. هذا شبيهٌ بطريقة أخذ العنصر في HTML الخاصية value التي ستُستخدَم قيمتها لعرض نص داخل حقل الإدخال. طريقة أخرى للتفكير في خاصيات المكوِّنات هي تخيلها كأنها قيم لخيارات الضبط المُرسَلة إلى المكوِّن. فإذا نظرتَ إلى نسخة لا تحتوي على صيغة JSX من المثال السابق فسيبدو لك جليًا أنَّ خاصيات المكوِّن ما هي إلا كائنٌ يُمرَّر إلى الخاصية createElement() (أي React.createElement(Badge, { name: "Bill" })): class Badge extends React.Component { render() { return React.createElement( "div", null, // null لم تُعرَّف خاصيات لذا ستكون القيمة this.props.name // كقيمة نصية this.prop.name استخدام ); } }; class BadgeList extends React.Component { render() { return React.createElement( "div", null, React.createElement(Badge, { name: "Bill" }), React.createElement(Badge, { name: "Tom" }) ); } }; ReactDOM.render(React.createElement(BadgeList, null), document.getElementById('app')); هذا شبيهٌ بطريقة ضبط الخاصيات مباشرةً على عقد React. لكن عند تمرير تعريف المكوِّن Badge إلى الدالة createElement() بدلًا من قعدة، فستصبح الخاصيات props متاحةً على المكوِّن نفسه (أي this.props.name). تُمكِّننا خاصيات المكوِّنات من إعادة استخدام المكوِّن مع أي اسم. في الشيفرة التي ألقينا إليها نظرةً في هذا القسم، لاحظنا أنَّ المكوِّن BadgeList يستخدم مكونَي Badge مع كائن this.props خاصٌ بكلٍ واحدٍ منها. يمكننا التأكد من ذلك بعرض قيمة this.props عندما يُهيَّئ المكوِّن Badge: class Badge extends React.Component { render() { return <div>{this.props.name}{console.log(this.props)}</div>; } }; class BadgeList extends React.Component { render() { return (<div> <Badge name="Bill" /> <Badge name="Tom" /> </div>); } }; ReactDOM.render(<BadgeList />, document.getElementById('app')); نلاحظ أنَّ كل نسخة من مكوِّنات React تملك نسخةً خاصةً بها من خاصيةٍ اسمها props التي تكون كائن JavaScript فارغ، ثم سيُملَأ هذا الكائن عبر المكوِّن الأب باستخدام أي قيمة أو مرجعية في JavaScript، ثم ستُستخدَم هذه القيمة من المكوِّن أو تُمرَّر إلى المكونات الأبناء. ملاحظات في البيئات التي تستخدم ES5، لن نتمكن من تعديل الخاصية this.props لأنها كائنٌ مجمَّد (أي Object.isFrozen(this.props) === true;). يمكنك أن تعدّ this.props على أنها كائنٌ للقراءة فقط. إرسال الخاصيات props إلى مكوِّن تُرسَل الخاصيات إلى المكوِّن عند إضافة قيم شبيهة بخاصيات HTML إلى المكوِّن عند استخدمه وليس عند تعريفه، فمثلًا، سنجد في الشيفرة الآتية أنَّ المكوِّن Badge قد عُرِّفَ أولًا، ثم أرسلنا خاصيةً له وهي name="Bill" أثناء استخدامه (أي عند تصيير ``): class Badge extends React.Component { render() { return <div>{this.props.name}</div>; } }; ReactDOM.render(<Badge name="Bill" />, document.getElementById('app')); أبقِ في ذهنك أنَّ بإمكاننا إرسال خاصية إلى المكوِّن في أي مكان يمكن أن يُستخدَم المكوِّن فيه. فعلى سبيل المثال، يبيّن المثال من القسم السابقة استخدام المكوِّن Badge والخاصية name ضمن المكوِّن BadgeList: class Badge extends React.Component { render() { return <div>{this.props.name}</div>; } }; class BadgeList extends React.Component { render() { return (<div> <Badge name="Bill" /> <Badge name="Tom" /> </div> ); } }; ReactDOM.render(<BadgeList />, document.getElementById('app')); ملاحظات يجب أن تُعدّ خاصيات المكوِّن غيرُ قابلةٍ للتعديل، ويمكن عدم تعديل الخاصيات المُرسَلة من مكوِّن آخر. فلو احتجتَ إلى تعديل قيمة خاصيات أحد المكوِّنات ثم إعادة تصييره، فلا تضبط الخاصيات باستخدام this.props.[PROP] = [NEW PROP]. الحصول على خاصيات المكوِّن كما ناقشنا في الدرس السابق، يمكن الوصول إلى إلى نسخة المكوِّن من أيٍّ من خيارات الضبط التي تستعمل دالةً عبر الكلمة المحجوزة this. ففي المثال الآتي استخدمنا الكلمة المحجوزة this للوصول إلى خاصيات props المكوِّن Badge من خيار الضبط render بكتابة this.props.name: class Badge extends React.Component { render() { return <div>{this.props.name}</div>; } }; ReactDOM.render(<Badge name="Bill" />, document.getElementById('app')); ليس من الصعب معرفة ما يحدث إذا ألقينا نظرةً على شيفرة JavaScript المحوَّلة من JSX: class Badge extends React.Component { render() { return React.createElement( "div", null, this.props.name ); } }; ReactDOM.render(React.createElement(Badge, { name: "Bill" }), document.getElementById('app')); أُرسِل الكائن { name: "Bill" } إلى الدالة createElement() إضافةً إلى مرجعيةٍ إلى المكوِّن Badge. القيمة { name: "Bill" } ستُضبَط كخاصية للمكوِّن قابلةٌ للوصول من الكائن props، أي أنَّ this.props.name === "Bill". ملاحظات تذكَّر أنَّ this.props للقراءة فقط، ولا يجوز ضبط الخاصيات باستخدام this.props.PROP = 'foo'. ضبط قيم افتراضية لخاصيات المكوِّن يمكن ضبط الخاصيات الافتراضية عند تعريف المكوِّن باستخدام خيار الضبط getDefaultProps. المثال الآتي يبيّن كيف عرَّفنا خيار ضبط افتراضي للمكوِّن Badge للخاصية name: class Badge extends React.Component { static defaultProps = { name:'John Doe' } render() { return <div>{this.props.name}</div>; } }; class BadgeList extends React.Component { render() { return (<div> <Badge /> <Badge name="Tom Willy" /> </div>); } }; ReactDOM.render(<BadgeList />, document.getElementById('app')); ستُضبَط الخاصيات الافتراضية على الكائن this.props إذا لم تُرسَل خاصيات إلى المكوِّن. يمكنك التحقق من ذلك بملاحظة أنَّ المكوِّن Badge الذي لم تُضبَط الخاصية name فيه ستأخذ القيمة الافتراضية 'John Doe'. خاصيات المكونات هي أكثر من مجرد سلاسل نصية قبل أن نلقي نظرةً على التحقق من الخاصيات، علينا أن نستوعب أولًا أنَّ خاصيات المكونات يمكن أن تكون أي قيمة صالحة في JavaScript. في المثال أدناه، سنضبط عدِّة خاصيات افتراضية تحتوي على مختلف قيم JavaScript: class MyComponent extends React.Component { static defaultProps = { propArray: [], propBool: false, propFunc: function(){}, propNumber: 5, propObject: {}, propString: 'string' } render() { return (<div> propArray: {this.props.propArray.toString()} <br /><br /> propFunc returns: {this.props.propFunc()} </div>) ; } }; ReactDOM.render(<MyComponent propArray={[1,2,3]} propFunc={function(){return 2;}} />, document.getElementById('app')); لاحظ كيف أعيدت الكتابة على الخاصيتين propArray و propObject مع قيم جديدة عند إنشاء نسخة من المكوِّن MyComponent. الفكرة الرئيسية من هذا القسم هو توضيح أنَّنا لسنا محدودين بالقيم النصية عند تمرير قيم للخاصيات. التحقق من خاصيات المكوِّنات لاستخدامٍ سليمٍ للخاصيات ضمن المكوِّنات، يجب أن نتحقق من قيمتها عند إنشاء نسخ المكوِّنات. عند تعريف خيار الضبط propTypes يمكننا أن نضبط كيف يجب أن تكون الخاصيات وكيف نتحقق منها. سنتحقق في المثال أدناه لنرى إن كانت الخاصيتان propArray و propObject من نوع البيانات الصحيح وسنرسلها إلى المكوِّن عند تهيئته: import PropTypes from 'prop-types'; class MyComponent extends React.Component { static propTypes = { propArray: PropTypes.array.isRequired, propFunc: PropTypes.func.isRequired, } render() { return (<div> propArray: {this.props.propArray.toString()} <br /><br /> propFunc returns: {this.props.propFunc()} </div>); } }; // لهذا المكوِّن خاصياتٌ خطأ ReactDOM.render(<MyComponent propArray={{test:'test'}} />, document.getElementById('app')); // لهذا المكوِّن خاصياتٌ صحيحة // ReactDOM.render(<MyComponent propArray={[1,2]} propFunc={function(){return 3;}} />, document.getElementById('app')); لم نرسل الخاصيات الصحيحة المُحدَّدة عبر propTypes لتوضيح أنَّ فعل ذلك سيؤدي إلى حدوث خطأ. ستؤدي الشيفرة السابقة إلى ظهور رسالة الخطأ الآتية: Warning: Failed propType: Invalid prop `propArray` of type `object` supplied to `MyComponent`, expected `array` Warning: Failed propType: Required prop `propFunc` was not specified in `MyComponent`. Uncaught TypeError: this.props.propFunc is not a function توفِّر React عددًا من المتحققات الداخلية (مثل PropTypes[VALIDATOR]) والتي سأشرحها بإيجاز فيما يلي، إضافةً إلى إمكانية إنشاء متحققات مخصصة. (انتقلت إلى مكتبة مختلفة*) المتحققات الأساسية من الأنواع React.PropTypes.string إذا اُستخدِمَت خاصيةٌ، فتحقق أنها سلسلة نصية React.PropTypes.bool إذا اُستخدِمَت خاصيةٌ، فتحقق أنها قيمة منطقية React.PropTypes.func إذا اُستخدِمَت خاصيةٌ، فتحقق أنها دالة React.PropTypes.number إذا اُستخدِمَت خاصيةٌ، فتحقق أنها عدد React.PropTypes.object إذا اُستخدِمَت خاصيةٌ، فتحقق أنها كائن React.PropTypes.array إذا اُستخدِمَت خاصيةٌ، فتحقق أنها مصفوفة React.PropTypes.any إذا اُستخدِمَت خاصيةٌ، فتحقق أنها من أي نوع من الأنواع متحققات القيم المطلوبة React.PropTypes.[TYPE].isRequired إضافة .isRequired إلى أي نوع من المتحققات سيؤدي إلى جعل الخاصية مطلوبةً (مثال ذلك propTypes:{propFunc:React.PropTypes.func.isRequired}) متحققات العناصر React.PropTypes.element الخاصية هي عنصر React. React.PropTypes.node أي شيء يمكن تصييره: الأرقام، أو السلاسل النصية، أو العناصر، أو مصفوفة تحتوي هذه الأنواع المتحققات المتعددة React.PropTypes.oneOf(['Mon','Fri']) الخاصية هي أحد أنواع القيم المُحدَّدة React.PropTypes.oneOfType([React.PropTypes.string,React.PropTypes.number]) الخاصية هي كائن يمكن أن يكون أحد أنواع القيم المُحدَّدة متحققات المصفوفات والكائنات React.PropTypes.arrayOf(React.PropTypes.number) الخاصية هي مصفوفة تحتوي على نوع واحد من القيم React.PropTypes.objectOf(React.PropTypes.number) هي كائن يحتوي على أحد أنواع القيم React.PropTypes.instanceOf(People) هي كائن يكون نسخةً من دالةٍ بانية معينة (كما في الكلمة المحجوزة instanceof) React.PropTypes.shape({color:React.PropTypes.string,size: React.PropTypes.number}) هي كائن يحتوي على خاصيات من أنواعٍ معينة المتحققات المخصصة function(props, propName, componentName){} توفير دالة خاصة بك للتحقق ترجمة -وبتصرف- للفصل React Component Properties من كتاب React Enlightenment
-
يبيّن هذا الدرس كيفية استخدام عقد React لإنشاء مكوِّنات React أساسية. ما هي مكونات React؟ سنشرح في هذا القسم طبيعة مكونات React ونغطي بعض التفاصيل التي تتعلق بإنشاء مكونات React. عادةً تكون واجهة المستخدم (تدعى بالشجرة tree) مقسمةً إلى أقسامٍ منطقية تسمى بالفروع (branches)، وتصبح هذه الشجرة نقطة البداية للمكوِّن وكل قسم في واجهة المستخدم يصبح مكونًا فرعيًا التي يمكن بدورها أن تُقسَّم إلى مكونات فرعية؛ ويؤدي ذلك إلى تنظيم واجهة المستخدم ويسمح أيضًا لتغيرات البيانات والحالات أن تمر من الشجرة إلى الفروع ومنها إلى الفروع الداخلية. إذا كان الشرح السابق لمكونات React مبهمًا فأقترح عليك أن تعاين واجهة المستخدم لأي تطبيق وتحاول ذهنيًا تقسيمها إلى أقسام منطقية. من المحتمل تسمية هذه الأقسام بالمكونات. مكوِّنات React هي تجريد برمجي (programmatic abstraction) لعناصر واجهة المستخدم وللأحداث ولتغيرات الحالة ولتغيرات DOM والغرض منها هو تسهيل إنشاء هذه الأقسام والأقسام الفرعية. فعلى سبيل المثال، الكثير من واجهات المستخدم للتطبيقات تحتوي على مكوِّن للتخطيط (layout) كأحد المكونات الرئيسية في واجهة المستخدم، وهذا المكون يحتوي بدوره على على مكونات فرعية مثل مكوِّن البحث ومكوِّن قائمة التنقل. ويمكن تقسيم مكوِّن البحث مثلًا إلى مكونات فرعية، فيمكن أن يكون حقل الإدخال منفصلًا عن زر البحث. وكما ترى، يمكن بسهولة أن تصبح واجهة المستخدم مكونةً من شجرةٍ من المكونات، وتُنشَأ العديد من واجهات المستخدم للتطبيقات حاليًا باستخدام مكونات ذاتُ غرضٍ وحيد. توفِّر React الطرائق اللازمة لإنشاء هذه المكونات عبر React.Component إذا كنتَ تستخدم الأصناف في ES6. تقبل الدالة createReactClass() من الحزمة create-react-class كائن ضبط وتُعيد نسخةً (instance) من مكوِّن React. يمكننا أن نقول أنَّ مكوِّن React هو أي جزء من الواجهة البرمجية التي تحتوي على عقد React (عبر React.createElement() أو صيغة JSX). أمضينا وقتًا طويلًا في بداية هذا الكتاب ونحن نتحدث عن عقد React، لذا أتوقع أنَّ محتويات مكوِّن React أصبح واضحةً وجليةً لك. كل ذلك يبدو سهلًا وبسيطًا حتى ندرك أنَّ مكوِّنات React يمكنها أن تحتوي على مكونات React فرعية، وهذا لا يختلف عن فكرة أنَّ عقد React يمكنها أن تحتوي على عقد React أخرى في شجرة DOM الافتراضية. قد يؤلمك رأسك من الكلام السابق، لكن إذا فكرتَ مليًا فستجد أنَّ المكوِّن يحيط نفسه بمجموعة منطقية من الفروع في شجرة من العقد. وبهذا يمكنك تعريف واجهة المستخدم كلها باستخدام مكوِّنات React وستكون النتيجة النهائية هي شجرة من عقد React التي يمكن تحويلها بسهولة إلى مستند HTML (الذي يتكون من عقد DOM التي تؤلِّف واجهة المستخدم). إنشاء مكونات React يمكن إنشاء مكوِّن React الذي قد تكون له حالة state باستدعاء الدالة createReactClass() من الحزمة create-react-class (أو اشتقاق React.Component إذا كنتَ تستخدم الأصناف في ES6). هذه الدالة (أو الدالة البانية) تأخذ وسيطًا واحدًا هو كائن يُستخدَم لتوفير تفاصيل المكوِّن. الجدول الآتي يبيّن خيارات الضبط المتاحة لمكوِّنات React: خيار الضبط الشرح render قيمة مطلوبة، وتُمثِّل عادةً دالةً تُعيد عقد React أو مكوِّنات React الأخرى أو القيمة null أو false. getInitialState كائن يحتوي على القيمة الابتدائية للخاصية this.state، يمكنك الحصول على قيمتها إذا كنت تستعمل الأصناف في ES6 عبر استخدام this.state في الدالة البانية. getDefaultProps كائن يحتوي على القيم التي ستُضبَط في الخاصية this.props، يمكنك ضبط قيمتها إذا كنت تستعمل الأصناف في ES6 باستخدام الخاصية static defaultProps في الصنف المشتق. propTypes كائن يحتوي على مواصفات التحقق من الخاصيات props. mixins مصفوفة من المخاليط (mixins، وهي كائنات تحتوي على توابع [methods]) التي يمكن مشاركتها بين المكوِّنات. لاحظ أن ES6 لا تدعم المخاليط، فلن تتمكن من استعمالها إذا كنتَ تستعمل الأصناف في ES6. statics كائن يحتوي على التوابع الساكنة (static methods). displayName سلسلة نصية تعطي اسمًا للمكوِّن، وتُستخدَم في رسائل التنقيح، وستُضبَط هذه القيمة تلقائيًا إذا كنّا نستعمل JSX. componentWillMount دالة رد نداء (callback function) التي ستستدعى مرةً واحدةً تلقائيًا قبل التصيير الابتدائي مباشرةً. componentDidMount دالة رد نداء (callback function) التي ستستدعى مرةً واحدةً تلقائيًا بعد التصيير الابتدائي مباشرةً. UNSAFE_componentWillReceiveProps دالة رد نداء (callback function) التي ستستدعى تلقائيًا عندما يستلم المكوِّن خاصياتٍ props جديدة. shouldComponentUpdate دالة رد نداء (callback function) التي ستستدعى تلقائيًا عندما يستلم المكوِّن خاصياتٍ props جديدة أو تغيّرت حالته state. UNSAFE_componentWillUpdate دالة رد نداء (callback function) التي ستستدعى تلقائيًا قبل أن يستلم المكوِّن خاصياتٍ props جديدة أو تغيّرت حالته state. componentDidUpdate دالة رد نداء (callback function) التي ستستدعى تلقائيًا بعد نقل التحديثات التي جرت على المكوِّن إلى DOM. componentWillUnmount دالة رد نداء (callback function) التي ستستدعى تلقائيًا قبل إزالة المكوِّن من شجرة DOM. أهم خيارات ضبط المكوِّنات هو render، وهذا الخيار مطلوبٌ وهو دالةٌ تعيد عقد ومكونات React. وجميع خيارات ضبط المكوِّنات الأخرى اختيارية. الشيفرة الآتية هي مثالٌ عن إنشاء المكوِّن Timer من عقد React باستخدام createReactClass() (سنستخدم الحزمة create-react-class في هذا المثال لترى كيف تستعمل، وسنستعمل الأصناف في ES6 لإنشاء بقية مكونات React لاحقًا). احرص على قراءة التعليقات في الشيفرة: var createReactClass = require('create-react-class'); // وتمرير كائن من خيارات الضبط Timer إنشاء المكون var Timer = createReactClass({ // this.state دالة تعيد كائنًا والذي سيصبح قيمة getInitialState: function() { return { secondsElapsed: Number(this.props.startTime) || 0 }; }, tick: function() { // تابع مخصص this.setState({ secondsElapsed: this.state.secondsElapsed + 1 }); }, componentDidMount: function() { // دالة رد النداء لدورة حياة المكوِّن this.interval = setInterval(this.tick, 1000); }, componentWillUnmount: function() { // دالة رد النداء لدورة حياة المكوِّن clearInterval(this.interval); }, render: function() { // JSX باستخدام صيغة React دالة تعيد عقد return ( <div> Seconds Elapsed: {this.state.secondsElapsed} </div> ); } }); ReactDOM.render(< Timer startTime="60" />, app); // startTime تمرير قيمة الخاصية تبدو الشيفرة السابقة طويلةً، لكن أغلبية الشيفرة السابقة تُنشِئ المكوِّن بتمرير كائن إلى الدالة createReactClass() لإنشاء مكوِّنٍ مع كائن ضبط يحتوي على خمس خاصيات (وهي getInitialState و tick و componentDidMount و componentWillUnmount و render). لاحظ أنَّ اسم المكوِّن Timer يبدأ بحرفٍ كبير، فعند إنشاء مكوِّنات React مخصصة، عليك أن تجعل أول حرفٍ من اسمها كبيرًا. أضف إلى ذلك أنَّ القيمة this ضمن خيارات الضبط تُشير إلى نسخة المكوِّن المُنشَأة. سنناقش الواجهة البرمجية للمكوِّنات بالتفصيل في نهاية هذا الدرس، لكن دعنا نتأمل خيارات الضبط المتاحة عند تعريف مكوِّن React وكيف استطعنا الإشارة إلى المكوِّن باستخدام الكلمة المحجوزة this. لاحظ أيضًا أنني أضفت في المثال السابق تابعًا خاصًا بالمكوِّن (وهو tick). بعد تركيب (mount) المكوِّن، فيمكننا استخدام الواجهة البرمجية التي تحتوي على أربعة توابع، وهي مبيّنة في الجدول الآتي: التابع مثال الشرح setState() this.setState({mykey: 'my new value'}); this.setState(function(previousState, currentProps) { return {myInteger: previousState.myInteger + 1}; }); التابع الرئيسي التي يُستخدَم لإعادة تصيير المكوِّن ومكوناته الفرعية. ForceUpdate() this.forceUpdate(function(){//callback}); استدعاء التابع forceUpdate() سيؤدي إلى استخدام التابع render() دون استدعاء shouldComponentUpdate(). أكثر تابع مستخدم في واجهة المكوِّنات البرمجية هو التابع setState()، وسيُشرَح استخدامه في درس حالة مكونات React. ملاحظات تسمى دوال رد النداء في خيارات ضبط المكوِّن (وهي componentWillUnmount و componentDidUpdate و UNSAFE_componentWillUpdate و shouldComponentUpdate و UNSAFE_componentWillReceiveProps و componentDidMount و UNSAFE_componentWillMount) بتوابع دورة الحياة (lifecycle methods) لأنَّ هذه التوابع المختلفة ستُنفَّذ في نقاط معيّنة في دورة حياة المكوِّن. الدالة createReactClass هي دالة تُنشِئ نسخةً من المكوِّنات وهي موجودة في الحزمة create-react-class. الدالة render() هي دالة صرفة، وهذا يعني أنها لا تعدّل حالة المكوِّن، وتعيد نفس النتيجة في كل مرة تستدعى فيها، ولا تقرأ أو تكتب إلى DOM أو تتفاعل مع المتصفح (باستخدام setTimout على سبيل المثال). فإذا أردت التعامل مع المتصفح، فافعل ذلك في التابع componentDidMount() أو غيره من توابع دورة الحياة. الإبقاء على الدالة render() صرفةً يجعل التصيير عمليًا ويُسهِّل التفكير في المكوِّنات. المكونات تعيد عقدة أو مكون واحد قيمة خيار الضبط render التي تُعرَّف عند إنشاء المكوِّن يجب أن تعيد مكوِّن أو عقدة React واحدة فقط. هذه العقدة أو المكون يمكن أن تحتوي على أي عدد من الأبناء. عقدة البداية في الشيفرة الآتية يمكن أن تحتوي هذه العقدة على أي عدد من العقد الأبناء: class MyComponent extends React.Component { render() { return <reactNode> <span>test</span> <span>test</span> </reactNode>; } }; ReactDOM.render(<MyComponent />, app); لاحظ أننا تستطيع إعادة عقد React بجعلها على عدّة أسطر، ويمكنك أن تحيط القيمة المعادة بقوسين هلاليين (). لاحظ كيف أعدنا المكوِّن MyComponent المُعرَّف باستخدام JSX بوضعه بين قوسين: class MyComponent extends React.Component { render() { return ( <reactNode> <span>test</span> <span>test</span> </reactNode> ); } }; ReactDOM.render(<MyComponent />, app); سيحدث خطأ إذا حاولنا إعادة أكثر من عقدة React واحدة. يمكنك أن تفكر فيها مليًا، فالخطأ يحدث لأنَّ من غير الممكن إعادة دالتَي React.createElement() باستخدام JavaScript. class MyComponent extends React.Component { render() { return ( <span>test</span> <span>test</span> ); } }; ReactDOM.render(<MyComponent />, app); ستؤدي الشيفرة السابقة إلى حدوث الخطأ الآتي: Syntax error: Adjacent JSX elements must be wrapped in an enclosing tag (8:3) 6 | return ( 7 | <span>test</span> \> 8 | <span>test</span> | ^ 9 | ); 10 | } 11 | }); من الشائع أن نرى المطورين يضيفون عنصر حاوي لتفادي هذا الخطأ. هذه المشكلة تؤثر على المكوِّنات أيضًا كما تؤثر على عقد React. فلا يمكن إعادة إلا مكوِّنٍ وحيد، ولكن يمكن أن يمتلك هذا المكوِّن عددًا غير محدودٍ من الأبناء. class MyComponent extends React.Component { render() { return ( <MyChildComponent/> ); } }; class MyChildComponent extends React.Component { render() { return <test>test</test>; } }; ReactDOM.render(<MyComponent />, app); إذا أعدتَ مكونين متجاورين، فسيحدث نفس الخطأ: var MyComponent = createReactClass({ render: function() { return ( <MyChildComponent/> <AnotherChildComponent/> ); } }); var MyChildComponent = createReactClass({ render: function() {return <test>test</test>;} }); var AnotherChildComponent = createReactClass({ render: function() {return <test>test</test>;} }); ReactDOM.render(<MyComponent />, app); رسالة الخطأ: Syntax error: Adjacent JSX elements must be wrapped in an enclosing tag (8:2) 6 | return ( 7 | <MyChildComponent/> \> 8 | <AnotherChildComponent/> | ^ 9 | ); 10 | } 11 | }); الإشارة إلى نسخة المكون عندما يُصيّر أحد العناصر (render) فستُنشأ نسخة من مكوِّن React من الخيارات المُمرَّرة إليه، يمكننا الوصول إلى هذه النسخة (instance) وخاصياتها (مثل this.props) وتوابعها (مثل this.setState()) بطريقتين. أوّل طريقة هي استخدام الكلمة المحجوزة this داخل دالة ضبط المكوِّن. ففي المثال الآتي، جميع تعابير console.log(this) ستُشير إلى نسخة المكوِّن: class Foo extends React.Component { componentWillMount(){ console.log(this); } componentDidMount(){ console.log(this); } render() { return <div>{console.log(this)}</div>; } }; ReactDOM.render(<Foo />, document.getElementById('app')); الطريقة الأخرى للحصول على مرجعية لنسخة المكوِّن تتضمن استخدام القيمة المُعادة من استدعاء الدالة ReactDOM.render(). بعبارةٍ أخرى، ستُعيد الدالة ReactDOM.render() مرجعيةً إلى أعلى مكوِّن جرى تصييره: class Bar extends React.Component { render() { return <div></div>; } }; var foo; // تخزين مرجعية إلى النسخة خارج دالة class Foo extends React.Component { render() { return <Bar>{foo = this}</Bar>; } }; var FooInstance = ReactDOM.render(<Foo />, document.getElementById('app')); // Foo التأكد أنَّ القيمة المُعادة هي مرجعية إلى نسخة console.log(FooInstance === foo); // true الناتج ملاحظات تُستخدَم الكلمة المحجوزة this داخل المكوِّن للوصول إلى خصائص المكوِّن كما في this.props.[NAME OF PROP] و this.props.children و this.state. يمكن أن تستخدم أيضًا لاستدعاء خاصيات وتوابع الصنف التي تتشاركها جميع المكوِّنات مثل this.setState. تعريف الأحداث في المكونات يمكن إضافة الأحداث إلى عقد React داخل خيار الضبط render كما ناقشنا سابقًا. سنضبط -في المثال الآتي- حدثَي React (وهما onClick و onMouseOver) على عقد React بصيغة JSX كما لو كنّا نضبط خيارات المكوِّن: class MyComponent extends React.Component { mouseOverHandler(e) { console.log('you moused over'); console.log(e); // sysnthetic event instance } clickHandler(e) { console.log('you clicked'); console.log(e); // sysnthetic event instance } render(){ return ( <div onClick={this.clickHandler} onMouseOver={this.mouseOverHandler}>click or mouse over</div> ) } }; ReactDOM.render(<MyComponent />, document.getElementById('app')); عندما تُصيّر React مكوِّنًا فستبحث عن خاصيات ضبط الأحداث في React (مثل onClick)، وتعامل هذه الخاصيات معاملةً مختلفةً عن بقية الخاصيات (جميع أحداث React مذكورة في الجدول أدناه). ومن الجلي أنَّ الأحداث في شجرة DOM الحقيقية سترتبط مع معالجاتها وراء الكواليس. أحد جوانب هذا الارتباط هو جعل سياق دالة معالجة الحدث في نفس مجال (scope) نسخة المكوِّن. لاحظ أنَّ قيمة this داخل دالة معالجة الحدث ففي المثال الآتي ستُشير إلى نسخة المكوِّن نفسها. class MyComponent extends React.Component { constructor(props) { super(props); // this هذا الربط ضروري لتعمل this.mouseOverHandler = this.mouseOverHandler.bind(this); } mouseOverHandler(e) { console.log(this); // إلى نسخة الكائن this تشير console.log(e); // sysnthetic event instance } render(){ return ( <div onMouseOver={this.mouseOverHandler}>mouse over me</div> ) } }; ReactDOM.render(<MyComponent />, document.getElementById('app')); تدعم React الأحداث الآتية: نوع الحدث الأحداث خاصيات متعلقة به الحافظة OnCopy onCut onPaste DOMDataTransfer clipboardData التركيب OnCompositionEnd onCompositionStart onCompositionUpdate data لوحة المفاتيح OnKeyDown onKeyPress onKeyUp AltKey charCode ctrlKey getModifierState(key) key keyCode locale location metaKey repeat shiftKey which التركيز OnChange onInput onSubmit DOMEventTarget relatedTarget النماذج OnFocus onBlur الفأرة OnClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp AltKey button buttons clientX clientY ctrlKey getModifierState(key) metaKey pageX pageY DOMEventTarget relatedTarget screenX screenY shiftKey الاختيار onSelect اللمس OnTouchCancel onTouchEnd onTouchMove onTouchStart AltKey DOMTouchList changedTouches ctrlKey getModifierState(key) metaKey shiftKey DOMTouchList targetTouches DOMTouchList touches واجهة المستخدم onScroll Detail DOMAbstractView view الدولاب onWheel DeltaMode deltaX deltaY deltaZ الوسائط OnAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata onLoadStart onPauseonPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting الصور onLoad onError الحركات onAnimationStart onAnimationEnd onAnimationIteration animationName pseudoElement elapsedTime الانتقالات onTransitionEnd propertyName pseudoElement elapsedTime ملاحظات توحِّد React التعامل مع الأحداث لكي تسلك سلوكًا متماثلًا في جميع المتصفحات. تنطلق الأحداث في React في مرحلة الفقاعات (bubbling phase). لإطلاق حدث في مرحلة الالتقاط (capturing phase) فأضف الكلمة "Capture" إلى اسم الحدث، أي أنَّ الحدث onClick سيصبح onClickCapture). إذا احتجتَ إلى تفاصيل كائن الأحداث المُنشَأ من المتصفح، فيمكنك الوصول إليه باستخدام الخاصية nativeEvent في كائن SyntheticEvent المُمرَّر إلى دالة معالجة الأحداث في React. لا تربط React الأحداث إلى العقد نفسها، وإنما تستخدم «تفويض الأحداث» (event delegation). يجب استخدام e.stopPropagation() أو e.preventDefault() يدويًا لإيقاف انتشار الأحداث بدلًا من استخدام return false;. لا تدعم React جميع أحداث DOM، لكن ما يزال بإمكاننا الاستفادة منها باستخدام توابع دورة الحياة في React. تركيب المكونات إذا لم يكن واضحًا لك أنَّ مكوِّنات React تستطيع أن تستخدم مكوِّنات React الأخرى فاعلم أنَّها تستطيع فعل ذلك. فيمكن أن تحتوي دالة الضبط render عند تعريف أحد مكوِّنات React إشارةً إلى المكونات الأخرى. فعندما يحتوي مكوِّنٌ ما على مكوِّنٍ آخر فيمكننا أن نقول أنَّ المكوِّن الأب «يمتلك» مكوِّنًا ابنًا (وهذا يسمى بالتركيب [composition]). في الشيفرة أدناه، يحتوي (أو يمتلك) المكوِّن BadgeList المكوِّنين BadgeBill و BadgeTom: class BadgeBill extends React.Component { render() {return <div>Bill</div>;} }; class BadgeTom extends React.Component { render() {return <div>Tom</div>;} }; class BadgeList extends React.Component { render() { return ( <div> <BadgeBill/> <BadgeTom /> </div>); } }; ReactDOM.render(<BadgeList />, document.getElementById('app')); جعلتُ الشيفرة السابقة بسيطةً عمدًا لتوضيح كيفية تركيب المكونات، وسنلقي في الدرس القادم نظرةً عن كيفية كتابة شيفرات التي تستخدم الخاصيات props لإنشاء مكوِّن Badge عام (generic). يمكن أن يأخذ المكوِّن العام Badge أي قيمة مقارنةً مع كتابة Badge مكتوبٌ فيه الاسم سابقًا. ملاحظات سمة أساسية لكتابة واجهات مستخدم قابلة للصيانة والإدارة هو تركيب المكوِّنات. صُمِّمَت مكوِّنات React لتحتوي على مكوِّناتٍ أخرى. لاحظ كيف تندمج شيفرة HTML والمكونات المُعرَّفة مسبقًا مع بعضهما بعضًا في دالة الضبط render(). استيعاب دورة حياة المكوِّن تمرّ مكوِّنات React بأحداث خلال حياتها وتسمى هذه الأحداث بدورة الحياة (lifecycle events). ترتبط هذه الأحدث بتوابع دورة الحياة. يمكنك أن تتذكر أننا ناقشنا بعض تلك التوابع في بداية هذا الدرس عندما ناقشنا عملية إنشاء المكوِّنات. توابع دورة الحياة توفِّر ارتباطات مع مراحل وطبيعة المكوِّن. ففي المثال الآتي الذي استعرته من أحد الأقسام السابقة، سنُسجِّل وقوع أحداث دورة الحياة componentDidMount و componentWillUnmount و getInitialState: var createReactClass = require('create-react-class'); // وتمرير كائن من خيارات الضبط Timer إنشاء المكون var Timer = createReactClass({ // this.state دالة تعيد كائنًا والذي سيصبح قيمة getInitialState: function() { return { secondsElapsed: Number(this.props.startTime) || 0 }; }, tick: function() { // تابع مخصص this.setState({ secondsElapsed: this.state.secondsElapsed + 1 }); }, componentDidMount: function() { // دالة رد النداء لدورة حياة المكوِّن this.interval = setInterval(this.tick, 1000); }, componentWillUnmount: function() { // دالة رد النداء لدورة حياة المكوِّن clearInterval(this.interval); }, render: function() { // JSX باستخدام صيغة React دالة تعيد عقد return ( <div> Seconds Elapsed: {this.state.secondsElapsed} </div> ); } }); ReactDOM.render(< Timer startTime="60" />, app); // startTime تمرير قيمة الخاصية يمكن تقسيم هذه التوابع إلى ثلاثة تصنيفات: مرحلة التركيب (mount) والتحديث (update) والإزالة (unmount). سأعرض جدولًا في كل تصنيف يحتوي على توابع دورة الحياة الخاصة به. مرحلة التركيب تحدث مرحلة التركيب (mounting phase) مرةً واحدةً في دورة حياة المكوِّن، وهي أول مرحلة وتبدأ عندما نُهيِّئ المكوِّن، وفي هذه المرحلة ستُعرَّف وتُضبَط خاصيات وحالة المكوِّن، وسيُركَّب المكوِّن مع جميع أبنائه إلى واجهة المستخدم المُحدَّدة (سواءً كانت DOM أو UIView أو غيرها). وفي النهاية يمكننا أن نجري بعض عمليات المعالجة إن كان ذلك لازمًا. التابع الشرح getInitialState() سيستدعى قبل تركيب المكوِّن، ويجب أن تعرِّف المكونات ذات الحالة (stateful) هذا التابع وتعيد بيانات الحالة الابتدائية. componentWillMount() سيستدعى مباشرةً قبل تركيب المكوِّن. componentDidMount() سيستدعى مباشرةً بعد تركيب المكوِّن. عملية التهيئة التي تتطلب وجود عقد DOM ستُعرَّف في هذا التابع. مرحلة التحديث تقع مرحلة التحديث (updating phase) مرارًا وتكرارًا خلال دورة حياة المكوِّن، وفي هذه المرحلة يمكننا إضافة خاصيات جديدة أو تغيير الحالة أو معالجة تفاعلات المستخدم أو التواصل مع شجرة المكوِّنات، وسنقضي جُلَّ وقتنا في هذه المرحلة. التابع الشرح componentWillReceiveProps(object nextProps) * shouldComponentUpdate(object nextProps, object nextState) سيستدعى عندما يقرر المكوِّن أنَّ أي تغييرات ستحتاج إلى تحديث شجرة DOM. إذا أردت استخدامه فيجب أن تقارن this.props مع nextProps و this.state مع nextState وتُعيد القيمة false لتخبر React بإمكانية تجاوز التحديث. componentWillUpdate(object nextProps, object nextState) سيستدعى مباشرةً قبل وقوع التحديث. لا يمكنك استدعاء this.setState() هنا. componentDidUpdate(object prevProps, object prevState) سيستدعى مباشرةً بعد وقوع التحديث. مرحلة الإزالة تقع مرحلة الإزالة (unmounting phase) مرةً واحدةً في دورة حياة المكوِّنات، وهي تحدث عند إزالة نسخة المكوِّن من واجهة المستخدم. ويمكن أن يحدث ذلك عندما ينتقل المستخدم إلى صفحة أخرى أو تتغير واجهة المستخدم أو اختفى المكوِّن …إلخ. التابع الشرح componentWillUnmount() سيستدعى مباشرةً قبل إزالة المكوِّن وحذفه. يجب أن تأتي عمليات «التنظيف» هنا. ملاحظات التابعان componentDidMount و componentDidUpdate هما مكانان جيدان تضع فيهما البنى المنطقية للمكتبات. راجع توثيق React في موسوعة حسوب لتأخذ نظرةً تفصيليةً حول أحداث دورة حياة مكونات React. ترتيب مرحلة التركيب: التهيئة الابتدائية getDefaultProps() (React.createClass) أو MyComponent.defaultProps (صنف ES6) getInitialState() (React.createClass) أو this.state (دالة بانية في ES6) componentWillMount() render() عملية تهيئة الأبناء وضبط دورة حياتهم componentDidMount() ترتيب مرحلة التحديث: componentWillReceiveProps() shouldComponentUpdate() render() توابع دورة الحياة للأبناء componentWillUpdate() ترتيب مرحلة الإزالة: componentWillUnmount() توابع دورة الحياة للأبناء إزالة نسخة المكوِّن الوصول إلى المكونات أو العقد الأبناء إذا احتوى مكوِّنٌ على مكونات أبناء أو عقد React داخله (كما في أو test) فيمكن الوصول إلى عقد React أو نسخ المكوِّنات الأبناء باستخدام الخاصية this.props.children. عند استخدام المكوِّن Parent في الشيفرة التالية، الذي يحتوي على عنصرَي والتي بدورها تحتوي على عقد React نصية. يمكن الوصول إلى جميع نسخ الأبناء داخل المكوِّن باستخدام this.props.children. وسأحاول في المثال الآتي أن أصل إليها داخل تابع دورة الحياة componentDidMount للعنصر الأب Parent: class Parent2 extends React.Component { componentDidMount() { // <span>child2text</span> الوصول إلى // ولا حاجة لاستخدام مصفوفة لطالما كان هنالك ابن واحد فقط console.log(this.props.children); // <span> لأننا نحصل على ابن العنصر child2text سينتج console.log(this.props.children.props.children); } render() {return <div />;} } class Parent extends React.Component { componentDidMount() { // <div>test</div><div>test</div> الوصول إلى مصفوفة تحتوي على console.log(this.props.children); // في المصفوفة <div> لأننا نصل إلى أول عنصر childtext سينتج console.log(this.props.children[1].props.children); } render() {return <Parent2><span>child2text</span></Parent2>;} } ReactDOM.render( <Parent><div>child</div><div>childtext</div></Parent>, document.getElementById('app') ); تعيد الخاصية this.props.children للمكوِّن Parent مصفوفةً تحتوي على إشارات لنسخ كائنات عقد React الأبناء. سنعرض هذه المصفوفة باستخدام console.log، أضف إلى ذلك أننا سنعرض في المكوِّن Parent قيمة العنصر الابن للعقدة الأولى (أي سنعرض قيمة العقدة النصية). لاحظ عندما استخدمنا المكوِّن Parent2 داخل المكوِّن Parent كان المكوِّن Parent2 يحتوي على عقدة React وحيدة هي child2text وبالتالي ستكون قيمة الخاصية this.props.children في تابع دورة الحياة componentDidMount للمكوِّن Parent2 هي إشارة مباشرة إلى عقدة في React، وليست مصفوفةً تحتوي على عنصرٍ وحيد. ولمّا كان من المحتمل أن تعيد الخاصية this.props.children مجموعةً واسعةً من العقد، فتوفِّر React مجموعةً من الأدوات للتعامل معها، وهذه الأدوات مذكورة في الجدول أدناه. الأداة الشرح React.Children.map(this.props.children, function(){}) تستدعي دالة لكل عنصر ابن مباشر موجود ضمن children مع تعيين this إلى قيمة thisArg. إن كان children عبارة عن جزء (fragment) مع مفتاح أو مصفوفة فلن تُمرَّر الدالة للكائن الحاوي. إن كانت قيمة children هي null أو undefined فستُعيد null أو undefined بدلًا من المصفوفة. React.Children.forEach(this.props.children, function(){}) مماثلة للتابع React.Children.map() ولكن لا تُعيد مصفوفة. React.Children.count(this.props.children) تُعيد العدد الكلي للمكوّنات الموجود ضمن children، مُكافئ لعدد المرات التي يُمرَّر فيها رد النداء إلى map أو التي يُستدعى فيها forEach. React.Children.only(this.props.children) تتحقّق من أنّ children يمتلك فقط ابنًا واحدًا (عنصر React) وتُعيده. فيما عدا ذلك سيرمي هذا التابع خطأً. React.Children.toArray(this.props.children) تُعيد بنية البيانات children كمصفوفة مع تعيين مفتاح لكل عنصر ابن. تكون مفيدةً إن أردت التعامل مع مجموعة من العناصر الأبناء في توابع التصيير لديك، خاصّة إن أردت إعادة ترتيب أو تجزئة this.props.children قبل تمريره. ملاحظات عندما يكون هنالك عنصرٌ ابن وحيد، فستكون قيمة this.props.children هي العنصر نفسه دون أن يحتوى في مصفوفة. وهذا يوفِّر عملية تهيئة المصفوفة. قد يربكك بدايةً أنَّ قيمة children ليست ما يعيده المكوِّن، وإنما ما هو محتوى داخل المكوِّن. استخدام الخاصيات ref تجعل الخاصية ref من الممكن تخزين نسخة من عنصر أو مكوِّن React معيّن باستخدام دالة الضبط render(). وهذا مفيدٌ جدًا عندما تريد الإشارة من داخل المكوِّن إلى عنصر أو مكوِّن آخر محتوى داخل الدالة render(). لإنشاء مرجعية، ضع الخاصية ref مع ضبط دالة كقيمةٍ لها في أي عنصر أو مكوِّن، ثم من داخل الدالة سيكون أول معامل (parameter) داخل مجال الدالة يشير إلى العنصر أو المكوِّن الذي عُرِّفَت الخاصية ref عليه. على سبيل المثال، سنعرض المحتويات المرجعية المشار إليها باستخدام ref عبر console.log: class C2 extends React.Component { render() {return <span ref={function(span) {console.log(span)}} />} } class C1 extends React.Component { render() { return( <div> <C2 ref={function(c2) {console.log(c2)}}></C2> <div ref={function(div) {console.log(div)}}></div> </div>); } } ReactDOM.render(<C1 ref={function(ci) {console.log(ci)}} />,document.getElementById('app')); لاحظ أنَّ الإشارات إلى المرجعيات تعيد نسخ المكوِّنات، بينما الإشارات إلى عناصر React تعيد مرجعيات إلى عناصر DOM في شجرة DOM الحقيقية (أي ليست مرجعيةً تشير إلى شجرة DOM الافتراضية، وإنما شجرة DOM الحقيقية). استخدامٌ شائعٌ للخاصية ref هو تخزين مرجعية إلى نسخة المكوِّن. ففي الشيفرة الآتية سأستخدم دالة رد نداء ref على حقل إدخال input نصي لتخزين مرجعية لنسخة المكوِّن لكي تستطيع التوابع الأخرى الوصول إليها عبر الكلمة المحجوزة this (كما في this.textInput.focus()): class MyComponent extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this) } handleClick() { // DOM API التركيز على المحتوى النصية باستخدام this.textInput.focus(); } render() { // قيمة الخاصية ref هي دالة رد نداء التي تؤدي إلى // عند تركيب المكون this.textInput الإشارة إلى return ( <div> <input type="text" ref={(thisInput) => {this.textInput = thisInput}} /> <input type="button" value="Focus the text input" onClick={this.handleClick} /> </div> ); } } ReactDOM.render( <MyComponent />, document.getElementById('app') ); ملاحظات لا يمكن ربط ref مع دالة عديم الحالة لأنَّ المكوِّن لا يملك نسخةً (instance) بعد. ربما تشاهد الخاصية ref مع سلسلة نصية بدلًا من دالة؛ وهذا أمرٌ مهمل في الإصدارات المستقبلية، ومن المُفضَّل استخدام الدوال. ستستدعى دالة رد نداء ref مباشرةً بعد تركيب المكوِّن. تسمح لنا الإشارات إلى نسخة المكوِّن باستدعاء توابع مخصصة على المكوِّن إذا كانت موجودةً عند تعريفه. كتابة المراجع مع تعبير تعريف دالة سطري يعني أنَّ React سترى كائن دالة مختلف في كل تحديث، وستستدعى ref مع القيمة null مباشرةً قبل استدعائها مع نسخة المكوِّن، أي أنَّ نسخة المكون ستزال في كل تغيير لقيمة ref وستستدعى ref القديمة مع القيمة null كوسيط. يبين توثيق React ملاحظةً مهمةً «ربّما تكون رغبتك الأولى لاستخدام المراجع هي تحقيق كل ما تُريده في تطبيقك. إن كانت هذه حالتك فخُذ لحظة للتفكير حول المكان المُلائم لوضع الحالة في التسلسل الهرمي للمُكوِّنات. يتضح عادةً أنّ المكان المناسب لوضع الحالة هو في المستويات العليا من التسلسل الهرمي للمُكوِّنات.». إجبار إعادة تصيير العنصر من المرجّح أنَّ: لاحظتَ أنَّ استدعاء ReactDOM.render() هو عملية التحميل الابتدائية للمكوِّن وجميع المكونات الفرعية له. وبعد عملية التركيب الابتدائية للمكوِّنات، ستحدث إعادة التصيير عندما: استدعي التابع setState() في المكوِّن استدعي التابع forceUpdate() في المكوِّن في أي وقت يجير إعادة تصيير المكوِّن (أو يُصيّر التصيير الابتدائي) فستُصيّر جميع المكوِّنات الأبناء التابعة له داخل شجرة DOM الافتراضية والتي قد تُسبِّب تغييرًا في شجرة DOM الحقيقية (أي واجهة المستخدم). ما أريد إيصاله لك هنا هو أنَّ إعادة تصيير أحد المكوِّنات في شجرة DOM الافتراضية لا يعني بالضرورة أنَّ تحديثًا سيطرأ على شجرة DOM الحقيقية. في المثال أدناه، سيبدأ استدعاء ReactDOM.render(< App / >, app); عملية التصيير، والذي سيؤدي إلى تصيير و ، عملية إعادة التصيير التالية ستحدث عند استخدام setInterval() لتابع المكوِّن setState() الذي سيؤدي إلى إعادة تصيير و ، لاحظ كيف ستتغير واجهة المستخدم عند تغيير حالة now: var Timer = createReactClass({ render: function() { return ( <div>{this.props.now}</div> ) } }); var App = createReactClass({ getInitialState: function() { return {now: Date.now()}; }, componentDidMount: function() { var foo = setInterval(function() { this.setState({now: Date.now()}); }.bind(this), 1000); setTimeout(function(){ clearInterval(foo); }, 5000); // .forceUpdate() لا تفعل هذا! هذا مجرد مثال توضيحي عن استخدام setTimeout(function(){ this.state.now = 'foo'; this.forceUpdate() }.bind(this), 10000); }, render: function() { return ( <Timer now={this.state.now}></Timer> ) } }); ReactDOM.render(< App />, app); تحدث عملية إعادة التصيير الآتية عند تشغيل setTimout() واستدعاء التابع this.forceUpdate() لاحظ أنَّ تحديث الحالة (this.state.now = 'foo';) لن تؤدي بمفردها إلى إعادة التصيير. ضبنا بداية الحالة باستخدام this.state ثم استدعينا this.forceUpdate() لكي يعاد تصيير العنصر مع الحالة الجديدة. ملاحظات لا تُحدِّث الحالة باستخدام this.state إطلاقًا! فعلنا ذلك في المثال السابق لتبيين طريقة استخدام this.forceUpdate() فحسب. ترجمة -وبتصرف- للفصل Basic React Components من كتاب React Enlightenment table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; }
-
تعلمنا في المقال السابق كيفية إنشاء عقد React باستخدام شيفرات JavaScript العادية، وسنلقي في هذا المقال نظرةً على إنشاء عقد React باستخدام صيغة JSX. إن لم تقرأ المقال السابق، عقد React، فانتقل إليه أولًا لقراءته ثم عد إلى هذا المقال. سنستخدم صيغة JSX بعد هذا المقال في بقية الكتاب ما لم نستعمل الدالة React.createElement() لأغراض التوضيح. بعد انتهائك من قراءة هذا المقال، يمكنك الاطلاع على شرح تفصيلي لجميع ميزات JSX في موسوعة حسوب. ما هي صيغة JSX؟ JSX هي صيغة شبيهة بصيغة XML أو HTML التي تستخدمها React لتوسعة ECMAScript لكي نستطيع كتابة تعابير شبيهة بلغة XML أو HTML داخل شيفرة JavaScript. هذه الصيغة مهيئة للعمل مع برمجيات التحويل مثل Babel لتحويل النص الشبيه بشيفرات HTML في ملفات JavaScript إلى كائنات JavaScript التي تستطيع محركات JavaScript تفسيرها. أساسيًا، عند استخدمنا لصيغة JSX يمكننا أن نكتب بنى شبيهة ببنى HTML (أي هياكل من العناصر كما في DOM) بنفس الملف الذي تكتب فيه شيفرة JavaScript، ثم يحوِّل Babel هذه التعابير إلى شيفرة JavaScript حقيقية. وعلى عكس ما جرت عليه العادة بوضع شيفرات JavaScript داخل HTML، تسمح لنا صيغة JSX بوضع شيفرات HTML داخل JavaScript. تسمح لنا JSX بكتابة شيفرة JavaScript الآتية: var nav = ( <ul id="nav"> <li><a href="#">Home</a></li> <li><a href="#">About</a></li> <li><a href="#">Clients</a></li> <li><a href="#">Contact Us</a></li> </ul> ); وسيحولها Babel إلى الشيفرة الآتية: var nav = React.createElement( "ul", { id: "nav" }, React.createElement( "li", null, React.createElement( "a", { href: "#" }, "Home" ) ), React.createElement( "li", null, React.createElement( "a", { href: "#" }, "About" ) ), React.createElement( "li", null, React.createElement( "a", { href: "#" }, "Clients" ) ), React.createElement( "li", null, React.createElement( "a", { href: "#" }, "Contact Us" ) ) ); لذا يمكننا أن نعدّ JSX على أنها اختصار لاستدعاء React.createElement(). فكرة دمج شيفرات HTML و JavaScript في نفس الملف هي فكرة مثيرة للجدال، حاول أن تتجاهل ذلك وتستعملها إذا وجدتها مفيدةً، وإذا لم تجدها مفيدةً فاكتب شيفرة JavaScript الضرورية لإنشاء عقد React يدويًا، فالخيار عائد لك تمامًا. أرى شخصيًا أنَّ JSX توفِّر صيغة مختصرة ومألوفةً لتعريف بنى هيكلية مع الخاصيات اللازمة لها والتي لا تتطلب تعلّم صيغة قوالب خاصة أو تتطلب الخروج من شيفرة JavaScript؛ وكلا الميزتين السابقتين مفيدتان عند إنشاء التطبيقات الكبيرة. من الواضح أنَّ صيغة JSX أسهل قراءةً وكتابةً من الأهرامات الكبيرة من استدعاءات دوال JavaScript مع تعريف الكائنات داخلها (قارن الشيفرتين السابقتين في هذا القسم لتتأكد من ذلك). أضف إلى ذلك أنَّ فريق تطوير React يعتقد أنَّ JSX أفضل لتعريف واجهات المستخدم من أحد حلول القوالب (مثل Handlebars): «إنَّ الشيفرة البرمجية والبنى الهيكلية مرتبطتان مع بعضهما بعضًا ارتباطًا وثيقًا. أضف إلى ذلك أنَّ الشيفرات البرمجية معقدة جدًا واستخدام لغات القوالب ستجعل الأمر صعبًا جدًا. وجدنا أنَّ أفضل حل لهذه المشكلة هو توليد شيفرات HTML مباشرةً من شيفرة JavaScript، وبهذا نستطيع استخدام كامل قدرات لغة برمجية حقيقية لبناء واجهات المستخدم.» ملاحظات لا تفكر في JSX على أنها لغة قوالب، وإنما هي صيغة JavaScript خاصة يمكن تصريفها، أي أنَّ JSX هي صيغة تسمح بتحويل بنى شبيهة ببنى HTML إلى شيفرات JavaScript. أداة Babel هي الأداة التي اختارها فريق تطوير React لتحويل شيفرات ES* و JSX إلى شيفرة ES5. يمكنك معرفة المزيد عن Babel بقراءة توثيقه الرسمي. باستخدام صيغة JSX: أصبح بإمكان الأشخاص غير المتخصصين تقنيًا فهم وتعديل الأجزاء المطلوبة. فيجد مطورو CSS والمصممون صيغة JSX أكثر ألفةً من شيفرة JavaScript. يمكنك استثمار كامل قدرات JavaScript في HTML وتتجنب تعلّم أو استخدام لغة خاصة بالقوالب. لكن اعلم أن JSX ليس محرّك قوالب، وإنما صيغة تصريحية للتعبير عن البنية الهيكلية الشجرية لمكونات UI. سيجد المُصرِّف (compiler) أخطاءً في شيفرة HTML الخاصة بك كنتَ ستغفل عنها. تحث صياغة JSX على فكر استخدام الأنماط السطرية (inline styles) وهو أمرٌ حسن. صيغة JSX منفصلة عن React، ولا تحاول JSX أن تتفق مع أي مواصفة تخص HTML أو XML، وإنما هي مصممة كميزة ECMAScript وهي تشبه HTML ظاهريًا فقط، وتجري كتابة مواصفة JSX كمسودة لكي تُستخدَم من أي شخص كإضافة لصياغة ECMAScript. في صيغة JSX، يجوز استخدام <foo-bar /> بمفرده، لكن لا يجوز استخدام <foo-bar>. أي عليك إغلاق جميع الوسوم دومًا. إنشاء عقد React باستخدام JSX بإكمالنا لما تعلمناه في المقال الماضي، يجب أن تكون قادرًا على إنشاء عقد React باستخدام الدالة React.createElement(). فيمكن مثلًا استخدام هذه الدالة لإنشاء عقد React التي تُمثِّل عقدًا حقيقيةً في HTML DOM إضافةً إلى عقدٍ مخصصة. سأريك حالتي الاستخدام السابقتين في المثال الآتي: // حقيقية HTML DOM التي تمثِّل عقدة React عقدة var HTMLLi = React.createElement('li', {className:'bar'}, 'foo'); // مخصصة HTML DOM التي تمثِّل عقدة React عقدة var HTMLCustom = React.createElement('foo-bar', {className:'bar'}, 'foo'); لاستخدام صيغة JSX بدلًا من React.createElement() لإنشاء هذه العقد، فكل ما علينا فعله هو تبديل استدعاءات الدالة React.createElement() إلى وسوم شبيهة بوسوم HTML التي تُمثِّل عناصر HTML التي تريد إنشاء شجرة DOM الافتراضية بها. يمكننا أن نكتب الشيفرة السابقة بصيغة JSX كما يلي: // حقيقية HTML DOM التي تمثِّل عقدة React عقدة var HTMLLi = <li className="bar">foo</li>; // مخصصة HTML DOM التي تمثِّل عقدة React عقدة var HTMLCustom = <foo-bar className="bar" >foo</foo-bar>; لاحظ أنَّ صيغة JSX غير محتواة في سلسلة نصية في JavaScript، وتكتبها كأنك تكتب الشيفرة داخل ملف .htmlعادي، وكما ذكرنا لعدّة مرات، ستحوِّل صيغة JSX إلى استدعاءات للدالة React.createElement() باستخدام Babel: // حقيقية HTML DOM التي تمثِّل عقدة React عقدة var HTMLLi = <li className="bar">foo</li>; // مخصصة HTML DOM التي تمثِّل عقدة React عقدة var HTMLCustom = <foo-bar className="bar" >foo</foo-bar>; ReactDOM.render(HTMLLi, document.getElementById('app1')); ReactDOM.render(HTMLCustom, document.getElementById('app2')); إذا أردتَ تفحص شيفرة HTML الناتجة عن المثال السابق، فستجد أنَّها شبيهة بهذه الشيفرة: <body> <div id="app1"><li class="bar" data-reactid=".0">foo</li></div> <div id="app2"><foo-bar class="bar" data-reactid=".1">foo</foo-bar></div> </body> إنشاء عقد React باستخدام JSX سهل جدًا كما لو كنتَ تكتب شيفرة HTML داخل ملفات JavaScript. ملاحظات تدعم JSX صيغة إغلاق الوسوم الخاصة بلغة XML، لذا يمكنك أن تهمل إضافة وسم الإغلاق إذا لم يمتلك العنصر أي أبناء. إذا مررت خاصيات إلى عناصر HTML التي ليست موجودة في مواصفة HTML فلن تعرضها React. أما إذا استخدمتَ عناصر HTML مخصصة (أي أنها ليست قياسية) فستُضاف الخاصيات التي ليست موجودةً في مواصفة HTML إلى العناصر المخصصة (فمثلًا <``x-my-component custom-attribute="foo" />). الخاصية class يجب أن تكتب className. الخاصية for يجب أن تكتب htmlFor. الخاصية style تقبل إشارةً مرجعيةً إلى كائنٍ يحتوي على خاصيات CSS بالصيغة المتعارف عليها في JavaScript (أي background-color تصبح backgroundColor). جميع الخاصيات مكتوبة بالصيغة المستخدمة في JavaScript، وذلك بحذف الشرطة وجعل الحرف الذي يليها كبيرًا (أي accept-charset ستصبح acceptCharset). لتمثيل عناصر HTML احرص على كتابة الوسوم بحرفٍ صغير. هذه هي قائمة بجميع خاصيات HTML التي تدعمها React: accept acceptCharset accessKey action allowFullScreen allowTransparency alt async autoComplete autoFocus autoPlay capture cellPadding cellSpacing challenge charSet checked classID className colSpan cols content contentEditable contextMenu controls coords crossOrigin data dateTime default defer dir disabled download draggable encType form formAction formEncType formMethod formNoValidate formTarget frameBorder headers height hidden high href hrefLang htmlFor httpEquiv icon id inputMode integrity is keyParams keyType kind label lang list loop low manifest marginHeight marginWidth max maxLength media mediaGroup method min minLength multiple muted name noValidate nonce open optimum pattern placeholder poster preload radioGroup readOnly rel required reversed role rowSpan rows sandbox scope scoped scrolling seamless selected shape size sizes span spellCheck src srcDoc srcLang srcSet start step style summary tabIndex target title type useMap value width wmode wrap تصيير JSX إلى DOM يمكن استخدام الدالة ReactDOM.render() لتصيير تعابير JSX إلى DOM. في الواقع، كل ما تفعله Babel هو تحوي JSX إلى React.createElement(). في المثال الآتي، سنُصيّر العنصر <li> والعنصر المخصص <foo-bar> إلى DOM باستخدام تعابير JSX: // حقيقية HTML DOM التي تمثِّل عقدة React عقدة var HTMLLi = <li className="bar">foo</li>; // مخصصة HTML DOM التي تمثِّل عقدة React عقدة var HTMLCustom = <foo-bar className="bar" >foo</foo-bar>; // <div id="app1"></div> إلى HTMLLi باسم React تصيير عقدة ReactDOM.render(HTMLLi, document.getElementById('app1')); // <div id="app2"></div> إلى HTMLCustom باسم React تصيير عقدة ReactDOM.render(HTMLCustom, document.getElementById('app2')); ستبدو شيفرة HTML كما يلي بعد تصيير العناصر إلى DOM: <body> <div id="app1"><li class="bar" data-reactid=".0">foo</li></div> <div id="app2"><foo-bar classname="bar" children="foo" data-reactid=".1">foo</foo-bar></div> </body> تذكّر أنَّ Babel يأخذ JSX ويحولها إلى عقد React (أي استدعاءات الدالة React.createElement()) ثم باستخدام هذه العقد المُنشَأة باستخدام React (في شجرة DOM الافتراضية) سنُصيّر العناصر إلى شجرة DOM الحقيقية. ما تفعله الدالة ReactDOM.render() هو تحويل عقد React إلى عقد DOM حقيقة ثم إضافتها إلى مستند HTML. ملاحظات ستستبدل أي عقد DOM داخل عناصر DOM التي سيُصيَّر إليها (أي أنها ستُحذَف ويحلّ المحتوى الجديد محلها). الدالة ReactDOM.render() لا تعدِّل عقدة عنصر DOM الذي تُصيّر React إليه، وإنما تتطلَّب React ملكيةً كاملةً للعقدة النصية عند التصيير، فلا يجدر بك إضافة أبناء أو إزالة أبناء من العقدة التي تضيف React فيها المكوِّن أو العقدة. التصيير إلى شجرة HTML DOM هو أحد الخيارات فقط، فهنالك خياراتٌ أخرى ممكنة، فمثلًا تستطيع التصيير إلى سلسلة نصية (أي ReactDOMServer.renderToString()) في جهة الخادم. إعادة تصيير نفس عنصر DOM سيؤدي إلى تحديث العقد الأبناء الحاليين إذا حدث تغييرٌ فيها، أو إذا أُضيفت عقدة عنصر ابن جديدة. لا تستدعي this.render() يدويًا أبدًا، اترك الأمر إلى React. استخدام تعابير JavaScript داخل JSX آمل الآن أن يكون واضحًا أنَّ JSX هي صيغة بسيطة ستحوَّل في نهاية المطاف إلى شيفرة JavaScript حقيقة، لكن ماذا سيحدث عندما نريد تضمين شيفرة JavaScript حقيقية داخل JSX؟ كل ما علينا فعله لكتابة تعابير JavaScript داخل JSX هو وضعها ضمن قوسين معقوفين {}. في شيفرة React الآتية، سنضيف تعبيرًا من تعابير JavaScript (وهو 2+2) محاطًا بقوسين معقوفين {} التي ستُفسَّر من معالج JavaScript: var label = '2 + 2'; var inputType = 'input'; // JSX لاحظ كيفية استخدام تعابير أو قيم جافاسكربت بين القوسين المعقوفين داخل صيغة var reactNode = <label>{label} = <input type={inputType} value={2+2} /></label>; ReactDOM.render(reactNode, document.getElementById('app')); ستحوَّل شيفرة JSX إلى النتيجة الآتية: var label = '2 + 2'; var inputType = 'input'; var reactNode = React.createElement( 'label', null, label, ' = ', React.createElement('input', { type: inputType, value: 2 + 2 }) ); ReactDOM.render(reactNode, document.getElementById('app')); بعد أن تُفسَّر الشيفرة السابقة من محرِّك JavaScript (أي المتصفح)، فستُقدَّر قيمة تعابير JavaScript وستبدو شيفرة HTML كما يلي: <div id="app"> <label data-reactid=".0"><span data-reactid=".0.0">2 + 2</span><span data-reactid=".0.1"> = </span><input type="input" value="4" data-reactid=".0.2"></label> </div> لا يوجد شيءٌ معقد في المثال السابق، بعد الأخذ بالحسبان أنَّ الأقواس ستُهرِّب شيفرة JSX. فالقوسان {} سيخبران JSX أنَّ المحتوى الموجود داخلهما هو شيفرة JavaScript فستتركها دون تعديل لكي يفسرها مُحرِّك JavaScript (أي التعبير 2+2) لاحظ أنَّ القوسين {} يمكن أن يستخدما في أي مكان داخل تعابير JSX لطالما كانت نتيجتها هو تعبيرٌ صالحٌ في JavaScript. استخدام تعليقات JavaScript داخل JSX يمكنك وضع تعليقات JavaScript في أي مكان في شيفرة JSX عدا المواضع التي تتوقع فيها JSX عقدة React ابن. في هذه الحالة ستحتاج إلى تهريب (escape) التعليق باستخدام {} لكي تعلم JSX أنَّ عليها تمرير المحتوى كشيفرة JavaScript. تفحص الشيفرة الآتية، واحرص على فهم متى عليك إخبار JSX أن تُهرِّب تعليق JavaScript لكيلا تُنشَأ عقدة React ابن: var reactNode = <div /*comment*/>{/* use {} here to comment*/}</div>; إذا لم نضع التعليق في الشيفرة السابقة الموجود داخل العقدة ضمن القوسين {} فسيحاول Babel تحويل التعليق إلى عقدة React نصية، وسيكون الناتج -غير المتوقع- دون استخدام {} كما يلي: var reactNode = React.createElement( "div", null, "/* use ", " here to comment*/" ); مما سينُتِج شيفرة HTML الآتية التي تحتوي على عقد نصية أُنشِئت خطأً: <div data-reactid=".0"> <span data-reactid=".0.0">/* use </span><span data-reactid=".0.1"> here to comment*/</span> </div> table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } استخدام أنماط CSS داخل JSX لتعريف أنماط CSS ضمن صيغة JSX، فعلينا تمرير مرجعية إلى كائنٍ يحتوي على خاصيات CSS وقيمها إلى الخاصية style. سنُهيّئ في بداية المثال الآتي كائن JavaScript باسم styles يحتوي على الأنماط التي نريد تضمينها سطريًا في JSX، ثم سنستخدم القوسين {} للإشارة إلى الكائن الذي يجب أن يُستخدَم كقيمة للأنماط (مثل style={styles}): var styles = { color: 'red', backgroundColor: 'black', fontWeight: 'bold' }; var reactNode = <div style={styles}>test</div>; ReactDOM.render(reactNode, document.getElementById('app1')); لاحظ أنَّ خاصيات CSS المُضمَّنة سطريًا مكتوبة بطريقة كتابة خاصيات CSS في JavaScript، وهذا ضروريٌ لأنَّ JavaScript لا تسمح باستخدام الشرطات – في أسماء الخاصيات. عند تحويل شيفرة JSX السابقة باستخدام Babel، ثم تفسيرها من محرِّك JavaScript فستكون شيفرة HTML الناتجة: <div style="color:red;background-color:black;font-weight:bold;" data-reactid=".0">test</div> ملاحظات يجب أن تبدأ السابقات الخاصة بالمتصفحات (باستثناء ms) بحرفٍ كبير، لهذا السبب تبدأ الخاصية WebkitTransition بحرف W كبير. يجب ألا يفاجئك استخدام الأحرف الكبيرة في أسماء خاصيات CSS بدلًا من الشرطات، فهذه هي الطريقة المتبعة للوصول إلى تلك الخاصيات في شجرة DOM عبر JavaScript (كما في document.body.style.backgroundImage). عند تحديد قيمة بواحدة البكسل، فستضيف React السلسلة النصية "px" تلقائيًا بعد القيم الرقمية باستثناء الخاصيات الآتية: columnCount fillOpacity flex flexGrow flexShrink fontWeight lineClamp lineHeight opacity order orphans strokeOpacity widows zIndex zoom تعريف الخاصيات في JSX في المقال السابق، ناقشنا تمرير خاصيات إلى الدالة React.createElement(type, props, children) عند إنشاء عقد React. ولمّا كانت صيغة JSX ستحوِّل إلى استدعاءات للدالة React.createElement() فأنت تملك فكرةً (من المقال السابق) كيف تعمل خاصيات React. لكن لمّا كانت JSX تُستخدَم للتعبير عن عناصر HTML فإنَّ الخاصيات المُعرَّفة ستُضاف إلى عنصر HTML الناتج. في المثال الآتي سأعرِّف عقدة <li> في React باستخدام JSX، ولها خمس خاصيات، لاحظ أنَّ إحداها هي خاصيةٌ غيرُ قياسيةٍ في HTML (وهي foo: 'bar') أما البقية فهي خاصيات HTML عادية: var styles = {backgroundColor:'red'}; var tested = true; var text = 'text'; var reactNodeLi = <li id="" data-test={tested?'test':'false'} className="blue" aria-test="test" style={styles} foo="bar"> {text} </li>; ReactDOM.render(reactNodeLi, document.getElementById('app1')); ستبدو شيفرة JSX بعد تحويلها كما يلي، لاحظي أنَّ الخاصيات أصبح وسائط مُمرَّرة إلى الدالة: var reactNodeLi = React.createElement( 'li', { id: '', 'data-test': tested ? 'test' : 'false', className: 'blue', 'aria-test': 'test', style: styles, foo: 'bar' }, text ); عند تصيير العقد reactNodeLi إلى DOM، فستبدو كما يلي: <div id="app1"> <li id="true" data-test="test" class="blue" aria-test="test" style="background-color:red;" data-reactid=".0"> text </li> </div> يجب أن تلاحظ الأمور الأربعة الآتية: ترك قيمة إحدى الخاصيات فارغةً سيؤدي إلى جعل قيمتها مساويةً إلى true (أي id="" ستصبح id="true"، و test ستصبح test="true"). الخاصية foo لن تُضاف إلى العنصر النهائي لأنها ليست خاصية HTML قياسية. لا يمكنك كتابة أنماط سطرية في JSX. عليك أن تُشير إلى كائنٍ يقطع في مجال تعريف شيفرة JSX أو تمرير كائن يحتوي على خاصيات CSS مكتوبةً كخاصيات JavaScript. يمكن أن تُضاف قيم JavaScript ضمن JSX باستخدام القوسين المعقوفين {} (كما في test={text} و data-test={tested?'test':'false'}). ملاحظات إذا كانت خاصيةٌ ما مكررةً فستؤخذ آخر قيمة لها. إذا مررت خاصيات إلى عناصر HTML التي ليست موجودة في مواصفة HTML فلن تعرضها React. أما إذا استخدمتَ عناصر HTML مخصصة (أي أنها ليست قياسية) فستُضاف الخاصيات التي ليست موجودةً في مواصفة HTML إلى العناصر المخصصة (فمثلًا <x-my-component custom-attribute="foo" />). الخاصية class يجب أن تكتب className. الخاصية for يجب أن تكتب htmlFor. الخاصية style تقبل إشارةً مرجعيةً إلى كائنٍ يحتوي على خاصيات CSS بالصيغة المتعارف عليها في JavaScript (أي background-color تصبح backgroundColor). خاصيات النماذج في HTML (مثل <input> أو <textarea></textarea> …إلخ.) المُنشَأة كعقد React ستدعم خاصيات التي يمكن تغييرها عبر تفاعل المستخدم مع العنصر؛ وهذه الخاصيات هي value و checked و selected. توفِّر React الخاصيات key و ref و dangerouslySetInnerHTML التي لا تتوافر في DOM وتأخذ دورًا فريدًا. يجب أن تكتب جميع الخاصيات مع حذف الشرطة - وجعل أول حرف يليها مكتوبًا بحرفٍ كبير، أي أنَّ الخاصية accept-charset ستُكتَب acceptCharset. هذه هي خاصيات HTML التي تدعمها تطبيقات React: accept acceptCharset accessKey action allowFullScreen allowTransparency alt async autoComplete autoFocus autoPlay capture cellPadding cellSpacing challenge charSet checked classID className colSpan cols content contentEditable contextMenu controls coords crossOrigin data dateTime default defer dir disabled download draggable encType form formAction formEncType formMethod formNoValidate formTarget frameBorder headers height hidden high href hrefLang htmlFor httpEquiv icon id inputMode integrity is keyParams keyType kind label lang list loop low manifest marginHeight marginWidth max maxLength media mediaGroup method min minLength multiple muted name noValidate nonce open optimum pattern placeholder poster preload radioGroup readOnly rel required reversed role rowSpan rows sandbox scope scoped scrolling seamless selected shape size sizes span spellCheck src srcDoc srcLang srcSet start step style summary tabIndex target title type useMap value width wmode wrap تعريف الأحداث في JSX في المقال السابق، شرحنا ووضحنا كيف يمكن أن ترتبط الأحدث مع عقد React. لفعل المثل في JSX عليك إضافة الأحداث ودالة المعالجة الخاصة بها كخاصية لشيفرة JSX التي تُمثِّل عقدة React. أُخِذ المثال الآتي من المقال السابق ويبيّن طريقة إضافة حدث إلى عقدة React دون استخدام JSX: var mouseOverHandler = function mouseOverHandler() { console.log('you moused over'); }; var clickhandler = function clickhandler() { console.log('you clicked'); }; var reactNode = React.createElement( 'div', { onClick: clickhandler, onMouseOver: mouseOverHandler }, 'click or mouse over' ); ReactDOM.render(reactNode, document.getElementById('app')); يمكن أن تُكتَب الشيفرة السابقة باستخدام JSX: var mouseOverHandler = function mouseOverHandler() { console.log('you moused over'); }; var clickHandler = function clickhandler() { console.log('you clicked'); }; var reactNode = <div onClick={clickHandler} onMouseOver={mouseOverHandler} >click or mouse over</div>; ReactDOM.render(reactNode, document.getElementById('app')); لاحظ أننا استخدمنا القوسين {} لربط دالة JavaScript إلى الحدث (أي onMouseOver={mouseOverHandler})، وهذه الطريقة تشابه طريقة ربط الأحداث السطرية في DOM. الأحداث التي تدعمها React موجودةٌ في الجدول الآتي: نوع الحدث الأحداث خاصيات متعلقة به الحافظة onCopy onCut onPaste DOMDataTransfer clipboardData التركيب onCompositionEnd onCompositionStart onCompositionUpdate data لوحة المفاتيح onKeyDown onKeyPress onKeyUp altKey charCode ctrlKey getModifierState(key) key keyCode locale location metaKey repeat shiftKey which التركيز onChange onInput onSubmit DOMEventTarget relatedTarget النماذج OnFocus onBlur الفأرة onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp altKey button buttons clientX clientY ctrlKey getModifierState(key) metaKey pageX pageY DOMEventTarget relatedTarget screenX screenY shiftKey الاختيار onSelect اللمس onTouchCancel onTouchEnd onTouchMove onTouchStart altKey DOMTouchList changedTouches ctrlKey getModifierState(key) metaKey shiftKey DOMTouchList targetTouches DOMTouchList touches واجهة المستخدم onScroll detail DOMAbstractView view الدولاب onWheel deltaMode deltaX deltaY deltaZ الوسائط onAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata onLoadStart onPause onPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting الصور onLoad onError الحركات onAnimationStart onAnimationEnd onAnimationIteration animationName pseudoElement elapsedTime الانتقالات onTransitionEnd propertyName pseudoElement elapsedTime ملاحظات توحِّد React التعامل مع الأحداث لكي تسلك سلوكًا متماثلًا في جميع المتصفحات. تنطلق الأحداث في React في مرحلة الفقاعات (bubbling phase). لإطلاق حدث في مرحلة الالتقاط (capturing phase) فأضف الكلمة "Capture" إلى اسم الحدث، أي أنَّ الحدث onClick سيصبح onClickCapture). إذا احتجتَ إلى تفاصيل كائن الأحداث المُنشَأ من المتصفح، فيمكنك الوصول إليه باستخدام الخاصية nativeEvent في كائن SyntheticEvent المُمرَّر إلى دالة معالجة الأحداث في React. لا تربط React الأحداث إلى العقد نفسها، وإنما تستخدم «تفويض الأحداث» (event delegation). يجب استخدام e.stopPropagation() أو e.preventDefault() يدويًا لإيقاف انتشار الأحداث بدلًا من استخدام return false;. لا تدعم React جميع أحداث DOM، لكن ما يزال بإمكاننا الاستفادة منها باستخدام توابع دورة الحياة في React. ترجمة وبتصرف للفصل JavaScript Syntax Extension (a.k.a, JSX) من كتاب React Enlightenment
-
سيناقش هذا المقال إنشاء عقد React (React Nodes) سواءً النصية (text nodes) أو عقد العناصر (element nodes) باستخدام JavaScript. ما هي عقدة React؟ النوع الأساسي أو القيمة التي تُنشَأ عند استخدام مكتبة React تسمى «عقدة React»، ويمكن تعريف عقدة React على أنها «تمثيل افتراضي خفيف وعديم الحالة وغير قابلٍ للتعديل لعقدة DOM» (لا تخف من الكلام السابق، هذا هو التعريف الرسمي، ما باليد حيلة :-) ). عقد React ليست عقد DOM حقيقية، وإنما تمثيل عن عقدة DOM محتملة، وهذا التمثيل يعد على أنه شجرة DOM افتراضية، ويُستخدم React لتعريف شجرة DOM الافتراضية باستخدام عقد React، والتي تُشكِّل البنية الأساسية لمكوِّنات React، والتي في نهاية المطاف ستُستخدَم لإنشاء بنية DOM الحقيقة وغير ذلك من البنى (مثل React Native). يمكن أن تُنشَأ عقد React باستخدام JSX أو JavaScript. سنلقي في هذا المقال نظرةً على إنشاء عقد React باستخدام JavaScript بمفردها دون استخدام JSX. إذ أرى أنَّ عليك فهم ما الذي تخفيه JSX وراء الكواليس لكي تفهم JSX فهمًا جيدًا. قد تحدثك نفسك بتجاوز هذا المقال لأنك تعرف مسبقًا أننا لن نستخدم JSX. لا تطعها! أقترح عليك أن تكمل قراءة هذا المقال لكي تعرف ما الذي تفعله لك JSX. ربما هذا أهم مقال في السلسلة ليتغلغل في ذهنك! إنشاء عقد العناصر سيُفضِّل المطورون في أغلبية الأوقات استخدام JSX مع React لإنشاء عقد العناصر، وبغض النظر عن ذلك، سنتعرّف في هذا المقال على طريقة إنشاء عقد React دون استخدام JSX باستعمال JavaScript فقط. أما المقال القادم فهو يناقش كيفية إنشاء عقد React باستخدام JSX. إنشاء عقد React سهلٌ جدًا فكل ما نحتاج له هو استخدام الدالة React.createElement(type, props, children) وتمرير مجموعة من الوسائط إليها والتي تُعرِّف عقدة DOM الفعلية (مثالًا نضبط قيمة type إلى عنصر من عناصر HTML مثل <li> أو عنصر مخصص مثل <my-li>). وسائط الدالة React.createElement()) مشروحة أدناه: type (string | React.createClass()) يمكن أن تكون سلسلة نصية تُمثِّل عنصر HTML (أو عنصر HTML مخصص) أو نسخة من مكوِّنات React. props (null | object) يمكن أن يكون null أو كائن يحتوي على خاصيات وقيم مرتبطة بها. children (null | string | React.Component | React.createElement()) يمكن أن تكون null أو سلسلة نصية التي ستحوّل إلى عقدة نصية، أو نسخة من React.Component أو React.createElement(). سأستخدم في المثال الآتي الدالة React.createElement() لإنشاء تمثيل في شجرة DOM الافتراضية لعقدة العنصر <li> والتي تحتوي على عقدة نصية فيها one (أي عقدة نصية في React) ومُعرِّف id يساوي li1. var reactNodeLi = React.createElement('li', {id:'li1'}, 'one'); لاحظ أنَّ أول وسيط يُعرِّف ما هو عنصر HTML الذي نريد تمثيله، ويعُرِّف الوسيط الثاني ما هي الخاصيات التي ستُضبَط على العناصر <li>، وأخيرًا سيُعرِّف الوسيط الثالث ما هي العقدة الموجودة داخل العنصر، وفي هذه الحالة وضعنا ببساطة عقدةً نصية (أي 'one') داخل <li>. لاحظ أنَّ آخر وسيط الذي سيصبح ابنًا للعقدة المُنشَأة يمكن أن يكون عقدة React نصية، أو عقدة عنصر في React، أو نسخة من مكوِّن React. كل ما فعلناه إلى الآن هو إنشاء عقدة عنصر في React تحتوي على عقدة React نصية، والتي خزناها في المتغير reactNodeLi، ولإنشاء شجرة DOM الوهمية فعلينا تصيير عقدة عنصر React في شجرة DOM الحقيقية، ولفعل ذلك سنستخدم الدالة ReactDOM.render(): ReactDOM.render(reactNodeLi, document.getElementById('app')); إذا أردنا وصف الشيفرة السابقة ببساطة، فهي تتضمن: إنشاء شجرة DOM افتراضية مبنية من عقدة عنصر React المُمرَّرة (أي reactNodeLi)، استخدام شجرة DOM الافتراضية لإعادة إنشاء فرع من شجرة DOM الحقيقية، إضافة الفرع المُنشَأ (أي <li id="li1">one</li>) إلى شجرة DOM الحقيقة كأحد أبناء العنصر <div id="app"></div>. بعبارةٍ أخرى، ستتغير شجرة DOM لمستند HTML من: <div id="app"></div> إلى: <div id="app"> // data-reactid قد أضافت الخاصية React لاحظ أنَّ <li id="li1" data-reactid=".0">one</li> </div> بيّن المثال السابق استخدامًا بسيطًا للدالة React.createElement(). يمكن للدالة React.createElement() إنشاء بنى معقدة أيضًا. فعلى سبيل المثال سنستخدم الدالة React.createElement() لإنشاء فرع من عقد React يُمثِّل قائمة غير مرتبة من العناصر (أي <ul>): // React في <li> إنشاء عقد عناصر var rElmLi1 = React.createElement('li', {id:'li1'}, 'one'); var rElmLi2 = React.createElement('li', {id:'li2'}, 'two'); var rElmLi3 = React.createElement('li', {id:'li3'}, 'three'); // السابقة إليها <li> وإضافة عناصر React في <ul> إنشاء عقدة العنصر var reactElementUl = React.createElement('ul', {className: 'myList'}, rElmLi1, rElmLi2, rElmLi3); قبل تصيير القائمة غير المرتبة إلى شجرة DOM، أرى إخبارك أنَّه يمكن تبسيط الشيفرة السابقة باستخدام React.createElement() بدلًا من المتغيرات. هذا المثال يوضِّح كيف يمكن تعريف الهيكلية باستخدام JavaScript: var reactElementUl = React.createElement( 'ul', { className: 'myList' }, React.createElement('li', {id: 'li1'},'one'), React.createElement('li', {id: 'li2'},'two'), React.createElement('li', {id: 'li3'},'three') ); عند تصيير الشيفرة السابقة إلى DOM فستبدو شيفرة HTML الناتجة كما يلي: <ul class="myList" data-reactid=".0"> <li id="li1" data-reactid=".0.0">one</li> <li id="li2" data-reactid=".0.1">two</li> <li id="li3" data-reactid=".0.2">three</li> </ul> يجب أن يكون واضحًا أنَّ عقد React هي كائنات JavaScript في شجرةٍ تمثِّل عقد DOM حقيقية داخل شجرة DOM الافتراضية. ثم تُستخدَم شجرة DOM الافتراضية لتعديل شجرة DOM الحقيقية داخل صفحة HTML. ملاحظات يمكن أن يكون الوسيط type المُمرَّر إلى الدالة React.createElement() سلسلةً نصية تُشير إلى عنصر من عناصر HTML القياسية (مثل 'li' الذي يمُثِّل العنصر <li>)، أو عنصرًا مخصصًا (مثل 'foo-bar' الذي يُمثِّل العنصر <foo-bar>)، أو نسخة من مكوِّن في React (أي نسخة من React.Component). هذه هي قائمة عناصر HTML التي تدعمها React (أي تمرير هذه العناصر كسلسلة نصية مكان المعامل type إلى الدالة createElement() سيؤدي إلى إنشاء عنصر HTML القياسي في DOM): a abbr address area article aside audio b base bdi bdo big blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd keygen label legend li link main map mark menu menuitem meta meter nav noscript object ol optgroup option output p param picture pre progress q rp rt ruby s samp script section select small source span strong style sub summary sup table tbody td textarea tfoot th thead time title tr track u ul var video wbr تصيير (rendering) العناصر إلى شجرة DOM توفِّر React الدالة React.render() من الملف react-dom.js التي يمكن أن تُستخدَم لتصيير عقد عناصر React إلى شجرة DOM. لقد رأينا الدالة render() مستخدمةً في هذا المقال. سنستخدم في المثال الآتي الدالة ReactDOM.render() وستُصيّر عقد العناصر '<li>' و '<foo-bar>' إلى شجرة DOM: // حقيقي DOM والتي تُمثِّل عقدة عنصر React عقدة var HTMLLi = React.createElement('li', {className:'bar'}, 'foo'); // مخصص HTML والتي تُمثِّل عقد عنصر React عقدة var HTMLCustom = React.createElement('foo-bar', {className:'bar'}, 'foo'); // <div id="app1"></div> إلى HTMLLi تصيير عقدة العنصر ReactDOM.render(HTMLLi, document.getElementById('app1')); // <div id="app2"></div> إلى HTMLCustom تصيير عقدة العنصر ReactDOM.render(HTMLCustom, document.getElementById('app2')); بعد تصيير العناصر إلى شجرة DOM، فسيبدو مستند HTML المُحدَّث كما يلي: <body> <div id="app1"><li class="bar" data-reactid=".0">foo</li></div> <div id="app2"><foo-bar classname="bar" children="foo" data-reactid=".1">foo</foo-bar></div> </body> الدالة ReactDOM.render() هي الطريقة المبدئية التي تمكننا من نقل عقد العناصر في React إلى شجرة DOM الوهمية، ثم إلى شجرة DOM الحقيقية. ملاحظات ستستبدل أي عقد DOM داخل عناصر DOM التي سيُصيَّر إليها (أي أنها ستُحذَف ويحلّ المحتوى الجديد محلها). الدالة ReactDOM.render() لا تعدِّل عقدة عنصر DOM الذي تُصيّر React إليه، وإنما تتطلَّب React ملكيةً كاملةً للعقدة النصية عند التصيير، فلا يجدر بك إضافة أبناء أو إزالة أبناء من العقدة التي تضيف React فيها المكوِّن أو العقدة. التصيير إلى شجرة HTML DOM هو أحد الخيارات فقط، فهنالك خياراتٌ أخرى ممكنة، فمثلًا تستطيع التصيير إلى سلسلة نصية (أي ReactDOMServer.renderToString()) في جهة الخادم. إعادة تصيير نفس عنصر DOM سيؤدي إلى تحديث العقد الأبناء الحاليين إذا حدث تغييرٌ فيها، أو إذا أُضيفت عقدة عنصر ابن جديدة. تعريف خاصيات العقد الوسيط الثاني الذي يُمرَّر إلى الدالة React.createElement(type, props, children) هو كائنٌ يحتوي على خاصيات ذات أسماء ترتبط بها قيم. تمتلك الخاصيات عدِّة أدوار: يمكن أن تتحول إلى خاصيات HTML. فلو طابقت إحدى الخاصيات خاصية HTML معروفة، فستُضاف إلى عنصر HTML النهائي في DOM. الخاصيات المُمرَّرة إلى createElement() ستصبح قيمًا مخزنةً في الكائن prop كنسخة من React.createElement() (أي [INSTANCE].props.[NAME_OF_PROP]). تستخدم الخاصيات بكثرة لإدخال قيم إلى المكوِّنات. تمتلك بعض الخاصيات تأثيراتٍ جانبية (مثل key و ref و dangerouslySetInnerHTML). يمكنك أن تتخيل الخاصيات على أنها خيارات ضبط لعقد React، ويمكنك أن تتخيلها كخاصيات HTML. سنُعرِّف في المثال الآتي عقدة العنصر <li> مع خمس خاصيات، إحدى تلك الخاصيات غير قياسية في HTML (أقصد بها foo:'bar') وبقية الخاصيات معروفة في HTML: var reactNodeLi = React.createElement('li', { foo:'bar', id:'li1', // للإشارة إلى صنف الأنماط في جافاسكربت className لاحظ استخدام الخاصية className: 'blue', 'data-test': 'test', 'aria-test': 'test', // CSS لضبط خاصية backgroundColor لاحظ استخدام الخاصية style: {backgroundColor: 'red'} }, 'text' ); ستبدو الشيفرة المُصيَّرة إلى صفحة HTML كما يلي: <li id="li1" data-test="test" class="blue" aria-test="test" style="background-color:red;" data-reactid=".0">text</li> ما عليك إدراكه من المثال السابق أنَّ خاصيات HTML القياسية فقط، وخاصيات data-* و aria-* هي الخاصيات الوحيدة التي تنتقل إلى شجرة DOM الحقيقية من شجرة DOM الافتراضية. لاحظ أنَّ الخاصية foo لن تظهر في شجرة DOM الحقيقية، فهذه الخاصية غير القياسية ستكون متاحةً في نسخة الكائن li لعقدة React (أي reactNodeLi.props.foo): var reactNodeLi = React.createElement('div',{ foo:'bar', id:'li1', className: 'blue', 'data-test': 'test', 'aria-test': 'test', style: {backgroundColor: 'red'} }, 'text' ); console.log(reactNodeLi.props.foo); // bar ReactDOM.render(reactNodeLi, document.getElementById('app')); ليس استخدام خاصيات React مقتصرًا على تحويلها إلى خاصيات HTML حقيقية، لكن يمكن أن تلعب دورًا في إعدادات الضبط المُمرَّرة إلى مكونات React. هذا الجانب من جوانب استخدام الخاصيات سيغطى في مقالٍ خاصٍ به، أما الآن فكل ما علينا إدراكه هو أنَّ تمرير خاصية إلى عقدة React هو أمرٌ يختلف عن تعريف خاصية في مكوِّن لكي تُستخدم لمدخلات ضبط ضمن ذاك المكوِّن. ملاحظات ترك قيمة إحدى الخاصيات فارغةً سيؤدي إلى جعل قيمتها مساويةً إلى true (أي id="" ستصبح id="true"، و test ستصبح test="true"). إذا كانت خاصيةٌ ما مكررةً فستؤخذ آخر قيمة لها. إذا مررت خاصيات إلى عناصر HTML التي ليست موجودة في مواصفة HTML فلن تعرضها React. أما إذا استخدمتَ عناصر HTML مخصصة (أي أنها ليست قياسية) فستُضاف الخاصيات التي ليست موجودةً في مواصفة HTML إلى العناصر المخصصة (فمثلًا <x-my-component custom-attribute="foo" />). الخاصية class يجب أن تكتب className. الخاصية for يجب أن تكتب htmlFor. الخاصية style تقبل إشارةً مرجعيةً إلى كائنٍ يحتوي على خاصيات CSS بالصيغة المتعارف عليها في JavaScript (أي background-color تصبح backgroundColor). خاصيات النماذج في HTML (مثل <input> أو <textarea></textarea> …إلخ.) المُنشَأة كعقد React ستدعم خاصيات التي يمكن تغييرها عبر تفاعل المستخدم مع العنصر؛ وهذه الخاصيات هي value و checked و selected. توفِّر React الخاصيات key و ref و dangerouslySetInnerHTML) التي لا تتوافر في DOM وتأخذ دورًا فريدًا. يجب أن تكتب جميع الخاصيات مع حذف الشرطة - وجعل أول حرف يليها مكتوبًا بحرفٍ كبير، أي أنَّ الخاصية accept-charset ستُكتَب acceptCharset. هذه هي خاصيات HTML التي تدعمها تطبيقات React: accept acceptCharset accessKey action allowFullScreen allowTransparency alt async autoComplete autoFocus autoPlay capture cellPadding cellSpacing challenge charSet checked classID className colSpan cols content contentEditable contextMenu controls coords crossOrigin data dateTime default defer dir disabled download draggable encType form formAction formEncType formMethod formNoValidate formTarget frameBorder headers height hidden high href hrefLang htmlFor httpEquiv icon id inputMode integrity is keyParams keyType kind label lang list loop low manifest marginHeight marginWidth max maxLength media mediaGroup method min minLength multiple muted name noValidate nonce open optimum pattern placeholder poster preload radioGroup readOnly rel required reversed role rowSpan rows sandbox scope scoped scrolling seamless selected shape size sizes span spellCheck src srcDoc srcLang srcSet start step style summary tabIndex target title type useMap value width wmode wrap تضمين أنماط CSS السطرية في عقد العناصر لتطبيق أنماط CSS السطرية (inline CSS styles) على عقدة React، فعليك تمرير كائن يحتوي على خاصيات CSS والقيم المرتبطة بها إلى الخاصية style. على سبيل المثال، سنمرر مرجعيةً إلى الكائن inlineStyles إلى الخاصية style: var inlineStyles = {backgroundColor: 'red', fontSize: 20}; var reactNodeLi = React.createElement('div', {style: inlineStyles}, 'styled') ReactDOM.render(reactNodeLi, document.getElementById('app1')); ستبدو شيفرة HTML الناتجة شبيهةً بما يلي: <div id="app1"> <div style="background-color:red;font-size:20px;" data-reactid=".0">styled</div> </div> هنالك أمران عليك الانتباه إليهما في الشيفرة السابقة: لم نضف الواحدة "px" إلى الخاصية fontSize لأنَّ React أضافته عوضًا عنّا. عند كتابة خاصيات CSS في JavaScript، يجب حذف الشرطة وجعل الحرف الذي يليها مكتوبًا بحرفٍ كبير (أي backgroundColor بدلًا من background-color). ملاحظات يجب أن تبدأ السابقات الخاصة بالمتصفحات (باستثناء ms) بحرفٍ كبير، لهذا السبب تبدأ الخاصية WebkitTransition بحرف W كبير (ولم أعد أنصح بإضافة هذه الخاصيات إلا لدعم المتصفحات القديمة جدًا). يجب ألا يفاجئك استخدام الأحرف الكبيرة في أسماء خاصيات CSS بدلًا من الشرطات، فهذه هي الطريقة المتبعة للوصول إلى تلك الخاصيات في شجرة DOM عبر JavaScript (كما في document.body.style.backgroundImage). عند تحديد قيمة بواحدة البكسل، فستضيف React السلسلة النصية "px" تلقائيًا بعد القيم الرقمية باستثناء الخاصيات الآتية: columnCount fillOpacity flex flexGrow flexShrink fontWeight lineClamp lineHeight opacity order orphans strokeOpacity widows zIndex zoom استخدام مصانع العناصر المُضمَّنة في React توفِّر React اختصارات مضمَّنة لإنشاء عقد عناصر HTML شائعة الاستخدام. تسمى هذه الاختصارات بمصانع عقد React. قبل الإصدار 16.0 من مكتبة React.js، كانت هذه المصانع مضمنة في المكتبة نفسها، لكن بعد الإصدار 16.0 فقد أسقط الدعم عنها، وظهرت وحدة باسم react-dom-factories) التي تتيح استخدام المصانع التي كانت مضمنة في React.js مع الإصدارات الجديدة من المكتبة. مصنع لعقد React هو دالةٌ تولِّد ReactElement مع قيمة معينة لخاصية type. باستخدام المصنع المضمن React.DOM.li()، يمكننا إنشاء عقدة العنصر <li>one</li> في React كما يلي: // DOM.li(props, children); import DOM from 'react-dom-factories'; var reactNodeLi = DOM.li({id:'li1'}, 'one'); وذلك بدلًا من استخدام: // React.createElement(type, prop, children) var reactNodeLi = React.createElement('li', null, 'one'); هذه قائمة بجميع المصانع المُضمَّنة في React: a,abbr,address,area,article,aside,audio,b,base,bdi,bdo,big,blockquote,body,br,button, canvas,caption,cite,code,col,colgroup,data,datalist,dd,del,details,dfn,dialog,div,dl, dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup, hr,html,i,iframe,img,input,ins,kbd,keygen,label,legend,li,link,main,map,mark,menu, menuitem,meta,meter,nav,noscript,object,ol,optgroup,option,output,p,param,picture, pre,progress,q,rp,rt,ruby,s,samp,script,section,select,small,source,span,strong, style,sub,summary,sup,table,tbody,td,textarea,tfoot,th,thead,time,title,tr,track, u,ul,var,video,wbr,circle,clipPath,defs,ellipse,g,image,line,linearGradient,mask, path,pattern,polygon,polyline,radialGradient,rect,stop,svg,text,tspa ملاحظات إذا كنتَ تستخدم JSX فقد لا تحتاج إلى استخدام المصانع إطلاقًا. يمكن إنشاء مصانع مخصصة إن كان ذلك ضروريًا. تعريف أحداث عقد React يمكن إضافة الأحداث إلى عقد React كما لو أردنا إضافة الأحداث إلى عقد DOM. سنضيف في المثال الآتي حدثين بسيطين click و mouseover إلى عقدة <div>: var mouseOverHandler = function mouseOverHandler() { console.log('you moused over'); }; var clickhandler = function clickhandler() { console.log('you clicked'); }; var reactNode = React.createElement( 'div', { onClick: clickhandler, onMouseOver: mouseOverHandler }, 'click or mouse over' ); ReactDOM.render(reactNode, document.getElementById('app')); لاحظ أنَّ اسم الخاصية لأحداث React تبدأ بالحرفين 'on' وتُمرِّر في الكائن props المُمرَّر إلى الدالة ReactElement(). تُنشئ React ما تسميه SyntheticEvent لكل حدث، والذي يحتوي على تفاصيل الحدث، وهي شبيهة بتفاصيل الأحداث المُعرَّفة في DOM. يمكن مثلًا الاستفادة من نسخة الكائن SyntheticEvent بتمريره إلى معالجات الأحداث، وسأفعل ذلك في المثال الآتي لتسجيل تفاصيل الحدث SyntheticEvent. var clickhandler = function clickhandler(SyntheticEvent) { console.log(SyntheticEvent); }; var reactNode = React.createElement( 'div', { onClick: clickhandler}, 'click' ); ReactDOM.render(reactNode, document.getElementById('app')); كل كائن من النوع syntheticEvent يملك الخاصيات الآتية: bubbles cancelable DOMEventTarget currentTarget defaultPrevented eventPhase isTrusted DOMEvent nativeEvent void preventDefault() isDefaultPrevented() void stopPropagation() isPropagationStopped() DOMEventTarget target timeStamp type هنالك خاصيات إضافية متوافرة اعتمادًا على نوع أو تصنيف الحدث المستخدم. فعلى سبيل المثال، الحدث onClick يحتوي على الخاصيات الآتية: altKey button buttons clientX clientY ctrlKey getModifierState(key) metaKey pageX pageY DOMEventTarget relatedTarget screenX screenY shiftKey يحتوي الجدول الآتي على خصائص syntheticEvent المتاحة لكل تصنيف من تصنيفات الأحداث: نوع الحدث الأحداث خاصيات متعلقة به الحافظة onCopy onCut onPaste DOMDataTransfer clipboardData التركيب onCompositionEnd onCompositionStart onCompositionUpdate data لوحة المفاتيح onKeyDown onKeyPress onKeyUp altKey charCode ctrlKey getModifierState(key) key keyCode locale location metaKey repeat shiftKey which التركيز onChange onInput onSubmit DOMEventTarget relatedTarget النماذج OnFocus onBlur الفأرة onClick onContextMenu onDoubleClick onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp altKey button buttons clientX clientY ctrlKey getModifierState(key) metaKey pageX pageY DOMEventTarget relatedTarget screenX screenY shiftKey الاختيار onSelect اللمس onTouchCancel onTouchEnd onTouchMove onTouchStart AltKey DOMTouchList changedTouches ctrlKey getModifierState(key) metaKey shiftKey DOMTouchList targetTouches DOMTouchList touches واجهة المستخدم onScroll detail DOMAbstractView view الدولاب onWheel deltaMode deltaX deltaY deltaZ الوسائط onAbort onCanPlay onCanPlayThrough onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata onLoadStart onPause onPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting الصور onLoad onError الحركات onAnimationStart onAnimationEnd onAnimationIteration animationName pseudoElement elapsedTime الانتقالات onTransitionEnd propertyName pseudoElement elapsedTime ملاحظات توحِّد React التعامل مع الأحداث لكي تسلك سلوكًا متماثلًا في جميع المتصفحات. تنطلق الأحداث في React في مرحلة الفقاعات (bubbling phase). لإطلاق حدث في مرحلة الالتقاط (capturing phase) فأضف الكلمة "Capture" إلى اسم الحدث، أي أنَّ الحدث onClick سيصبح onClickCapture. إذا احتجتَ إلى تفاصيل كائن الأحداث المُنشَأ من المتصفح، فيمكنك الوصول إليه باستخدام الخاصية nativeEvent في كائن SyntheticEvent المُمرَّر إلى دالة معالجة الأحداث في React. لا تربط React الأحداث إلى العقد نفسها، وإنما تستخدم «تفويض الأحداث» (event delegation). يجب استخدام e.stopPropagation() أو e.preventDefault() يدويًا لإيقاف انتشار الأحداث بدلًا من استخدام return false;. لا تدعم React جميع أحداث DOM، لكن ما يزال بإمكاننا الاستفادة منها باستخدام توابع دورة الحياة في React. ترجمة وبتصرف للفصل React Nodes من كتاب React Enlightenment table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; }
-
سنتحدث في هذا المقال عن كيفية ضبط صفحة HTML لكي يمكن تفسيرها في متصفح الويب، وسيتمكن المتصفح في وقت التنفيذ من تحويل تعابير JSX ويشغِّل شيفرة React بنجاح. استخدام react.js و react-dom.js في صفحة HTML الملف react.js هو الملف الأساسي الذي نحتاج له لإنشاء عناصر React وكتابة مكونات React. عندما ترغب بعرض المكونات التي أنشأتها في مستند HTML (أي كتابتها إلى DOM) فستحتاج أيضًا إلى الملف react-dom.js. يعتمد الملف react-dom.js على الملف react.js ويجب تضمينه بعد تضمين الملف react.js. مثالٌ عن مستند HTML يحتوي على React: <!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> </head> <body> </body> </html> بتحميل الملفين react.js و react-dom.js في صفحة HTML، أصبح بإمكاننا إنشاء عقد ومكونات React ثم عرضها في DOM. المثال البسيط الآتي يُنشِئ مكوِّن HelloMessage يحتوي على عقدة العنصر <div> ثم سيُعرَض في شجرة DOM داخل عنصر HTML المُعرَّف <div id="app"></div>: <!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> <script src="https://unpkg.com/create-react-class@15.6.0-rc.0/create-react-class.js" crossorigin></script> </head> <body> <div id="app"></div> <script> var HelloMessage = createReactClass({ // استخدمنا هذه الوحدة لترى كيف تستعمل displayName: 'HelloMessage', render: function render() { return React.createElement('div',null,'Hello ',this.props.name); } }); ReactDOM.render(React.createElement(HelloMessage,{ name: 'Ahmed' }), document.getElementById('app')); </script> </body> </html> هذا كل ما تحتاج له لاستخدام React. لكنه لن يُمكِّنك من استخدام JSX، والتي سنناقشها في القسم التالي. ملاحظات لا تجعل العنصر <body> العنصر الأساسي في تطبيق React. احرص دومًا على وضع عنصر ؤ داخل <body>، وأعطه ID، ثم صيِّر (render) المكونات داخله. وهذا يعطي React مجالًا خاصًا بها لتجري التعديلات، دون أن تقلق عن أي شيءٍ آخر يجري تعديلات على العناصر الأبناء للعنصر <body>. استخدام JSX عبر Babel عملية إنشاء المكوِّن HelloMessage وعنصر <div> في المثال الآتي جرت باشتقاق الصنف React.Component واستخدام الدالة React.createElement(). يُفترَض أن تبدو الشيفرة الآتية مألوفةً لأنها مماثلة لشيفرة HTML من القسم السابق: <!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> </head> <body> <div id="app"></div> <script> class HelloMessage extends React.Component { render(){ return React.createElement('div', null, 'Hello ', this.props.name); } }; ReactDOM.render(React.createElement(HelloMessage, { name: 'Ahmed' }), document.getElementById('app')); </script> </body> </html> يمكنك أن تستخدم JSX اختياريًا عبر Babel، فمن الممكن تبسيط عملية إنشاء عناصر React بتجريد استدعاءات الدالة React.createElement() ونكتبها بطريقة تشبه طريقة كتابة عناصر HTML. فبدلًا من كتابة الشيفرة الآتية التي تستخدم الدالة React.createElement(): return React.createElement('div', null, 'Hello ', this.props.name); يمكننا أن نكتب ما يلي باستخدام JSX: return <div>Hello {this.props.name}</div>; ثم باستخدام Babel نستطيع تحويلها إلى الشيفرة التي تستخدم React.createElement() لكي يتمكن محرِّك JavaScript من تفسيرها. يمكننا القول مجازًا أنَّ JSX هي شكلٌ من أشكال HTML التي تستطيع كتابتها مباشرةً ضمن شيفرة JavaScript والتي تحتاج إلى خطوة إضافية هي التحويل، وتجرى عملية التحويل باستخدام Babel إلى شيفرة ECMAScript 5 لكي تتمكن المتصفحات من تشغيلها. بعبارةٍ أخرى، سيحوِّل Babel شيفرة JSX إلى استدعاءات للدالة React.createElement(). سنتحدث عن المزيد من تفاصيل JSX في القسم المخصص لها، لكننا الآن يمكننا أن نعدّ JSX على أنها طبقة تجريد اختيارية توفَّر للسهولة عند إنشاء عناصر React، ولن تعمل في متصفحات ES5 ما لم نحوِّلها بدايةً باستخدام Babel. تحويل JSX عبر Babel في المتصفح يُضبَط عادةً Babel لكي يُعالِج ملفات JavaScript أثناء عملية التطوير باستخدام أداة سطر الأوامر الخاصة به (عبر استخدام Webpack على سبيل المثال)؛ لكن من الممكن استخدام Babel مباشرةً في المتصفح بتضمين سكربت معيّن. ولمّا كنّا في بداياتنا فسنتفادى استخدام الأدوات التي تعمل من سطر الأوامر أو نتعلم استخدام مُحمِّل للوحدات، وذلك لكي ننطلق في React. استخدام browser.js لتحويل JSX في المتصفح حوّلنا مكوِّن React في مستند HTML الآتي إلى صياغة JSX، وستحدث عملية التحويل بسبب تضيمننا لملف babel.min.js وإعطائنا لعنصر <script> الخاصية type مع القيمة text/``b``abel: <!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js" crossorigin></script> </head> <body> <div id="app"></div> <script type="text/babel"> class HelloMessage extends React.Component { // React.Component لاحظ استخدام render(){ return <div>Hello {this.props.name}</div>; } }; ReactDOM.render(<HelloMessage name="Ahmed" />, document.getElementById('app')); /*** سابقًا ***/ /* var HelloMessage = createReactClass({ * render: function() { * return <div>Hello {this.props.name}</div>; * } * }); * * ReactDOM.render(<HelloMessage name="Ahmed" />, document.getElementById('app')); */ </script> </body> </html> تحويلنا لشيفرة JSX في المتصفح هو حلٌ سهلٌ وبسيط، لكنه ليس مثاليًا لأنَّ عملية التحويل تجري في وقت التشغيل، وبالتالي استخدام babel.min.js ليس حلًا عمليًا يمكن استخدامه في البيئات الإنتاجية. ملاحظات أداة Babel هي اختيار المطورين من فريق React لتحويل شيفرات ES* وصيغة JSX إلى شيفرة ES5. تعلّم المزيد حول Babel بمراجعة توثيقه. باستخدام صيغة JSX: أصبح بإمكان الأشخاص غير المتخصصين تقنيًا فهم وتعديل الأجزاء المطلوبة. فيجد مطورو CSS والمصممون صيغة JSX أكثر ألفةً من شيفرة JavaScript. يمكنك استثمار كامل قدرات JavaScript في HTML وتتجنب تعلّم أو استخدام لغة خاصة بالقوالب. لكن اعلم أن JSX ليس محرّك قوالب، وإنما صيغة تصريحية للتعبير عن البنية الهيكلية الشجرية لمكونات UI. سيجد المُصرِّف (compiler) أخطاءً في شيفرة HTML الخاصة بك كنتَ ستغفل عنها. تحث صياغة JSX على فكر استخدام الأنماط السطرية (inline styles) وهو أمرٌ حسن. اعرف محدوديات JSX. تجري كتابة مواصفة JSX كمسودة لكي تُستخدَم من أي شخص كإضافة لصياغة ECMAScript. استخدام ES6 و ES* مع React Babel ليس جزءًا من React، وليس الغرض من إنشاء Babel هو تحويل JSX، وإنما أُنشِئ كمُصرِّف JavaScript (compiler) بادئ الأمر. إذ يأخذ شيفرة ES ويحوِّلها لكي تعمل على المتصفحات التي لا تدعم شيفرة ES. في هذه الآونة، يستخدم Babel أساسيًا لتحويل شيفرات ES6 و ES7 إلى ES5. عند إجراء عمليات التحويل هذه فمن البسيط تحويل تعابير JSX إلى استدعاءات React.createElement(). وبجانب تحويل Babel لشيفرات JSX، فيسمح أيضًا تحويل الشيفرات التي تعمل في إصدارات مستقبلية من ES*. مستند HTML الآتي يحتوي على مكوِّن HelloMessage مع إعادة كتابته لكي يستفيد من ميزة الأصناف في ES6. فلن يحوِّل Babel صيغة JSX فحسب، بل سيحوِّل صيغة أصناف ES6 إلى صيغةٍ تستطيع المتصفحات التي تحتوي محرِّك ES5 تفسيرها: <!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js" crossorigin></script> </head> <body> <div id="app"></div> <script type="text/babel"> class HelloMessage extends React.Component { // React.Component لاحظ استخدام render(){ return <div>Hello {this.props.name}</div>; } }; ReactDOM.render(<HelloMessage name="John" />, document.getElementById('app')); /*** سابقًا ***/ /* var HelloMessage = createReactClass({ * render: function() { * return <div>Hello {this.props.name}</div>; * } * }); * * ReactDOM.render(<HelloMessage name="John" />, document.getElementById('app')); */ </script> </body> </html> يأخذ Babel في المستند السابق الشيفرةَ الآتية: class HelloMessage extends React.Component { render(){ return <div>Hello {this.props.name}</div>; } }; ReactDOM.render(<HelloMessage name="John" />, document.getElementById('app')); ويحولها إلى: "use strict"; var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } var HelloMessage = (function (_React$Component) { _inherits(HelloMessage, _React$Component); function HelloMessage() { _classCallCheck(this, HelloMessage); _get(Object.getPrototypeOf(HelloMessage.prototype), "constructor", this).apply(this, arguments); } _createClass(HelloMessage, [{ key: "render", value: function render() { return React.createElement( "div", null, "Hello ", this.props.name ); } }]); return HelloMessage; })(React.Component); ; ReactDOM.render(React.createElement(HelloMessage, { name: "John" }), document.getElementById('app')); أغلبية ميزات ES6 (مع بعض الاستثناءات الطفيفة) يمكن تحويلها باستخدام Babel. ملاحظات بالطبع ما يزال بإمكاننا استخدام Babel لغرضه الأساسي (وهو تصريف شيفرات JavaScript الحديثة إلى شيفرة JavaScript القديمة) دون استخدام JSX. لكن أغلبية المطورين الذين يستعملون Babel يستفيدون من قدرته على تحويل JSX إضافةً إلى ميزات ES* غير المدعومة في المتصفحات القديمة. يمكنك مراجعة توثيق Babel لمزيدٍ من المعلومات. تجربة React في JSFiddle يمكن استخدام نفس الضبط الذي أجريناه في هذا المقال مع JSFilddle. إذ تُستعمَل نفس الملفات (react.js و react-dom.js و babel.min.js) لجعل تنفيذ تطبيقات React سهلًا. هذا مثالٌ عن JSFiddle يحتوي على مكوِّن HelloMessage المستخدم في هذا المقال. لاحظ أنَّ لسان Babel يشير إلى أنَّ شيفرة JavaScript الموجودة في هذا اللسان ستحوَّل باستخدام Babel. وإذا ضغطتَ على لسان Resources ستجد أنَّ JSFiddle يُضمِّن الملفين react.js و react-dom.js. سيُفتَرَض بعد قراءتك لهذا المقال أنَّك تدرك ما هي المتطلبات الأساسية لإعداد React و Babel عبر babel.min.js، وصحيحٌ أنَّ JSFiddle لا يُصرِّح بوضوح عن طريقة إعداده، لكنه يستخدم نفس الضبط السابق (أو ما يشبهه) لتشغيل شيفرات React. ترجمة وبتصرف للفصل React Setup من كتاب React Enlightenment
-
قبل أن نتعرف سويةً على آلية عمل React، دعنا نتعرف بدايةً على بعض المصطلحات التي ستسهِّل عملية التعليم. سأورد قائمةً من المصطلحات الشائعة، إضافةً إلى تعريفاتها، وسنستعمل هذه المصطلحات أثناء حديثنا عن React. Babel يحوِّل Babel شيفرة JavaScript ES (أي JS 2015 و JS 2016 و JS 2017) إلى شيفرة ES5. إنَّ Babel هي أداة من اختيار مطوري React لكتابة شيفرات ES وتحويل JSX إلى شيفرة ES5. Babel CLI يأتي Babel مع أداة تعمل من سطر الأوامر تسمى Babel CLI، ويمكن أن تستخدم لبناء الملفات من سطر الأوامر. خيارات ضبط المكونات (Component Configuration Options) وهي وسائط الضبط التي تُمرَّر (ككائن) إلى الدالة React.createClass() أو الدالة البانية (في حال كنت تستعمل الأصناف في ES6) مما ينتج نسخةً (instance) من مكوِّن React. توابع دورة حياة المكونات (Component Life Cycle Methods) توابع دورة حياة المكونات هي مجموعة فرعية من أحداث المكونات، المفصولة (اصطلاحيًا) عن الخيارات الأخرى لضبط المكونات، أي هي: componentWillUnmount componentDidUpdate UNSAFE_componentWillUpdate shouldComponentUpdate UNSAFE_componentWillReceiveProps componentDidMount UNSAFE_componentWillMount تُنفَّذ هذه التوابع في نقاط مُحدَّدة من دورة حياة المكوِّن. شجرة DOM (Document Object Model) شجرة DOM هي الواجهة البرمجية لمستندات HTML و XML و SVG، وهي توفِّر تمثيلًا هيكليًا للمستند كشجرة. تُعرِّف DOM الدوال التي تسمح بالوصول إلى الشجرة، لذا يمكنها تغيير بنية المستند وأنماط التنسيق التابعة له ومحتواه. توفِّر DOM تمثيلًا للمستند على شكل مجموعة مهيكلة من العقد (nodes) والكائنات (objects)، والتي تملك مختلف الخاصيات (properties) والتوابع (methods). يمكن أن تملك العقد معالجات أحداث (event handlers) مرتبطة بها، وستستدعى تلك المعالجات عند تفعيل الحدث. باختصار، تصل شجرة DOM بين صفحات الويب ولغات البرمجة. ES5 الإصدار الخامس من معيار ECMAScript. ES6 أو ECMAScript 2015 الإصدار السادس من معيار ECMAScript، والذي يحتوي على إضافات كثيرة على لغة JavaScript. ES7 أو ECMAScript 2016 الإصدار السابع من معيار ECMAScript. ES* تستخدم لتمثيل النسخة الحالية من JavaScript إضافةً إلى الإصدارات المستقبلية، والتي يمكن الكتابة بها باستخدام أدوات مثل Babel. عندما ترى «ES*» فمن المرجح أنَّ المقصود بها هو ES5 و ES6 و ES7 معًا. JSX JSX هي صيغة إضافية اختيارية تشبه XML لمعيار ECMAScript التي يمكن أن تُستخدم لتعريف بنية شجريّة شبيهة بلغة HTML في ملفات JavaScript. ستحوَّل تعابير JSX في ملف JavaScript إلى صياغة JavaScript قبل أن يتمكن محرِّك JavaScript من تفسير الملف. تُستَخدم برمجية Babel عادةً لتحويل تعابير JSX. Node.js Node.js هي بيئة تشغيل مفتوحة المصدر ومتعددة المنصات لكتابة شيفرات JavaScript. بيئة التشغيل Node.js تُفسِّر شيفرات JavaScript باستخدام محرِّك V8. npm npm هو مدير حزم للغة JavaScript نَشَأ من مجتمع Node.js. خاصيات React (أي React props) يمكنك أن تعدّ الخاصيات (props) على أنها خيارات الضبط لعقد React، وفي نفس الوقت يمكنك أن تتخيلها كخاصيات HTML. تملك الخاصيات عدِّة أدوار: يمكن أن تصبح خاصيات HTML، فلو طابقت خاصيةٌ ما إحدى خاصيات HTML فستُضاف كخاصية HTML في شجرة DOM النهائية. الخاصيات المُمرَّرة إلى الدالة createElement() تصبح قيمًا مخزنةً في الكائن prop كنسخة (instance) من React.createElement() أي [INSTANCE].props.[NAME_OF_PROP] تستخدم الخاصيات بكثرة لتمرير قيم إلى المكونات. بعض الخاصيات لها تأثيرات جانبية (مثل key و ref و dangerouslySetInnerHTML). React React هي مكتبة JavaScript تُستخدم لكتابة واجهات للمستخدمة بمرونة وكفاءة وفعالية عالية. مكوِّن React يُنشَأ مكوِّن React باستدعاء الوحدة create-react-class (أو React.Component عند استخدام الأصناف في ES6). هذه الدالة تأخذ كائنًا من الخيارات الذي يُستخدَم لضبط وإنشاء مكونات React. أحد أشهر خيارات الضبط هو الدالة render التي تعيد عقد React أي React nodes. والتالي يمكننك أن تعدّ مكوِّن React على أنه تجريد (abstraction) يحتوي على مكوِّن أو عقدة React واحد أو أكثر. الوحدة create-react-class توفر هذه الوحدة طريقة لإنشاء مكونات React دون إنشاء صنف جديد وجعله مشتقًا من الصنف React.Component. هذه الوحدة موجودة لكتابة شيفرات React.js دون ES6. عقد عناصر React عقد عناصر React (React Element Nodes أو ReactElement) هو تمثيل يشبه عناصر HTML في شجرة DOM يُنشَأ باستخدام React.createElement();. عقد React عقد React (React Nodes أي عقد العناصر والعقد النصية) هو نوع الكائنات الرئيسي في React ويمكن إنشاؤه باستخدام React.createElement('div');. بعبارةٍ أخرى، عقد React هي كائنات تُمثِّل عقد DOM وعقد DOM الأبناء التابعة لها. وهي تمثيلٌ خفيفٌ وعديم الحالة (stateless) وغير قابلٍ للتعديل (immutable) لعقدة DOM. مصانع عقد React مصانع عقد React (React Node Factories) هي دالة تولِّد عقد عنصر React ذات نوعٍ (type) مُحدَّد. كانت هذه الخاصية موجودة في إصدارات سابقة من React.js ثم أهملت. دالة مكون React عديم الحالة أي React Stateless Function Component، وتكون عندما يتألف المكوِّن من الخاصيات فقط، دون حالة، ويمكن أن يكتب المكوِّن كدالة نقية مما يجعلنا نتجنب إنشاء نسخة من مكوِّن React. var MyComponent = function(props){ return <div>Hello {props.name}</div>; }; ReactDOM.render(<MyComponent name="Ahmed" />, app); عقد عناصر React عقد عناصر React (React Text Nodes، أي ReactText) هي تمثيل للعقد النصية في شجرة DOM الوهمية كما في React.createElement('div', null, 'a text node');. شجرة DOM الوهمية (Virtual DOM) Virtual DOM هي شجرة مخزَّنة في ذاكرة JavaScript لعناصر ومكونات React وتُستخدَم لإعادة تصيير (re-render) شجرة DOM بكفاءة عالية (بمعرفة الاختلافات بينها وبين شجرة DOM الحقيقية). Webpack Webpack هو مُحمِّل للوحدات (modules) والمجمِّع (bundler) لها، والذي يأخذ الوحدات (.js أو .css أو .txt …إلخ.) مع اعتمادياتها ويولِّد وسائط ساكنة (static assets) تُمثِّل هذه الوحدات. ترجمة -وبتصرف- للفصل React Semantics من كتاب React Enlightenment
-
React.js هي مكتبة JavaScript التي يمكن أن تستخدم لبناء واجهات المستخدم؛ فباستخدام React يمكن للمستخدمين إنشاء مكوِّنات قابلة لإعادة الاستخدام، وهذه المكونات تظهر البيانات أثناء تغيِّرها مع الزمن. يسمح لنا React Native بإنشاء تطبيقات أصيلة للهواتف الذكية باستخدام React. بكلمات أخرى، React هي أداة في JavaScript التي تُسهِّل إنشاء وصيانة واجهات المستخدم ذات الحالة (stateful) وعديمة الحالة (stateless)، وتوفر القدرة على تعريف وتقسيم واجهة المستخدم إلى مكوِّنات منفصلة (تسمى أيضًا بمكونات React) باستخدام عقد شبيهة بلغة HTML تسمى عقد React (أي React nodes). ستتحول عقد React في النهاية إلى صيغة قابلة للعرض في واجهات المستخدم (مثل HTML/DOM أو canvas أو SVG …إلخ.). يمكنني أن أستفيض بالشرح محاولًا تعريف React باستخدام الكلمات، لكنني أظن أنَّ من الأفضل أن أريك ما تفعله. لا تحاول أن تفهم كل التفاصيل الدقيقة أثناء شرحي لما بقي من هذا المقال، فالغرض من بقية هذه السلسلة أن تشرح لك بالتفصيل ما سيرد في هذه المقدمة. استخدام React لإنشاء مكونات شبيهة بعنصر select>> ما يلي هو عنصر <select> يحتوي على عناصر <option>. لحسن الحظ، الغرض من العنصر <select> معروفٌ لديك: <select size="4"> <option>Volvo</option> <option>Saab</option> <option selected>Mercedes</option> <option>Audi</option> </select> عندما يفسِّر المتصفح الشجرة السابقة من العناصر فسيُنتِج واجهة مستخدم تحتوي على قائمة نصية من العناصر التي يمكن اختيارها. أما في المتصفح، فشجرة DOM وشجرة DOM الظل (shadow DOM) تعملان معًا خلف الكواليس لتحويل العنصر <select> إلى مكوِّن UI. لاحظ أنَّ المكوِّن <select> يسمح للمستخدم باختيار أحد العناصر وبالتالي سيُخزِّن حالة ذاك الاختيار (أي انقر على Volvo وستختارها بدلًا من Mercedes). يمكننا باستخدام React إنشاء مكوِّن <select> خاص بنا باستخدام عقد React لإنشاء مكوِّن React والذي في النهاية سيُنتِج عناصر HTML في شجرة DOM. لنُنشِئ مكوِّنًا خاصًا بنا شبيهًا بالعنصر <select> باستخدام React. تعريف مكون React (أي React Component) سنُنشِئ فيما يلي مكوِّن React باشتقاق الصنف (class) React.Component لإنشاء المكوِّن MySelect. كما ترى، المكوِّن MySelect مُنشَأ من عدِّة أنماط إضافةً إلى عقدة <div> فارغة: class MySelect extends React.Component { // MySelect تعريف المكوِّن render(){ var mySelectStyle = { border: '1px solid #999', display: 'inline-block', padding: '5px' }; // JSX استخدام {} للإشارة إلى متغير جافاسكربت داخل // JSX باستخدام <div> سنعيد عنصر return <div style={mySelectStyle}></div>; } }; العنصر <div> السابق شبيهٌ بعناصر HTML العادية، وهو موجودٌ داخل شيفرة JavaScript التي تسمى JSX! صيغة JSX هي صياغة JavaScript اختيارية ومخصصة التي تستخدمها مكتبة React للتعبير عن عقد React التي يمكن أن ترتبط مع عناصر HTML حقيقية، أو عناصر مخصصة، أو عقد نصية. علينا ألّا نفترض أنَّ عقد React المُعرَّفة باستخدام JSX مماثلة تمامًا لعناصر HTML، فهنالك بعض الاختلافات بينها، وبعض القصور أيضًا. يجب تحويل صياغة JSX إلى شيفرات JavaScript حقيقية التي يمكن تفسيرها من محركات ECMAScript 5، فإن لم تحوّل الشيفرة السابقة فستسبب خطأً في JavaScript. الأداة الرسمية لتحويل شيفرات JSX إلى شيفرات JavaScript تسمى Babel. بعد أن يحوِّل Babel العنصر <div> في الشيفرة السابقة إلى شيفرة JavaScript فستبدو كما يلي: return React.createElement('div', { style: mySelectStyle }); بدلًا من: return <div style={mySelectStyle}></div>; في الوقت الحالي، ضع في ذهنك أنَّه عندما تكتب عناصر شبيهة بعناصر HTML في شيفرة React فستحوَّل في نهاية المطاف إلى شيفرة JavaScript حقيقية، إضافةً إلى تحويل أي شيفرة مكتوبة تحتوي على ميزات ECMAScript 6 وما بعدها إلى ECMAScript 5. المكوِّن <MySelect> يحتوي -عند هذه النقطة- على عقدة <div> فارغة فقط، أي أنَّ مكوِّن دون أي فائدة، لذا دعونا نغيِّر ذلك. سنُعرِّف مكونًا آخر باسم <MyOption> وسنستخدم المكوِّن <MyOption> داخل المكوِّن <MySelect> (ويسمى ذلك التركيب أي composition). تفحَّص شيفرة JavaScript المُحدَّثة الآتية التي تُعرِّف كلًا من مكونَي <MySelect> و <MyOption>: class MySelect extends React.Component { // MySelect تعريف المكوِّن render(){ var mySelectStyle = { border: '1px solid #999', display: 'inline-block', padding: '5px' }; // JSX استخدام {} للإشارة إلى متغير جافاسكربت داخل // <MyOption> يحتوي على المكون JSX باستخدام <div> إعادة عنصر return ( <div style={mySelectStyle}> <MyOption value="Volvo"></MyOption> <MyOption value="Saab"></MyOption> <MyOption value="Mercedes"></MyOption> <MyOption value="Audi"></MyOption> </div> ); } }; class MyOption extends React.Component { // MyOption تعريف المكون render(){ // JSX باستخدام <div> إعادة عنصر return <div>{this.props.value}</div>; } }; يفترض أنَّك لاحظت وجود المكوِّن <MyOption> داخل المكوِّن <MySelect> والذين أنشأناهما باستخدام JSX. تمرير خيارات المكون باستخدام خاصيات React لاحظ أنَّ المكوِّن <MyOption> يتألف من عنصر <div> يحتوي على التعبير {this.props.value}. تُستخدَم الأقواس المعقوفة {} داخل JSX للإشارة أنَّ محتواها هو تعبيرٌ صالحٌ في JavaScript. بعبارة أخرى، يمكننا أن نكتب شيفرات JavaScript عادية داخل القوسين {}. استخدمنا القوسين {} للوصول إلى الخاصيات المُمرَّرة إلى المكوِّن <MyOption>. بعبارةٍ أخرى، عندما يعرض المكوِّن <MyOption> فستوضع قيمة الخيار value التي جرى تمريرها عبر خاصيةٍ شبيهةٍ بخاصيات HTML (أي value="Volvo") داخل عنصر <div>. هذه الخاصيات التي تشبه خاصيات HTML تسمى خاصيات React، وتستخدمها مكتبة React لتمرير الخيارات التي لا تتغير إلى المكوِّنات، ومرَّرنا في مثالنا الخاصية value إلى المكوِّن <MyOption>، والأمر لا يختلف عن تمرير وسيط إلى دالة JavaScript، وهذا ما تفعله JSX خلف الكواليس. تصيير (Render) مكوِّن إلى شجرة DOM الافتراضية (Virtual DOM) ثم إلى شجرة DOM في هذه المرحلة، عرَّفنا مكوِّنين من مكونات React، لكننا لم نصيِّرها إلى شجرة DOM الافتراضية ومنها إلى شجرة HTML DOM. قبل أن نفعل ذلك، أود أن أشير إلى أنَّ كل ما فعلناه هو تعريف مكونين باستخدام JavaScript. وكل ما فعلناه -نظريًا- هو تعريف مكونات UI، وليس من الضروري أن تذهب هذه المكونات إلى شجرة DOM أو حتى إلى شجرة DOM الافتراضية (Virtual DOM). ويمكننا -نظريًا- أن نصيّر (render) هذه المكونات إلى منصة من منصات الهواتف الذكية أو إلى العنصر <canvas>)، لكننا لن نفعل ذلك هنا. تذكّر أنَّ استخدام React يمنحنا تنظيمًا لعناصر واجهة المستخدم التي يمكن تحويلها إلى شجرة DOM أو تطبيقاتٍ أخرى. لنصيّر الآن المكوِّن <MySelect> إلى شجرة DOM الافتراضية والتي بدورها ستصيّر إلى شجرة DOM الأساسية داخل صفحة HTML. في شيفرة JavaScript التالية، ستلاحظ أننا أضفنا استدعاءً للدالة ReactDOM.render() في آخر سطر، ومررنا إلى الدالة ReactDOM.render() المكوِّن الذي نريد تصييره (وهو <MySelect>) ومرجعية إلى عنصر HTML موجودٌ في شجرة HTML DOM (وهو <div id="app"></div>) الذي نريد عرض المكوِّن <MySelect> فيه. class MySelect extends React.Component { // MySelect تعريف المكوِّن render(){ var mySelectStyle = { border: '1px solid #999', display: 'inline-block', padding: '5px' }; // JSX استخدام {} للإشارة إلى متغير جافاسكربت داخل // <MyOption> يحتوي على المكون JSX باستخدام <div> إعادة عنصر return ( <div style={mySelectStyle}> <MyOption value="Volvo"></MyOption> <MyOption value="Saab"></MyOption> <MyOption value="Mercedes"></MyOption> <MyOption value="Audi"></MyOption> </div> ); } }; class MyOption extends React.Component { // MyOption تعريف المكون render(){ // JSX باستخدام <div> إعادة عنصر return <div>{this.props.value}</div>; } }; ReactDOM.render(<MySelect />, document.getElementById('app')); لاحظ أنَّ كل ما فعلناه هو إخبار React أين ستبدأ بتصيير المكونات وما هي المكونات التي عليها بدء التصيير بها. بعد ذلك ستصيّر React أيّة مكونات محتواة داخل المكوِّن الأصلي (مثل المكوِّن <MyOption> داخل <MySelect>). انتظر لحظة! ربما تفكِّر الآن أننا لم نُنشِئ العنصر <select> أصلًا، وكل ما فعلناه هو إنشاء قائمة ثابتة عديم الحالة من السلاسل النصية. سنصلح ذلك في الخطوة القادمة. قبل أن نكمل إلى الخطوة القادمة، أحب أن أشير إلى عدم وجود أي تعاملات ضمنية مع شجرة DOM لكي نعرض المكوِّن في شجرة DOM. بعبارةٍ أخرى، لم نستدعِ شيفرة jQuery أثناء إنشاء هذا المكوِّن؛ وجميع التعاملات مع شجرة DOM الفعلية قد أصبحت مجردةً (abstract) عبر استعمال شجرة DOM الافتراضية الخاصة بمكتبة React. في الواقع، عندما نستخدم React فما نفعله هو وصف شجرة DOM الافتراضية التي تأخذها React وتحوِّلها إلى شجرة DOM الفعلية لنا. استخدام حالة React (أي React state) لكي نجعل عنصر <MySelect> الخاص بنا يحاكي عنصر <select> الأصلي في HTML فعلينا أن نضيف حالةً (state) له. فما فائدة عنصر <select> المخطط إذا لم يكن قادرًا على الاحتفاظ بقيمة الاختيار الذي اخترناه. تأتي الحالة (state) عندما يحتوي المكوِّن على نسخة من المعلومات. وبخصوص عنصر <MyOption> المخصص، الحالة هي النص المختار حاليًا أو عدم وجود نص مختار من الأساس. لاحظ أنَّ الحالة تتضمن عادةً أحداثًا تابعة للمستخدم (مثل الفأرة أو لوحة المفاتيح أو حافظة النسخ …إلخ.) أو أحداثًا تابعة للشبكة (أي AJAX) وتستخدم قيمتها لتحديد إن كانت واجهة المستخدم للمكوِّن تحتاج إلى إعادة تصيير (re-render، فتغيير القيمة سيؤدي إلى إعادة التصيير). ترتبط الحالة عادةً بأعلى مكوِّن الذي يُنشِئ مكوِّن UI. كنا في السابق نستخدم الدالة getInitialState() في React لنستطيع ضبط الحالة الافتراضية، فلو أردنا ضبط حالة المكون إلى false (أي لا يوجد أي نص مختار) فسنعيد كائن حالة عند استدعاء الدالة getInitialState() (أي return {selected: false};). دورة حياة الدالة getInitialState() هي استدعاء الدالة مرةً قبل تركيب المكوِّن، وستُستخدَم القيمة المعادة منها كقيمة افتراضية للخاصية this.state. الطريقة السابقة قديمة ولم تعد مستخدمةً إلا إذا كنتَ من محبي الوحدة create-react-class والتي سنأتي على ذكرها لاحقًا، أما في الأصناف في ES6، فنحن نستعمل this.state ضمن الدالة البانية (constructor) للصنف الخاص بالمكون. أي أننا سنكتب في الدالة البانية للصنف MySelect تعريفًا للحالة التي نريدها. هذه نسخة مُحدَّثة من الشيفرة أضفنا فيها الحالة إلى المكوِّن، أنصحك بقراءة التعليقات التي أضعها في الشيفرة والتي تجذب انتباهك إلى التغييرات التي حدثت في الشيفرة. class MySelect extends React.Component { constructor(){ // إضافة الحالة الافتراضية super(); this.state = {selected: false}; // this.state.selected = false; } render(){ var mySelectStyle = { border: '1px solid #999', display: 'inline-block', padding: '5px' }; return ( <div style={mySelectStyle}> <MyOption value="Volvo"></MyOption> <MyOption value="Saab"></MyOption> <MyOption value="Mercedes"></MyOption> <MyOption value="Audi"></MyOption> </div> ); } }; class MyOption extends React.Component { render(){ return <div>{this.props.value}</div>; } }; ReactDOM.render(<MySelect />, document.getElementById('app')); بعد ضبط الحالة الافتراضية، سنستدعي دالة رد نداء (callback function) باسم select التي ستُطلَق عندما يضغط المستخدم على خيارٍ ما. داخل هذه الدالة سنحصل على نص الخيار الذي اختاره المستخدم (عبر المعامل event) وسنستخدمه لضبط الحالة setState للمكوِّن الحالي. لاحظ أننا نستخدم تفاصيل الكائن event المُمرَّر إلى دالة رد النداء select. يُفترَض أنَّ هذا النمط من البرمجة مألوفٌ لديك إن كانت لديك أيّ خبرة مع مكتبة jQuery من قبل. من أهم ما يجب ملاحظته في الشيفرة الآتية هو اتباع التوابع في مكوّنات React المُعرَّفة كأصناف ES6 لنفس القواعد في أصناف ES6 الاعتيادية، يعني هذا أنّها لا تربط this بنسخة الكائن، بل يجب عليك أن تستخدم بشكل صريح التابع .bind(this) في الدالة البانية: class MySelect extends React.Component { constructor(){ // إضافة الحالة الافتراضية super(); this.state = {selected: false}; // this.state.selected = false; this.select = this.select.bind(this); // هذا السطر مهم، راجع الشرح أعلاه } select(event){ // select إضافة الدالة if(event.target.textContent === this.state.selected){ // إزالة التحديد this.setState({selected: false}); // تحديث الحالة }else{ // إضافة التحديد this.setState({selected: event.target.textContent}); // تحديث الحالة } } render(){ var mySelectStyle = { border: '1px solid #999', display: 'inline-block', padding: '5px' }; return ( <div style={mySelectStyle}> <MyOption value="Volvo"></MyOption> <MyOption value="Saab"></MyOption> <MyOption value="Mercedes"></MyOption> <MyOption value="Audi"></MyOption> </div> ); } }; class MyOption extends React.Component { render(){ return <div>{this.props.value}</div>; } }; ReactDOM.render(<MySelect />, document.getElementById('app')); ولكي تحصل مكوِّنات <MyOption> على وصولٍ للدالة select فمررنا إشارةً مرجعيةً إليها عبر خاصيات React (props) من المكوِّن <MySelect> إلى المكوِّن <MyOption>. ولفعل ذلك أضفنا select={this.select} إلى مكونات <MyOption>. بعد ضبط ما سبق، يمكننا إضافة onClick={this.props.select} إلى المكوِّن <MyOption>. أرجو أن يكون واضحًا أنَّ ما فعلناه هو ربط الحدث click الذي سيستدعي الدالة select. تتكفّل React بربط دالة التعامل مع حدث النقر الحقيقي في شجرة DOM نيابةً عنّا. class MySelect extends React.Component { constructor(){ super(); this.state = {selected: false}; this.select = this.select.bind(this); } select(event){ if(event.target.textContent === this.state.selected){ this.setState({selected: false}); }else{ this.setState({selected: event.target.textContent}); } } render(){ var mySelectStyle = { border: '1px solid #999', display: 'inline-block', padding: '5px' }; return ( <div style={mySelectStyle}> <MyOption select={this.select} value="Volvo"></MyOption> <MyOption select={this.select} value="Saab"></MyOption> <MyOption select={this.select} value="Mercedes"></MyOption> <MyOption select={this.select} value="Audi"></MyOption> </div> ); } }; class MyOption extends React.Component { render(){ return <div onClick={this.props.select}>{this.props.value}</div>; } }; ReactDOM.render(<MySelect />, document.getElementById('app')); بعد فعلنا لذلك، يمكننا الآن ضبط الحالة بالنقر على أحد الخيارات؛ وبعبارةٍ أخرى، عندما تنقر على خيارٍ ما فستستدعى الدالة select وتضبط حالة المكوِّن <MySelect>. لكن مستخدم المكوِّن لن يعرف أبدًا أنَّ ذلك قد حصل لأنَّ كل ما فعلناه حتى الآن هو تغيير حالة المكوِّن، ولا توجد أي تغذية بصرية تشير إلى اختيار أي عنصر. لذا لنصلح ذلك. ما علينا فعله الآن هو تمرير الحالة الراهنة إلى المكوِّن <MyOption> لكي يستجيب -بصريًا- إلى تغيير حالة المكوِّن. باستخدام الخاصيات عبر props، سنُمرِّر الحالة selected من المكوِّن <MySelect> إلى المكوِّن <MyOption> بوضع الخاصية state={this.state.selected} في جميع مكونات <MyOption>. أصبحنا نعلم الآن ما هي الحالة (أي this.props.state) والقيمة الحالية (أي this.props.value) للخيار لكي نتحقق إذا كانت الحالة تُطابِق القيمة الموجودة في مكوِّن <MyOption> ما. وإذا كانت تطابقها، فسنعلم أنَّه يجب تحديد هذا الخيار، وسنفعل ذلك باستخدام عبار if بسيطة التي تضيف أنماط تنسيق (selectedStyle) إلى عنصر <div> في JSX إذا كانت الحالة تُطابِق قيمة الخيار الحالي. وفيما عدا ذلك، سنعيد عنصر React مع النمط unSelectedStyle: class MySelect extends React.Component { constructor(){ super(); this.state = {selected: false}; this.select = this.select.bind(this); } select(event){ if(event.target.textContent === this.state.selected){ this.setState({selected: false}); }else{ this.setState({selected: event.target.textContent}); } } render(){ var mySelectStyle = { border: '1px solid #999', display: 'inline-block', padding: '5px' }; return ( <div style={mySelectStyle}> <MyOption state={this.state.selected} select={this.select} value="Volvo"></MyOption> <MyOption state={this.state.selected} select={this.select} value="Saab"></MyOption> <MyOption state={this.state.selected} select={this.select} value="Mercedes"></MyOption> <MyOption state={this.state.selected} select={this.select} value="Audi"></MyOption> </div> ); } }; class MyOption extends React.Component { render(){ var selectedStyle = {backgroundColor:'red', color:'#fff',cursor:'pointer'}; var unSelectedStyle = {cursor:'pointer'}; if(this.props.value === this.props.state){ return <div style={selectedStyle} onClick={this.props.select}>{this.props.value}</div>; }else{ return <div style={unSelectedStyle} onClick={this.props.select}>{this.props.value}</div>; } } }; ReactDOM.render(<MySelect />, document.getElementById('app')); صحيحٌ أنَّ عنصر <select> الذي أنشأناه ليس جميلًا أو كاملًا كما كنتَ ترجو، لكنني أظن أنَّك ترى ما الغرض الذي حققناه. مكتبة React تسمح لك بالتفكير بالعناصر بطريقة منظمة ومهيكلة هيكليةً صحيحة. قبل الانتقال إلى شرح دور شجرة DOM الافتراضية، أود أنَّ أوضِّح أنَّه من غير الضروري استخدام JSX و Babel. يمكنك تخطي هذه الأدوات واستخدام شيفرات JavaScript مباشرة. سأريك نسخةً أخيرةً من الشيفرة بعد تحويل JSX باستخدام Babel. إذا لم ترغب باستخدام JSX فيمكنك أن تكتب الشيفرة الآتية يدويًا بدلًا من الشيفرة التي كتبناها خلال هذا المقال: class MySelect extends React.Component { constructor() { super(); this.state = { selected: false }; this.select = this.select.bind(this); } select(event) { if (event.target.textContent === this.state.selected) { this.setState({ selected: false }); } else { this.setState({ selected: event.target.textContent }); } } render() { var mySelectStyle = { border: '1px solid #999', display: 'inline-block', padding: '5px' }; return React.createElement("div", { style: mySelectStyle }, React.createElement(MyOption, { state: this.state.selected, select: this.select, value: "Volvo" }), React.createElement(MyOption, { state: this.state.selected, select: this.select, value: "Saab" }), React.createElement(MyOption, { state: this.state.selected, select: this.select, value: "Mercedes" }), React.createElement(MyOption, { state: this.state.selected, select: this.select, value: "Audi" })); } }; class MyOption extends React.Component { render() { var selectedStyle = { backgroundColor: 'red', color: '#fff', cursor: 'pointer' }; var unSelectedStyle = { cursor: 'pointer' }; if (this.props.value === this.props.state) { return React.createElement("div", { style: selectedStyle, onClick: this.props.select }, this.props.value); } else { return React.createElement("div", { style: unSelectedStyle, onClick: this.props.select }, this.props.value); } } }; ReactDOM.render(React.createElement(MySelect, null), document.getElementById('app')); فهم دور شجرة DOM الافتراضية (virtual DOM) سأنهي جولتنا بأكثر جوانب React حديثًا بين المطورين، إذ سأتحدث عن شجرة DOM الافتراضية (React virtual DOM). لاحظنا -عبر الأمثلة في هذا المقال- أنَّ التعامل الوحيد مع شجرة DOM الحقيقية أثناء إنشائنا لعنصر <select> خاص بنا هو عندما أخبرنا الدالة ReactDOM.render() أين ستعرض مكوِّنات UI في صفحة HTML (أي عندما عرضناها في <div id="app"></div>). من المرجح أن يكون هذا تعاملك الوحيد مع شجرة DOM الحقيقية عندما تبني تطبيق React من شجرة من المكوِّنات. وهنا تأتي قيمة مكتبة React. فعند استخدامك لها، ليس عليك أن تفكر بشجرة DOM بنفس الطريقة التي كنتَ تفكِّر فيها عند كتابتك لشيفرة jQuery. فمكتبة React تستبدل jQuery عبر تجريد استخدام شجرة DOM. ولأنَّ شجرة DOM الافتراضية حلّت محل شجرة DOM الحقيقية، سمح ذلك بإجراء تحديثات لشجرة DOM الحقيقية مع أداءٍ ممتاز. تبقي شجرة DOM الافتراضية سجلًا بجميع التغيرات في واجهة المستخدم اعتمادًا على الحالة والخاصيات (state و props)، ثم تقارنها بشجرة DOM الحقيقية وتجري أقل مقدار ممكن من التعديلات عليها. بصيغةٍ أخرى، لا تُحدَّث شجرة DOM الحقيقية إلا بأقل قدر ممكن وذلك عند تغيير الحالة أو الخاصيات. صراحةً، هذا المفهوم ليس ثوريًا أو جديدًا، يمكنك فعل المثل باستخدام شيفرة jQuery مكتوبة بعناية، لكنك لن تحتاج إلى التفكير بهذه الأمور عند استخدام React. فشجرة DOM الافتراضية تجري عمليات تحسين الأداء عوضًا عنك، فلا حاجة لأن تقلق حول أي شيء، فكل ذلك يحدث وراء الكواليس ونادرًا ما تحتاج إلى التعامل مع شجرة DOM الحقيقية نفسها. أرغب أن أنهي هذه المقدمة بالقول أنَّ استخدام React يلغي تقريبًا الحاجة إلى استخدام أي مكتبات أخرى مثل jQuery. واستخدام شجرة DOM الافتراضية يريحنا من كثيرٍ من التفاصيل الدقيقة، لكن قيمة مكتبة React لا تكمن في شجرة DOM الافتراضية فقط، وإنما يمكننا أن نعدّ شجرة DOM الافتراضية على أنها الفستق الحلبي المبشور فوق الحلوى؛ فببساطة، قيمة مكتبة React تكون في أنها توفِّر طريقةً سهلةً الإنشاء والصيانة لإنشاء شجرة من مكوِّنات الواجهة الرسومية. تخيل بساطة إنشاء واجهة رسومية إذا بنيتَ تطبيقك باستخدام مكوِّنات React القابلة لإعادة الاستخدام. تذكر هذه السلسلة عندما تريد أن تعرِّف ما هي React. مكتبة React.js هي مكتبة JavaScript التي يمكن استخدامها لبناء واجهات المستخدم، وباستخدام React يمكن للمطورين إنشاء مكونات قابلة لإعادة الاستخدام، وهذه المكونات تُظهِر البيانات وتستطيع تغييرها مع الزمن؛ وتوجد أيضًا مكتبة React Native لبناء تطبيقات للهواتف الذكية باستخدام React. ترجمة وبتصرف للفصل What is React? من كتاب React Enlightenment
-
يسمح لك استخدام حلقات for أو while في بايثون بأتمتة وتكرار المهام بطريقة فعّالة. لكن في بعض الأحيان، قد يتدخل عامل خارجي في طريقة تشغيل برنامجك، وعندما يحدث ذلك، فربما تريد من برنامجك الخروج تمامًا من حلقة التكرار، أو تجاوز جزء من الحلقة قبل إكمال تنفيذها، أو تجاهل هذا العامل الخارجي تمامًا. لذا يمكنك فعل ما سبق باستخدام تعابير break و continue و pass. التعبير break يوفِّر لك التعبير break القدرة على الخروج من حلقة التكرار عند حدوث عامل خارجي. حيث عليك وضع التعبير break في الشيفرة التي ستُنفَّذ في كل تكرار للحلقة، ويوضع عادةً ضمن تعبير if. ألقِ نظرةً إلى أحد الأمثلة الذي يستعمل التعبير break داخل حلقة for: number = 0 for number in range(10): number = number + 1 if number == 5: break # break here print('Number is ' + str(number)) print('Out of loop') هذا برنامجٌ صغيرٌ، هيّأنا في بدايته المتغير number بجعله يساوي الصفر، ثم بنينا حلقة تكرار for التي تعمل لطالما كانت قيمة المتغير number أصغر من 10. ثم قمنا بزيادة قيمة المتغير number داخل حلقة for بمقدار 1 في كل تكرار، وذلك في السطر number = number + 1. ثم كانت هنالك عبارة if التي تختبر إن كان المتغير number مساوٍ للرقم 5، وعند حدوث ذلك فسيُنفَّذ التعبير break للخروج من الحلقة. وتوجد داخل حلقة التكرار الدالة print() التي تُنفَّذ في كل تكرار إلى أن نخرج من الحلقة عبر التعبير break، وذلك لأنَّها موجودة بعد التعبير break. لكي نتأكد أننا خرجنا من الحلقة، فوضعنا عبارة print() أخيرة موجودة خارج حلقة for. سنرى الناتج الآتي عند تنفيذ البرنامج: Number is 1 Number is 2 Number is 3 Number is 4 Out of loop الناتج السابق يُظهِر أنَّه بمجرد أن أصبح العدد الصحيح number مساويًا للرقم 5، فسينتهي تنفيذ حلقة التكرار عبر التعبير break. الخلاصة: التعبير break يؤدي إلى الخروج من حلقة التكرار. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن التعبير continue التعبير continue يسمح لنا بتخطي جزء من حلقة التكرار عند حدوث عامل خارجي، لكن إكمال بقية الحلقة إلى نهايتها. بعبارةٍ أخرى: سينتقل تنفيذ البرنامج إلى أوّل حلقة التكرار عند تنفيذ التعبير continue. يجب وضع التعبير continue في الشيفرة التي ستُنفَّذ في كل تكرار للحلقة، ويوضع عادةً ضمن تعبير if. سنستخدم نفس البرنامج الذي استعملناها لشرح التعبير break أعلاه، لكننا سنستخدم التعبير continue بدلًا من break: number = 0 for number in range(10): number = number + 1 if number == 5: continue # continue here print('Number is ' + str(number)) print('Out of loop') الفرق بين استخدام التعبير continue بدلًا من break هو إكمال تنفيذ الشيفرة بغض النظر عن التوقف الذي حدث عندما كانت قيمة المتغير number مساويةً إلى الرقم 5. لننظر إلى الناتج: Number is 1 Number is 2 Number is 3 Number is 4 Number is 6 Number is 7 Number is 8 Number is 9 Number is 10 Out of loop نلاحظ أنَّ السطر الذي يجب أن يحتوي على Number is 5 ليس موجودًا في المخرجات، لكن سيُكمَل تنفيذ حلقة التكرار بعد هذه المرحلة مما يطبع الأرقام من 6 إلى 10 قبل إنهاء تنفيذ الحلقة. يمكنك استخدام التعبير continue لتفادي استخدام تعابير شرطية معقدة ومتشعّبة، أو لتحسين أداء البرنامج عن طريق تجاهل الحالات التي ستُرفَض نتائجها. الخلاصة: التعبير continue سيؤدي إلى جعل البرنامج يتجاهل تنفيذ حلقة التكرار عند تحقيق شرط معين، لكن بعدئذٍ سيُكمِل تنفيذ الحلقة كالمعتاد. التعبير pass التعبير pass يسمح لنا بالتعامل مع أحد الشروط دون إيقاف عمل حلقة التكرار بأي شكل، أي ستُنفَّذ جميع التعابير البرمجية الموجودة في حلقة التكرار ما لم تستعمل تعابير مثل break أو continue فيها. وكما هو الحال مع التعابير السابقة، يجب وضع التعبير pass في الشيفرة التي ستُنفَّذ في كل تكرار للحلقة، ويوضع عادةً ضمن تعبير if. سنستخدم نفس البرنامج الذي استعملناها لشرح التعبير break أو continue أعلاه، لكننا سنستخدم التعبير pass هذه المرة: number = 0 for number in range(10): number = number + 1 if number == 5: pass # pass here print('Number is ' + str(number)) print('Out of loop') التعبير pass الذي يقع بعد العبارة الشرطية if يخبر البرنامج أنَّ عليه إكمال تنفيذ الحلقة وتجاهل مساواة المتغير number للرقم 5. لنشغِّل البرنامج ولننظر إلى الناتج: Number is 1 Number is 2 Number is 3 Number is 4 Number is 5 Number is 6 Number is 7 Number is 8 Number is 9 Number is 10 Out of loop لاحظنا عند استخدامنا للتعبير pass في هذا البرنامج أنَّ البرنامج يعمل كما لو أننا لم نضع عبارة شرطية داخل حلقة التكرار؛ حيث يخبر التعبير pass البرنامج أن يكمل التنفيذ كما لو أنَّ الشرط لم يتحقق. يمكن أن تستفيد من التعبير pass عندما تكتب برنامجك لأوّل مرة أثناء تفكيرك بحلّ مشكلة ما عبر خوارزمية، لكن قبل أن تضع التفاصيل التقنية له. الخلاصة تسمح لك التعابير break و continue و pass باستعمال حلقات for و while بطريقةٍ أكثر كفاءة. ترجمة –وبتصرّف– للمقال How To Use Break, Continue, and Pass Statements when Working with Loops in Python 3 لصاحبته Lisa Tagliaferri اقرأ أيضًا الدرس التالي: كيفية تعريف الدوال في بايثون 3 الدرس السابق: كيفية إنشاء حلقات تكرار for في بايثون 3 المرجع الشامل إلى تعلم لغة بايثون كتاب البرمجة بلغة بايثون
-
يسمح لنا استخدام حلقات التكرار في برمجة الحاسوب بأتمتة وتكرار المهام المتشابهة مرّاتٍ عدِّة. وسنشرح في هذا الدرس كيفية استخدام حلقة for في بايثون. حلقة for تؤدي إلى تكرار تنفيذ جزء من الشيفرات بناءً على عدّاد أو على متغير، وهذا يعني أنَّ حلقات for تستعمل عندما يكون عدد مرات تنفيذ حلقة التكرار معلومًا قبل الدخول في الحلقة، وذلك على النقيض من حلقات while المبنية على شرط. حلقات for تُبنى حلقات for في بايثون كما يلي: for [iterating variable] in [sequence]: [do something] ستُنفَّذ الشيفرات الموجودة داخل حلقة التكرار عدِّة مرات إلى أن تنتهي الحلقة. لننظر إلى كيفية مرور الحلقة for على مجالٍ من القيم: for i in range(0,5): print(i) سيُخرِج البرنامج السابق عند تشغيله الناتج الآتي: 0 1 2 3 4 ضبطنا المتغير i في حلقة for ليحتوي على القيمة التي ستُنفَّذ عليها حلقة التكرار، وكان مجال القيم التي ستُسنَد إلى هذا المتغير من 0 إلى 5. ثم طبعًا قيمة المتغير في كل دوران لحلقة التكرار، لكن أبقِ في ذهنك أنَّنا نميل إلى بدء العد من الرقم 0 في البرمجة، وعلى الرغم من عرض خمسة أرقام، لكنها تبدأ بالرقم 0 وتنتهي بالرقم 4. من الشائع أن ترى استخدامًا لحلقة for عندما تحتاج إلى تكرار كتلة معيّنة من الشيفرات لعددٍ من المرات. استخدام حلقات التكرار مع الدالة range() إحدى أنواع السلاسل غير القابلة للتعديل في بايثون هي تلك الناتجة من الدالة range()، وتستخدم الدالة range() في حلقات التكرار للتحكم بعدد مرات تكرار الحلقة. عند التعامل مع الدالة range() عليك أن تمرر معاملًا رقميًا أو معاملين أو ثلاثة معاملات: start يشير إلى القيم العددية الصيحية التي ستبدأ بها السلسلة، وإذا لم تُمرَّر قيمة لهذا المعامل فستبدأ السلسلة من 0 stop هذا المعامل مطلوب دومًا وهو القيمة العددية الصحيحة التي تمثل نهاية السلسلة العددية لكن دون تضمينها step هي مقدار الخطوة، أي عدد الأرقام التي يجب زيادتها (أو إنقاصها إن كنّا نتعامل مع أرقام سالبة) في الدورة القادمة، وقيمة المعامل step تساوي 1 في حال لم تُحدَّد له قيمة لننظر إلى بعض الأمثلة التي نُمرِّر فيها مختلف المعاملات إلى الدالة range(). لنبدأ بتمرير المعامل stop فقط، أي أنَّ السلسلة الآتية من الشكل range(stop): for i in range(6): print(i) كانت قيمة المعامل stop في المثال السابق مساويةً للرقم 6، لذا ستمر حلقة التكرار من بداية المجال 0 إلى نهايته 6 (باستثناء الرقم 6 كما ذكرنا أعلاه): 0 1 2 3 4 5 المثال الآتي من الشكل range(start ,stop) الذي تُمرَّر قيم بدء السلسلة ونهايتها: for i in range(20,25): print(i) المجال –في المثال السابق– يتراوح بين 20 (بما فيها الرقم 20) إلى 25 (باستثناء الرقم 25)، لذا سيبدو الناتج كما يلي: 20 21 22 23 24 الوسيط step الخاص بالدالة range() شبيه بمعامل الخطوة الذي نستعمله عند تقسيم [السلاسل النصية](آلية فهرسة السلاسل النصية وطريقة تقسيمها في بايثون 3) لأنه يستعمل لتجاوز بعض القيم ضمن السلسلة. يأتي المعامل step في آخر قائمة المعاملات التي تقبلها الدالة range() وذلك بالشكل الآتي range(start, stop, step). لنستعمل المعامل step مع قيمة موجبة: for i in range(0,15,3): print(i) سيؤدي المثال السابق إلى إنشاء سلسلة من الأرقام التي تبدأ من 0 وتنتهي عند 15 لكن قيمة المعامل step هي 3، لذا سيتم تخطي رقمين في كل دورة، أي سيكون الناتج كالآتي: 0 3 6 9 12 يمكننا أيضًا استخدام قيمة سالبة للمعامل step للدوران إلى الخلف، لكن علينا تعديل قيم start و stop بما يتوافق مع ذلك: for i in range(100,0,-10): print(i) قيمة المعامل start في المثال السابق هي 100، وكانت قيمة المعامل stop هي 0، والخطوة هي -10، لذا ستبدأ السلسلة من الرقم 100 وستنتهي عند الرقم 0، وسيكون التناقص بمقدار 10 في كل دورة، ويمكننا ملاحظة ذلك في الناتج الآتي: 100 90 80 70 60 50 40 30 20 10 الخلاصة: عندما نبرمج باستخدام لغة بايثون، فسنجد أننا نستفيد كثيرًا من السلاسل الرقمية التي تنتجها الدالة range(). دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن استخدام حلقة for مع أنواع البيانات المتسلسلة يمكن الاستفادة من القوائم (من النوع list) وغيرها من أنواع البيانات المتسلسلة واستعمالها كمعاملات لحلقات for، فبدلًا من الدوران باستخدام الدالة range() فيمكننا تعريف قائمة ثم الدوران على عناصرها. سنُسنِد في المثال الآتي قائمةً إلى متغير، ثم سنستخدم حلقة for للدوران على عناصر القائمة: sharks = ['hammerhead', 'great white', 'dogfish', 'frilled', 'bullhead', 'requiem'] for shark in sharks: print(shark) في هذه الحالة، قمنا بطباعة كل عنصر موجود في القائمة؛ وصحيحٌ أننا استعملنا الكلمة shark كاسم للمتغير، لكن يمكنك استعمال أي اسم صحيح آخر ترغب به، وستحصل على نفس النتيجة: hammerhead great white dogfish frilled bullhead requiem الناتج السابق يُظهِر دوران الحلقة for على جميع عناصر القائمة مع طباعة كل عنصر في سطرٍ منفصل. يشيع استخدام القوائم والأنواع الأخرى من البيانات المتسلسلة مثل السلاسل النصية وبنى tuple مع حلقات التكرار لسهولة الدوران على عناصرها. يمكنك دمج هذه الأنواع من البيانات مع الدالة range() لإضافة عناصر إلى قائمة، مثلًا: sharks = ['hammerhead', 'great white', 'dogfish', 'frilled', 'bullhead', 'requiem'] for item in range(len(sharks)): sharks.append('shark') print(sharks) الناتج: ['hammerhead', 'great white', 'dogfish', 'frilled', 'bullhead', 'requiem', 'shark', 'shark', 'shark', 'shark', 'shark', 'shark'] أضفنا هنا السلسلة النصية 'shark' خمس مرات (وهو نفس طول القائمة sharks الأصلي) إلى القائمة sharks. يمكننا استخدام حلقة for لبناء قائمة جديدة: integers = [] for i in range(10): integers.append(i) print(integers) هيّئنا في المثال السابق قائمةً فارغةً باسم integers لكن حلقة التكرار for ملأت القائمة لتصبح كما يلي: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] وبشكلٍ شبيهٍ بما سبق، يمكننا الدوران على السلاسل النصية: sammy = 'Sammy' for letter in sammy: print(letter) الناتج: S a m m y يمكن الدوران على بنى tuple كما هو الحال في القوائم والسلاسل النصية. عند المرور على عناصر نوع البيانات dictionary، فمن المهم أن تبقي بذهنك البنية الخاصة به (key:value) لكي تضمن أنَّك تستدعي العنصر الصحيح من المتغير. هذا مثالٌ بسيطٌ نعرض فيه المفتاح (key) والقيمة (value): sammy_shark = {'name': 'Sammy', 'animal': 'shark', 'color': 'blue', 'location': 'ocean'} for key in sammy_shark: print(key + ': ' + sammy_shark[key]) الناتج: name: Sammy animal: shark location: ocean color: blue عند استخدام متغيرات من النوع dictionary مع حلقات for فيكون المتغير المرتبط بحلقة التكرار متعلقًا بمفتاح القيم، وعلينا استخدام التعبير dictionary_variable[iterating_variable] للوصول إلى القيمة الموافقة للمفتاح. ففي المثال السابق كان المتغير المرتبط بحلقة التكرار باسم key وهو يُمثِّل المفاتيح، واستعملنا التعبير sammy_shark[key] للوصول إلى القيمة المرتبطة بذاك المفتاح. الخلاصة: تُستعمَل حلقات التكرار عادةً للدوران على عناصر البيانات المتسلسلة وتعديلها. حلقات for المتشعّبة يمكن تشعّب حلقات التكرار في بايثون، كما هو الحال في بقية لغات البرمجة. حلقة التكرار المتشعبة هي الحلقة الموجودة ضمن حلقة تكرار أخرى، وهي شبيهة بعبارات if المتشعّبة. تُبنى حلقات التكرار المتشعبة كما يلي: for [first iterating variable] in [outer loop]: # Outer loop [do something] # Optional for [second iterating variable] in [nested loop]: # Nested loop [do something] يبدأ البرنامج بتنفيذ حلقة التكرار الخارجية، ويُنفَّذ أوّل دوران فيها، وأوّل دوران سيؤدي إلى الدخول إلى حلقة التكرار الداخلية، مما يؤدي إلى تنفيذها إلى أن تنتهي تمامًا. ثم سيعود تنفيذ البرنامج إلى بداية حلقة التكرار الخارجية، ويبدأ بتنفيذ الدوران الثاني، ثم سيصل التنفيذ إلى حلقة التكرار الداخلية، وستُنفَّذ حلقة التكرار الداخلية بالكامل، ثم سيعود التنفيذ إلى بداية حلقة التكرار الخارجية، وهلّم جرًا إلى أن ينتهي تنفيذ حلقة التكرار الخارجية أو إيقاف حلقة التكرار عبر استخدام [التعبير break](كيفية استخدام تعابير break و continue و pass عند التعامل مع حلقات التكرار في بايثون 3) أو غيره من التعابير. لنُنشِئ مثالًا يستعمل حلقة forمتشعبة لكي نفهم كيف تعمل بدقة. حيث ستمر حلقة التكرار الخارجية في المثال الآتي على قائمة من الأرقام اسمها num_list، أما حلقة التكرار الداخلية فستمر على قائمة من السلاسل النصية اسمها alpha_list: num_list = [1, 2, 3] alpha_list = ['a', 'b', 'c'] for number in num_list: print(number) for letter in alpha_list: print(letter) سيظهر الناتج الآتي عند تشغيل البرنامج: 1 a b c 2 a b c 3 a b c يُظهِر الناتج السابق أنَّ البرنامج أكمل أوّل دوران على عناصر حلقة التكرار الخارجية بطباعة الرقم 1، ومن ثم بدأ تنفيذ حلقة التكرار الدخلية مما يطبع الأحرف a و b و c على التوالي. وبعد انتهاء تنفيذ حلقة التكرار الداخلية، فعاد البرنامج إلى بداية حلقة التكرار الخارجية طابعًا الرقم 2، ثم بدأ تنفيذ حلقة التكرار الداخلية (مما يؤدي إلى إظهار a و b و c مجددًا). وهكذا. يمكن الاستفادة من حلقات for المتشعبة عند المرور على عناصر قوائم تتألف من قوائم. فلو استعملنا حلقة تكرار وحيدة لعرض عناصر قائمة تتألف من عناصر تحتوي على قوائم، فستُعرَض قيم القوائم الداخلية: list_of_lists = [['hammerhead', 'great white', 'dogfish'],[0, 1, 2],[9.9, 8.8, 7.7]] for list in list_of_lists: print(list) الناتج: ['hammerhead', 'great white', 'dogfish'] [0, 1, 2] [9.9, 8.8, 7.7] وفي حال أردنا الوصول إلى العناصر الموجودة في القوائم الداخلية، فيمكننا استعمال حلقة for متشعبة: list_of_lists = [['hammerhead', 'great white', 'dogfish'],[0, 1, 2],[9.9, 8.8, 7.7]] for list in list_of_lists: for item in list: print(item) الناتج: hammerhead great white dogfish 0 1 2 9.9 8.8 7.7 الخلاصة: نستطيع الاستفادة من حلقات for المتشعبة عندما نريد الدوران على عناصر محتوى في قوائم. الخلاصة رأينا في هذا الدرس كيف تعمل حلقة التكرار for في لغة بايثون، وكيف نستطيع إنشاءها واستعمالها. حيث تستمر حلقة for بتنفيذ مجموعة من الشيفرات لعددٍ مُحدِّدٍ من المرات. هذه المقالة جزء من سلسة مقالات حول تعلم البرمجة في بايثون 3. ترجمة –وبتصرّف– للمقال How To Construct For Loops in Python 3 لصاحبته Lisa Tagliaferri اقرأ أيضًا الدرس التالي: كيفية استخدام تعابير break و continue و pass عند التعامل مع حلقات التكرار في بايثون 3 الدرس السابق: كيفية إنشاء حلقات تكرار while في بايثون 3 المرجع الشامل إلى تعلم لغة بايثون كتاب البرمجة بلغة بايثون
-
نستفيد من البرامج الحاسوبية خيرَ استفادة في أتمتة المهام وإجراء المهام التكرارية لكيلا نحتاج إلى القيام بها يدويًا، وإحدى طرائق تكرار المهام المتشابهة هي استخدام حلقات التكرار، وسنشرح في درسنا هذا حلقة تكرار while. حلفة تكرار while تؤدي إلى تكرار تنفيذ قسم من الشيفرة بناءً على متغير منطقي (boolean)، وسيستمر تنفيذ هذه الشيفرة لطالما كانت نتيجة التعبير المستعمل معها تساوي true. يمكنك أن تتخيل أنَّ حلقة while هي عبارة شريطة تكرارية، فبعد انتهاء تنفيذ العبارة الشرطية if فيُستَكمَل تنفيذ بقية البرنامج، لكن مع حلقة while فسيعود تنفيذ البرنامج إلى بداية الحلقة بعد انتهاء تنفيذها إلى أن يصبح الشرط مساويًا للقيمة false. وعلى النقيض من حلقات for التي تُنفَّذ عدد معيّن من المرات، فسيستمر تنفيذ حلقات while اعتمادًا على شرطٍ معيّن، لذا لن تحتاج إلى عدد مرات تنفيذ الحلقة قبل إنشائها. حلقة while الشكل العام لحلقات while في لغة بايثون كالآتي: while [a condition is True]: [do something] سيستمر تنفيذ التعليمات البرمجية الموجودة داخل الحلقة إلى أن يصبح الشرط false. لنُنشِئ برنامجًا صغيرًا فيه حلقة while، ففي هذه البرنامج سنطلب من المستخدم إدخال كلمة مرور. وهنالك خياران أمام حلقة التكرار: - إما أن تكون كلمة المرور صحيحة، فعندها سينتهي تنفيذ حلقة while. - أو أن تكون كلمة المرور غير صحيحة، فعندها سيستمر تنفيذ حلقة التكرار. لنُنشِئ ملفًا باسم password.py في محررنا النصي المفضَّل، ولنبدأ بتهيئة المتغير paasword بإسناد سلسلة نصية فارغة إليه: password = '' نستخدم المتغير السابق للحصول على مدخلات المستخدم داخل حلقة التكرار while. علينا بعد ذلك إنشاء حلقة while مع تحديد ما هو الشرط الذي يجب تحقيقه: password = '' while password != 'password': أتبَعنا –في المثال السابق– الكلمة المحجوزة while بالمتغير password، ثم سنتحقق إذا كانت قيمة المتغير password تساوي السلسلة النصية 'password' (لا تنسَ أنَّ قيمة المتغير سنحصل عليها من مدخلات المستخدم)، يمكنك أن تختار أي سلسلة نصية تشاء لمقارنة مدخلات المستخدم بها. هذا يعني أنَّه لو أدخل المستخدم السلسلة النصية password فستتوقف حلقة التكرار وسيُكمَل تنفيذ البرنامج وستُنفَّذ أيّة شيفرات خارج الحلقة، لكن إذا أدخل المستخدم أيّة سلسلة نصية لا تساوي password فسيُكمَل تنفيذ الحلقة. علينا بعد ذلك إضافة الشيفرة المسؤولة عمّا يحدث داخل حلقة while: password = '' while password != 'password': print('What is the password?') password = input() نفَّذ البرنامج عبارة print داخل حلقة while والتي تسأل المستخدم عن كلمة مروره، ثم أسندنا قيمة مدخلات المستخدم (التي حصلنا عليها عبر الدالة input()) إلى المتغير password. سيتحقق البرنامج إذا كانت قيمة المتغير password تساوي السلسلة النصية 'password'، وإذا تحقق ذلك فسينتهي تنفيذ حلقة while. لنضف سطرًا آخر إلى البرنامج لنعرف ماذا يحدث إن أصبحت قيمة الشرط مساويةً إلى false: password = '' while password != 'password': print('What is the password?') password = input() print('Yes, the password is ' + password + '. You may enter.') لاحظ أنَّ آخر عبارة print() موجودة خارج حلقة while، لذا عندما يُدخِل المستخدم الكلمة password عند سؤاله عن كلمة مروره، فستُطبَع آخر جملة والتي تقع خارج حلقة التكرار. لكن ماذا يحدث لو لم يدخل المستخدم الكلمة password قط؟ حيث لن يستمر تنفيذ البرنامج ولن يروا آخر عبارة print() وسيستمر تنفيذ حلقة التكرار إلى ما لا نهاية! يستمر تنفيذ حلقة التكرار إلى ما لا نهاية إذا بقي تنفيذ البرنامج داخل حلقة تكرار دون الخروج منها. وإذا أردتَ الخروج من حلقة تكرار نهائية، فاضغط Ctrl+C في سطر الأوامر. احفظ البرنامج ثم شغِّله: python password.py سيُطلَب منك إدخال كلمة المرور، ويمكنك تجربة ما تشاء من الكلمات. هذا مثالٌ عن ناتج البرنامج: What is the password? hello What is the password? sammy What is the password? PASSWORD What is the password? password Yes, the password is password. You may enter. أبقِ في ذهنك أنَّ السلاسل النصية حساسة لحالة الأحرف إلا إذا استعملتَ دالةً من دوال النصوص لتحويل السلسلة النصية إلى حالة الأحرف الصغيرة (على سبيل المثال) قبل التحقق منها. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن مثال عن برنامج يستخدم حلقة while بعد أن تعلمنا المبدأ الأساسي لحلقة تكرار while، فلنُنشِئ لعبة تعمل على سطر الأوامر لتخمين الأرقام والتي تستعمل الحلقة while . نريد من الحاسوب أن يُنشِئ أرقامًا عشوائيةً لكي يحاول المستخدمون تخمينها، لذا علينا استيراد الوحدة random عبر استخدام العبارة import، وإذا لم تكن هذه الحزمة مألوفةً لك فيمكنك قراءة المزيد من المعلومات عن توليد الأرقام العشوائية في توثيق بايثون. لنُنشِئ بدايةً ملفًا باسم guess.py في محررك النصي المفضَّل: import random علينا الآن إسناد عدد صحيح عشوائي إلى المتغير number، ولنجعل مجاله من 1 إلى 25 (بما فيها تلك الأرقام) كيلا نجعل اللعبة صعبة جدًا. import random number = random.randint(1, 25) يمكننا الآن إنشاء حلقة while، وذلك بتهيئة متغير ثم كتابة الحلقة: import random number = random.randint(1, 25) number_of_guesses = 0 while number_of_guesses < 5: print('Guess a number between 1 and 25:') guess = input() guess = int(guess) number_of_guesses = number_of_guesses + 1 if guess == number: break هيئنا متغيرًا اسمه number_of_guesses قيمته 0، وسوف نزيد قيمته عند كل تكرار للحلقة لكي لا تصبح حلقتنا لا نهائية. ثم سنضيف تعبير while الذي يشترط ألّا تزيد قيمة المتغير number_of_guesses عن 5. وبعد المحاولة الخامسة سيُعاد المستخدم إلى سطر الأوامر، وإذا حاول المستخدم إدخال أيّ شيء غير رقمي فسيحصل على رسالة خطأ. أضفنا داخل حلقة while عبارة print() لطلب إدخال رقم من المستخدم، ثم سنأخذ مدخلات المستخدم عبر الدالة input() ونُسنِدَها إلى المتغير guess، ثم سنحوِّل المتغير guess من سلسلة نصية إلى عدد صحيح. وقبل انتهاء حلقة التكرار، فعلينا زيادة قيمة المتغير number_of_guesses بمقدار 1، لكيلا تُنفَّذ حلقة التكرار أكثر من 5 مرات. وفي النهاية، كتبنا عبارة if شرطية لنرى إذا كان المتغير guess الذي أدخله المستخدم مساوٍ للرقم الموجود في المتغير number الذي ولَّده الحاسوب، وإذا تحقق الشرط فسنستخدم عبارة break للخروج من الحلقة. أصبح البرنامج جاهزًا للاستخدام، ويمكننا تشغيله عبر تنفيذ الأمر: python guess.py صحيحٌ أنَّ البرنامج يعمل عملًا سليمًا، لكن المستخدم لن يعلم إذا كان تخمينه صحيحًا ويمكنه أن يخمِّن الرقم خمس مرات دون أن يعلم إذا كانت إحدى محاولاته صحيحة. هذا مثال عن مخرجات البرنامج: Guess a number between 1 and 25: 11 Guess a number between 1 and 25: 19 Guess a number between 1 and 25: 22 Guess a number between 1 and 25: 3 Guess a number between 1 and 25: 8 لنضف بعض العبارات الشرطية خارج حلقة التكرار لكي يحصل المستخدم على معلومات فيما إذا استطاعوا تخمين الرقم أم لا، وسنضيف هذه العبارات في نهاية الملف: import random number = random.randint(1, 25) number_of_guesses = 0 while number_of_guesses < 5: print('Guess a number between 1 and 25:') guess = input() guess = int(guess) number_of_guesses = number_of_guesses + 1 if guess == number: break if guess == number: print('You guessed the number in ' + str(number_of_guesses) + ' tries!') else: print('You did not guess the number. The number was ' + str(number)) في هذه المرحلة سيُخبِر البرنامجُ المستخدمَ إذا استطاعوا تخمين الرقم، لكن ذلك لن يحدث إلا بعد انتهاء حلقة التكرار وبعد انتهاء عدد مرات التخمين المسموحة. ولمساعد المستخدم قليلًا، فلنضف بعض العبارات الشرطية داخل حلقة while وتلك العبارات ستخبر المستخدم إذا كان تخمينه أعلى من الرقم أو أصغر منه، لكي يستطيعوا تخمين الرقم بنجاح، وسنضيف تلك العبارات الشرطية قبل السطر الذي يحتوي على if guess == number: import random number = random.randint(1, 25) number_of_guesses = 0 while number_of_guesses < 5: print('Guess a number between 1 and 25:') guess = input() guess = int(guess) number_of_guesses = number_of_guesses + 1 if guess < number: print('Your guess is too low') if guess > number: print('Your guess is too high') if guess == number: break if guess == number: print('You guessed the number in ' + str(number_of_guesses) + ' tries!') else: print('You did not guess the number. The number was ' + str(number)) وعندما نُشغِّل البرنامج مرةً أخرى بتنفيذ python guess.py، فيمكننا ملاحظة أنَّ المستخدم سيحصل على بعض المساعدة، فلو كان الرقم المولَّد عشوائيًا هو 12 وكان تخمين المستخدم 18، فسيُخبره البرنامج أنَّ الرقم الذي خمنه أكبر من الرقم العشوائي، وذلك لكي يستطيع تعديل تخمنيه وفقًا لذلك. هنالك الكثير من التحسينات التي يمكن إجراؤها على الشيفرة السابقة، مثل تضمين آلية لمعالجة الأخطاء التي تحدث عندما لا يُدخِل المستخدم عددًا صحيحًا، لكن كان غرضنا هو رؤية كيفية استخدام حلقة while في برنامج قصير ومفيد يعمل من سطر الأوامر. الخلاصة شرحنا في هذا الدرس كيف تعمل حلقات while في بايثون وكيفية إنشائها. حيث تستمر حلقات while بتنفيذ مجموعة من الأسطر البرمجية لطالما كان الشرط مساويًا للقيمة true. هذه المقالة جزء من سلسة مقالات حول تعلم البرمجة في بايثون 3. ترجمة –وبتصرّف– للمقال How To Construct While Loops in Python 3 لصاحبته Lisa Tagliaferri اقرأ أيضًا الدرس التالي: كيفية إنشاء حلقات تكرار for في بايثون 3 الدرس السابق: كيفية كتابة التعليمات الشرطية في بايثون 3 المرجع الشامل إلى تعلم لغة بايثون كتاب البرمجة بلغة بايثون