-
المساهمات
21 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو محمد الخضور
-
نتحدث في هذا الفيديو عن جهاز الموجه أو الراوتر Router الذي هو واحد من مكونات الشبكة، حيث سنتعرف عليه وعلى وظائفه وأنواعه وطريقة عمله. إذا أردت التعرف أكثر على مجال الشبكات، فننصحك بالانضمام إلى دورة علوم الحاسوب، ولا تنسَ الاستعانة خلال رحلة تعلمك وعملك بتوثيقات موسوعة حسوب المجانية. وإذا أردت متابعة المعلومات البرمجية العلمية مكتوبة فيمكنك الاطلاع على قسم البرمجة في أكاديمية حسوب، كما يمكنك متابعة جديد الفيديوهات التقنية المتاحة على يوتيوب أكاديمية حسوب مجانًا.
-
سنقدم في هذا المقال العديد من الطرق الشائعة لكتابة شيفرات بايثون الاصطلاحية إلى جانب نظيراتها من الطرق غير البايثونية فيما يتعلق بقواميس بايثون والتعامل مع المتغيرات وعاملها الثلاثي. تُعد القواميس dictionaries نواة العديد من برامج بايثون نظرًا للمرونة التي توفرها أزواج القيم المفتاحية key-value pairs عبر ربط أجزاء البيانات ببعضها بعضًا، وبالتالي من المفيد أن تتعلم حول اصطلاحات قواميس بايثون الأكثر استخدامًا في الشيفرات، ويمكنك مراجعها بقراءة مقال فهم القواميس في بايثون. استخدام التابعين ()get و ()setdefault للتعامل مع القواميس لدى محاولتك الوصول إلى مفتاح أحد القواميس دون وجوده، سيتسبب ذلك بخطأ من النوع KeyError، لذا يلجأ المبرمجون عادةً إلى تجنُّب هذه الحالة بكتابة شيفرات غير بايثونية، كما في المثال التالي: >>> # Unpythonic Example >>> numberOfPets = {'dogs': 2} >>> if 'cats' in numberOfPets: # Check if 'cats' exists as a key. ... print('I have', numberOfPets['cats'], 'cats.') ... else: ... print('I have 0 cats.') ... I have 0 cats. تتحقق الشيفرة السابقة من وجود السلسلة النصية cats مثل مفتاح ضمن القاموس المسمى numberOfPets؛ فإذا كانت كذلك فعلًا، يُطبع محتوى القاموس الموافق لهذا المفتاح باستخدام التعليمة ['numberOfPets['cats مثل جزء من الرسالة المعروضة للمستخدم باستخدام الدالة ()print؛ وفي حال عدم وجود هذا المفتاح تُطبع عبارة باستخدام دالة ()print أخرى دون الوصول إلى القاموس numberOfPetsكون المفتاح غير موجود وبالتالي لا تسبب بوقوع الخطأKeyError`. تمتلك قواميس بايثونتابع ()get، الذي يسمح بتحديد قيمة افتراضية لتعاد في حال طلب مفتاح غير موجود في القاموس. والشيفرة البايثونية التالية تكافئ الشيفرة في المثال السابق: >>> # Pythonic Example >>> numberOfPets = {'dogs': 2} >>> print('I have', numberOfPets.get('cats', 0), 'cats.') I have 0 cats. يتحقق الاستدعاء (numberOfPets.get('cats', 0 من كون المفتاح cats موجودًا في القاموس numberOfPets، فإذا كان موجودًا فعلًا يعيد الاستدعاء القيمة من القاموس الموافقة للمفتاح cats، وإلا يعيد الاستدعاء الوسيط الثاني وهو في حالتنا 0. إذًا، يمثّل استخدام التابع ()get لتحديد قيمة افتراضية لتعاد في حال استدعاء مفتاح غير موجود في القاموس طريقةً أقصر وأكثر مقروئية من استخدام العبارة الشرطية if-else. قد ترغب بالمقابل بتعيين قيمة افتراضية في حال عدم وجود مفتاح ما. على سبيل المثال، بفرض أن القاموس numberOfPets لا يتضمن المفتاح 'cats'، ستتسبب التعليمة numberOfPets['cats'] += 10 بخطأ من النوع KeyError، وقد ترغب بإضافة شيفرة مهمتها التحقق من غياب مفتاح محدد مُعينةً قيمة افتراضية على النحو التالي: >>> # Unpythonic Example >>> numberOfPets = {'dogs': 2} >>> if 'cats' not in numberOfPets: ... numberOfPets['cats'] = 0 ... >>> numberOfPets['cats'] += 10 >>> numberOfPets['cats'] 10 ولكن بما أن هذا النمط شائع الاستخدام أيضًا، فالقواميس تتضمن تابعًا أكثر بايثونية وهو ()setdefault. تكافئ الشيفرة التالية تلك الموجودة في المثال السابق: >>> # Pythonic Example >>> numberOfPets = {'dogs': 2} >>> numberOfPets.setdefault('cats', 0) # Does nothing if 'cats' exists. 0 >>> numberOfPets['cats'] += 10 >>> numberOfPets['cats'] 10 فبدلًا من استخدام جملة if شرطية للتحقق من وجود مفتاح ما في قاموس وتعيين قيمة افتراضية في حال غيابه، استخدم التابع ()setdefault. استخدام الصنف collections.defaultdict لتعيين قيم القواميس الافتراضية يتيح استخدام الصنف collections.defaultdict إمكانية التخلص التام من أخطاء KeyError، إذ يُمكّنك من إنشاء قاموس افتراضي عن طريق استيراد الوحدة collections واستدعاء الدالة ()collections.defaultdict مُمررًا إليها نمط معطيات تختاره ليكون قيمةً افتراضية. على سبيل المثال، في حال تمرير الدالة int مثل وسيط إلى الدالة ()collections.defaultdict تكون قد أنشأت كائنًا شبيهًا بالقاموس dictionary-like object يستخدم القيمة 0 قيمةً افتراضية في حال طلب مفاتيح غير موجودة في القاموس. دعنا نكتب التالي في الصدفة التفاعلية: >>> import collections >>> scores = collections.defaultdict(int) >>> scores defaultdict(<class 'int'>, {}) >>> scores['Al'] += 1 # No need to set a value for the 'Al' key first. >>> scores defaultdict(<class 'int'>, {'Al': 1}) >>> scores['Zophie'] # No need to set a value for the 'Zophie' key first. 0 >>> scores['Zophie'] += 40 >>> scores defaultdict(<class 'int'>, {'Al': 1, 'Zophie': 40}) مرّرنا في الشيفرة السابقة الدالة ()int ولم نستدعيها، وهذا سبب إهمالنا لأقواسها إذ كتبناها بالشكل int ضمن استدعاء الدالة (collections.defaultdict(int. يمكن تمرير الدالة list مثل وسيط وبالتالي استخدام قائمة فارغة بمثابة قيمة افتراضية للقاموس، كما في المثال التالي: >>> import collections >>> booksReadBy = collections.defaultdict(list) >>> booksReadBy['Al'].append('Oryx and Crake') >>> booksReadBy['Al'].append('American Gods') >>> len(booksReadBy['Al']) 2 >>> len(booksReadBy['Zophie']) # The default value is an empty list. 0 إذًا، في الحالات التي نحتاج فيها إلى قيمة افتراضية لكل احتمالية ممكنة للمفاتيح، فمن الأسهل استخدام الدالة ()collections.defaultdict بدلًا من استخدام قاموس عادي واستدعاء التابع ()setdefault. استخدام القواميس بدلا من العبارة Switch تمتلك لغات البرمجة مثل لغة جافا العبارة البرمجية switch، والتي تعد أحد أنواع العبارة الشرطية if-elif-else، إذ أنها تشغِّل الشيفرة بناءً على القيمة التي يتضمنها متغير معين من بين مجموعة قيم، أما لغة بايثون فلا تتضمن العبارة switch ما يجعل مبرمجي بايثون يكتبون في بعض الحالات شيفراتٍ كما في المثال التالي، والذي يشغّل مجموعة من تعليمات الإسناد على ضوء القيمة التي يتضمنها المتغير season من بين مجموعة قيم، على النحو التالي: # All of the following if and elif conditions have "season ==": if season == 'Winter': holiday = 'New Year\'s Day' elif season == 'Spring': holiday = 'May Day' elif season == 'Summer': holiday = 'Juneteenth' elif season == 'Fall': holiday = 'Halloween' else: holiday = 'Personal day off' لا يمكن عدّ الشيفرة السابقة غير بايثونية تمامًا، لكنها طريقة طويلة قليلًا، ومن الجدير بالذكر هنا أن عبارة switch في لغة جافا تفرض في سياقها استخدام تعليمة break في نهاية كل من كتلها، وبدونها سيتابع البرنامج تنفيذ الكتلة التالية الخاصة بالحالة التالية لقيمة المتغير، وبالتالي يمثّل نسيان استخدام التعليمة break مصدرًا شائعًا للأخطاء. في المقابل، تتكرر العبارات if-elif في مثالنا السابق، وهذا ما يجعل بعض المبرمجين يفضلون إنشاء قاموس بدلًا عنها. فيما يلي صيغة بايثونية مختصرة لنفس المثال السابق: holiday = {'Winter': 'New Year\'s Day', 'Spring': 'May Day', 'Summer': 'Juneteenth', 'Fall': 'Halloween'}.get(season, 'Personal day off') تتضمن الشيفرة السابقة تعليمة إسناد واحدة، إذ تمثّل القيمة المُخزّنة في المتغير holiday القيمة المعادة من استدعاء التابع ()get، الذي يعيد القيمة الموافقة للمفتاح المُعيّن في المتغير season، وفي حال كون المفتاح المطلوب غير موجود، سيعيد التابع ()get القيمة الافتراضية 'Personal day off'. جعل استخدام قاموس من الشيفرة مختصرة، إلا أنها قد تكون أصعب قليلًا للقراءة، ويعود القرار حول استخدامها من عدمه لك. التعابير الشرطية: عامل بايثون الثلاثي القبيح تعمل العوامل الثلاثية Ternary operators (والتي تُسمّى في بايثون رسميًا بالتعابير البرمجية الشرطية أو تعابير الانتقاء الثلاثية) على تقييم تعبير ما إلى إحدى قيمتين بناءً على شرط ما، وهذا ما نفعله عادةً باستخدام التعبير البايثوني if-else، على النحو التالي: >>> # Pythonic Example >>> condition = True >>> if condition: ... message = 'Access granted' ... else: ... message = 'Access denied' ... >>> message 'Access granted' تعني كلمة "ثلاثي Ternary" ببساطة أن العامل يستقبل ثلاثة مدخلات، إلا أن مرادفها في عالم البرمجة هو "التعبير الشرطي". تقدّم التعابير الشرطية صيغةً مختصرة تُكتب ضمن سطر واحد، وهذا الأمر مناسب لهذا النمط الثلاثي، وتُنفّذ هذه التعابير في بايثون من خلال توزيع معيّن لكل من الكلمتين المفتاحيتين ifو else، كما في المثال التالي: >>> valueIfTrue = 'Access granted' >>> valueIfFalse = 'Access denied' >>> condition = True 1 >>> message = valueIfTrue if condition else valueIfFalse >>> message 'Access granted' 2 >>> print(valueIfTrue if condition else valueIfFalse) 'Access granted' >>> condition = False >>> message = valueIfTrue if condition else valueIfFalse >>> message 'Access denied' يُقيّم التعبير ذو الرقم 1 إلى القيمة الموافقة للوسيط valueIfTrue في حال كون متغير الشرط condition محققًا، أي مُقيّم على أنه True؛ أما في حال كونه غير محقق، أي أنه مُقيّم على أنه False فيُقيّم التعبير إلى القيمة الموافقة للوسيط valueIfFalse. وصف مبتكر لغة بايثون هذا التعبير مازحًا بأنه "قبيح عمدًا intentionally ugly"، إذ تضع معظم لغات البرمجة المُتضمنة العوامل الثلاثية الشرط بدايةً متبوعًا بالتعليمات المطلوبة في حال تحققه ثم تلك المطلوبة في حال عدم تحققه، ولكن يمكن -مع هذه الطريقة- استخدام تعبير شرطي في أي موضع نريد مكان أي قيمة أو تعبير، حتى مثل وسيط لإحدى الدوال كما هو الحال في السطر 2 من الشيفرة السابقة. ما هو سبب اعتماد بايثون لهذه الصياغة في الإصدار 2.5 منها رغم كونها تنافي المبدأ التوجيهي الأول القائل "الجمال أفضل من القبح"؟ السبب هو أن العوامل الثلاثية شائعة الاستخدام بين المبرمجين رغم كونها ذات مقروئية منخفضة نسبيًا، لذلك طالبوا بايثون بدعمها. جديرٌ بالذكر أنه من الممكن إساءة استخدام اختصار العوامل المنطقية بغية إنشاء نوع من أنواع العوامل الثلاثية، إذ سيُقيّم تعبير مثل: condition and valueIfTrue or valueIfFalse إلى القيمة الموافقة للتعبير valueIfTrue في حال تحقق الشرط وإلى valueIfFalse في حال عدم تحققه (ماعدا حالة خاصة مهمة). لاحظ المثال التالي: >>> # Unpythonic Example >>> valueIfTrue = 'Access granted' >>> valueIfFalse = 'Access denied' >>> condition = True >>> condition and valueIfTrue or valueIfFalse 'Access granted' ينطوي العامل الثلاثي الزائف هذا على خطأ غامض خاص يحدث عند كون قيمة التعبير valueIfTrue من النمط الخاطئ (بمعنى أنها 0 أو False أو None أو سلسلة نصية فارغة)، فعندها سيُقيّم التعبير بصورة غير متوقعة إلى القيمة الموافقة للتعبير valueIfFalse عند تحقُق الشرط. استمر المبرمجون مع ذلك باستخدام هذا العامل الثلاثي الزائف، وأصبح السؤال "لماذا بايثون لا تتضمّن عاملًا ثلاثيًا؟" من الأسئلة الدائمة المطروحة على فريق مطوري بايثون الرئيسي. وكان من المتوقع أنه مع إنشاء التعابير الشرطية في بايثون سيتوقف المبرمجون عن طلب تضمين العامل الثلاثي وسيكفّون عن استخدام العامل الثلاثي الزائف المُتسبب بوقوع الخطأ. مع ذلك، تبقى التعابير الشرطية "قبيحة" بما يكفي لكي لا يتشجع المبرمجون على استخدامها، فرغم كون الجمال أفضل من القبح إلا أن العامل الثلاثي "القبيح" هو مثال على حكمة كون الواقع العملي غير مثالي. ليست التعابير الشرطية بايثونية تمامًا وليست غير بايثونية في الوقت نفسه، ففي حال استخدمتها، تجنب تداخلها: >>> # Unpythonic Example >>> age = 30 >>> ageRange = 'child' if age < 13 else 'teenager' if age >= 13 and age < 18 else 'adult' >>> ageRange 'adult' إذ تمثّل التعابير الشرطية خير مثال على أن السطور البرمجية المُكثّفة قد تكون صحيحة من الناحية البرمجية ولكنها صعبة الفهم والقراءة. التعامل مع قيم المتغيرات ستضطر غالبًا إلى التحقق من القيم المخزنة في المتغيرات وتعديلها، وتتضمن بايثون عدّة طرق لفعل ذلك مبينة من خلال المثالين التاليين. الإسناد التسلسلي وعوامل المقارنة في حال أردت التحقق من كون عدد ما ينتمي إلى مجال محدد، ستستخدم غالبًا العامل المنطقي and على النحو التالي: # Unpythonic Example if 42 < spam and spam < 99: إلا أن بايثون تسمح بتسلسل عوامل المقارنة وبالتالي من الممكن الاستغناء عن العامل and. تكافئ الشيفرة التالية تلك المذكورة في المثال السابق: # Pythonic Example if 42 < spam < 99: ينطبق الأمر ذاته على تسلسل عامل الإسناد =، إذ من الممكن تعيين عدد من المتغيرات إلى نفس القيمة ضمن سطر برمجي واحد، على النحو التالي: >>> # Pythonic Example >>> spam = eggs = bacon = 'string' >>> print(spam, eggs, bacon) string string string يمكن استخدام العامل and للتحقق من كون المتغيرات الثلاث تخزّن فعلًا نفس القيمة، أو استخدام سلسلة من العامل == للتحقق من المساواة ببساطة أكبر. >>> # Pythonic Example >>> spam = eggs = bacon = 'string' >>> spam == eggs == bacon == 'string' True ميزة تسلسل العوامل بسيطة، إلا أنها ذات فائدة كبيرة في بايثون، ولكن بالمقابل سيتسبب الاستخدام الخاطئ لها بالأخطاء. التحقق من كون قيمة متغير تنتمي لمجموعة محددة من القيم قد تصادف أحيانًا حالة معاكسة تمامًا لتلك الواردة في الفقرة السابقة: وهي التحقق من كون قيمة متغير ما هي واحدة من مجموعة قيم ممكنة، ومن الممكن إنجاز ذلك باستخدام العامل or كما في التعبير التالي: spam == 'cat' or spam == 'dog' or spam == 'moose` إلا أن الجزء المتكرر ==spam يجعل من التعبير السابق غير عملي الاستخدام، ويمكن بدلًا من ذلك وضع القيم المحتملة ضمن بنية صف والتحقق من كون قيمة متغير ما موجودةً ضمن هذا الصف باستخدام العامل in، كما في المثال التالي: >>> # Pythonic Example >>> spam = 'cat' >>> spam in ('cat', 'dog', 'moose') True لا تمتاز هذه الطريقة بالسهولة فحسب، بل إنها أسرع أيضًا من حيث الوقت اللازم. الخلاصة ما من لغة برمجة إلا ولديها الاصطلاحات والممارسات الأفضل الخاصة بها، وقد ركّز هذا المقال على الطرق العملية التي يستخدمها مبرمجو بايثون لكتابة شيفرات "بايثونية" للتعامل مع القواميس والمتغيرات وعامل بايثون الثلاثي. تمتلك القواميس التابعين ()get و ()setdefault بغية التعامل مع المفاتيح غير الموجودة، ولكن من الممكن استخدام قاموس من النوع collections.defaultdict لتخزين القيم الافتراضية للمفاتيح غير الموجودة. لا تتضمن لغة بايثون العبارة switch، لكن يمثّل استخدام القواميس طريقةً مختصرة لتطبيق ما يكافئ استخدام العبارة switch دون الحاجة إلى استخدام عدد كبير من عبارات if-elif-else، كما يمكن استخدام العوامل الثلاثية عند الحاجة إلى تقييم شرط ما إلى إحدى قيمتين. وتتحقق العوامل == من كون المتغيرات متساوية أم لا، في حين يتحقق العامل in من كون المتغير يمثل واحدة من مجموعة قيم ممكنة. ترجمة -وبتصرف- لجزء من الفصل السادس "كتابة شيفرات بايثون" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: كتابة شيفرات بايثون: صيغ شائعة الاستخدام على نحو خاطئ اتخاذ القرار: العبارات الشرطية والحلقات التكرارية في البرمجة فهم العمليات المنطقية في بايثون
-
ما من لغة برمجة إلا وتصف نفسها بالقوة، الصفة عديمة المعنى في عالم لغات البرمجة، حتى أن كتيب بايثون التعليمي الرسمي يبدأ بالعبارة "بايثون هي لغة برمجة قوية سهلة التعلم". ولكن ما من خوارزمية تنفيذها حكر على لغة برمجة دون غيرها، وما من وحدة قياس لتحديد مدى "قوة" لغة برمجة ما (ولكن من الممكن بالطبع قياس الكم الذي يجادل به المبرمجون دفاعًا عن لغتهم المفضلة). إلا أن كل لغة تتميز بأنماطها التصميمية الخاصة وبثغراتها، ما يشكّل بالنتيجة نقاط قوتها وضعفها. ولكتابة شيفرات بايثون كالمحترفين، فلا بدّ من معرفتك لما يتجاوز حدود قواعدها النحوية ومكتباتها المعيارية. وتتمثّل الخطوة التالية بتعلّم الاصطلاحات أو الممارسات الخاصة بكتابة الشيفرات في بايثون. إذ تسمح بعض ميزات لغة بايثون لنفسها بكتابة الشيفرات باستخدام طرق خاصة بلغة بايثون والتي غدت معروفة باسم الطرق البايثونية Pythonic. سنقدم في هذا المقال العديد من الطرق الشائعة لكتابة شيفرات بايثون الاصطلاحية إلى جانب نظيراتها من الطرق غير البايثونية. فما يعد "بايثوني" قد يختلف من مبرمج لآخر. مبادئ بايثون التوجيهية العشرون مبادئ بايثون التوجيهية العشرون أو ما يعرف باسم "زن بايثون The Zen of Python" الموضوعة من قبل تيم بيترز Tim Peters تختص بتصميم لغة بايثون وبرامجها. وليس من الضروري أن تتبع في برامجك كامل هذه التوجيهات، إلا أنه من المفيد تذكرها دومًا. كما أنها تمثل هديةً مخفية أو طرفة كامنة تظهر لدى تشغيل الأمر import this كما يلي: >>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. --snip-- ** ملاحظة**: الغامض في الأمر أن عدد المبادئ التوجيهية التي كتبها تيم بيترز هو تسعة عشر. وفي هذا الصدد يقال أنّ مُنشئ بايثون جيدو فان روسوم قد قال بأن الحكمة رقم 20 المفقودة ما هي إلا بعض من غرائب وطرائف تيم بيترز، إذ تركها تيم لجيدو ليملأها، الأمر الذي لم يقم به الأخير على ما يبدو. وبالنتيجة هذه المبادئ التوجيهية لا تتعدى كونها آراء يمكن للمبرمجين أن يؤيدوها أو يعارضوها. وكأي مجموعة من المبادئ الجيدة فإنها تناقض نفسها موفرةً أكبر قدر ممكن من المرونة. وفيما يلي تفسيري لهذه الحكم: لعل الشيفرة الجميلة هي تلك التي تسهل قراءتها وفهمها. فغالبًا ما يكتب المبرمجون شيفراتهم بسرعة غير مكترثين بمدى قابليتها للقراءة، فالحاسوب سيشغلها على أية حال، إلا أن تصحيحها أو اكتشاف الأخطاء فيها سيكون أمرًا صعبًا بالنسبة للمبرمجين الآخرين. وجمال الشيفرة بالنتيجة هو أمر شخصي، إلا أن الشيفرة المكتوبة دون اكتراث لمدى قابليتها للفهم ستكون في نظر الآخرين قبيحة حتمًا. ولعل أهم سبب لشعبية بايثون هو كون شيفراتها لا تعمها الفوضى بعلامات ترقيم بلا جدوى كغيرها من لغات البرمجة ما يسهل التعامل معها. لنفرض أني كتبت الآن "القول مُفسّر لنفسه" كشرح لهذه الحكمة، أليس بالشرح المريع؟ كذلك الأمر بالنسبة للشيفرات، فمن المحبذ أن تتبع الأساليب المطولة الصريحة، متجنبًا إخفاء آليات عمل الشيفرة تحت قناع ميزات اللغة التي تتطلب معرفة عميقة بها لفهمها. تُذكرنا هاتان الحكمتان بحقيقة أنه من الممكن إتمام أي أمر باستخدام تقنيات بسيطة أو معقدة. لنفرض أنه لديك عمل بسيط يتطلب إنجازه استخدام مجرفة يدوية، عندها سيكون استخدام مجرفة هيدروليكية بسعة 50 طن لإنجازه ضرب من المبالغة. أما إذا كان العمل المطلوب ضخمًا جدًا، فإن تعقيدية استخدام مجرفة هيدروليكية واحدة يبقى أفضل من التعقيدية المتشعبة التي تكتنف التنسيق بين فريق مكون من 100 جرافة يدوية لإنجاز العمل. اجعل الأفضلية دومًا للبساطة على التعقيد، شرط أن تعرف حدود البساطة. يميل المبرمجون لتنظيم شيفرتهم ضمن فئات، لاسيما تلك الفئات التي تتضمن فئات فرعية والتي تتضمن بدورها فئات فرعية أيضًا. ولا تضيف هذه البنى الهرمية عادةً إلى الشيفرة تنظيمًا بقدر ما تضيف له بيروقراطية. ولا بأس بأن تكتب شيفرتك ضمن إحدى الوحدات عالية المستوى أو ضمن بنية معطيات واحدة. ولكن إن بدت شيفرتك بالشكل ()spam.eggs.chicken.fish أو ['spam['eggs']['chicken']['fish، فاعلم أن شيفرتك شديدة التعقيد والتشعب. يميل المبرمجون عادةً إلى حشر أكبر كم ممكن من التعليمات ضمن أصغر جزء ممكن من الشيفرة، كما في السطر البرمجي: print('\n'.join("%i bytes = %i bits which has %i possiblevalues." % (j, j*8, 256**j-1) for j in (1 << i for i in range(8)))) فرغم كون شيفرة كهذه قد تكون محط إعجاب الأصدقاء، إلا أنها حتمًا ستكون مصدر إزعاج لمن يتوجب عليه فهمها من زملاء العمل. فلا تجعل شيفرتك تنفذ مهام عديدة معًا، إذ أن الشيفرات الموزعة على أسطر متعددة عادةً ما تكون أسهل للقراءة من تلك المحشورة في سطر واحد ضمن مسافة ضيقة. هذه الحكمة تصب في نفس معنى تلك القائلة البساطة أفضل من التعقيد. رغم كون ()strcmp تشير وضوحًا إلى دالة مقارنة السلاسل المحرفية String Compare لشخص يعمل في ميدان البرمجة بلغة سي C منذ السبعينيات، إلا أن الحواسيب في أيامنا تملك ما يكفي من الذاكرة لكتابة أسماء الدوال كاملةً. فلا تحذف أحرفًا من أسماء المعرفات ولا تبالغ في اختصار شيفرتك. خذ وقتك لاختيار أسماء المتغيرات والدوال لتكون وصفية ومحددة. كما أن تضمين سطر فارغ ما بين الأقسام المختلفة من شيفرتك أمر يماثل بأهميته فاصل الفقرات في الكتب المطبوعة، إذ يوضح للقارئ الأجزاء المتوجب قراءتها معًا ككتلة واحدة. هذه الحكمة تصب في نفس معنى تلك القائلة الجمال أفضل من القبح. تنطوي هاتان الحكمتان على شيء من التناقض. فمن ناحية عالم البرمجة مليء بما يسمى "أفضل الممارسات" best practices التي على المبرمجين الانصياع لها ما أمكن أثناء كتابة شيفراتهم، إلا أن الالتفاف على هذه الممارسات كالتفاف سريع قد يكون أمرًا مغريًا لكنه قد يسبب المزيد من الفوضى والتشابك لتكون الشيفرة بالنتيجة غير مُتسقة وذات مقروئية منخفضة. ومن ناحية أُخرى سيؤدي الالتزام المبالغ به بالقواعد بغض النظر عن خصوصية الواقع إلى شيفرة مجردة وبمقروئية منخفضة أيضًا. فعلى سبيل المثال، غالبًا ما يؤدي سعي لغة جافا لموائمة كافة شيفراتها وفقًا لنموذجها كائني التوجه إلى شيفرات متداولة كثيرة حتى لأصغر البرامج. الحل إذًا بإمساك العصا من المنتصف ما بين الحكمتين، الأمر الذي سيغدو أسهل مع تراكم خبراتك، فمع الوقت لن تتعلم القواعد فحسب، بل ستتعلم متى يمكنك كسرها. حقيقة كون المبرمجين يميلون لتجاهل رسائل الأخطاء لا تعني أن البرامج يجب أن تتوقف عن إصدارها. تحدث الأخطاء الصامتة عندما تعيد الدوال شيفراتٍ خاطئة أو قيمة خالية None بدلًا من التعامل مع الاستثناءات. ترشدنا هاتان الحكمتان إلى حقيقة أنه من الأفضل للبرنامج أن يفشل ويتوقف عن العمل بسرعة على أن نُسكت الخطأ ونتابع التنفيذ، ففي هذه الحالة كل ما نفعله هو تأجيل حتمية وقوع الخطأ لاحقًا وسيكون حينها تنقيحه أصعب نظرًا لاكتشافه بعد فترة طويلة من وقوع سببه الفعلي. فمن الممكن أن تقرر تجاهل رسائل الأخطاء الناتجة عن برامجك دومًا، لكن تأكد من كونك تفعل ذلك لسبب وجيه. جعلت الحواسيب البشر ميالين لتصديق الخرافات: فلحل أعقد المشاكل في حواسيبنا غدونا نمارس طقسنا الأشهر في إعادة تشغيلها، الأمر الذي سيصلح أي مشكلة مبهمة، إلا أن الواقع مختلف، فالحواسيب ليست مسحورة، وإن كانت شيفرتك لا تعمل فلابد من وجود سبب لن يكتشفه ويحل المشكلة سوى التفكير النقدي التفصيلي. تجنّب تجربة الحلول العشوائية العمياء وصولًا لعمل الشيفرة بأي وسيلة، فبهذه الطريقة أنت تخفي المشكلة فقط بدلًا من حلها جذريًا. تمثل هذه الحكمة انتقاد لشعار لغة البرمجة Perl القائل: "هناك أكثر من طريقة لإنجاز الأمر!" إذ تبيّن أن وجود ثلاث أو أربع طرق مختلفة لكتابة شيفرة تؤدي نفس الغرض هو سيف ذو حدين، فمن ناحية هذه الميزة تمنحك المرونة في كيفية كتابة الشيفرات، إلا أنها تتطلب منك تعلم كافة الطرق الممكنة للكتابة حتى تصبح قادرًا على قراءة شيفرات الآخرين. وبالتالي فإنّ هذه المرونة لا تستحق عناء الجهد الإضافي اللازم خلال تعلم لغة البرمجة. هذه الحكمة عبارة عن طرفة، إشارةً لكون مبتكر بايثون جيدو فان روسوم هولندي الأصل. تشير هاتان الحكمتان لحقيقة كون الشيفرة التي تُنفّذ ببطء هي أسوأ وضوحًا من تلك التي تُنفّذ سريعًا. ولكن بالمقابل من الأفضل الانتظار بعض الوقت لحين تنفيذ برنامجك على أن يُنفّذ بسرعة وبنتائج خاطئة. تتعقد الكثير من الأمور بمرور الوقت، كالقوانين الضريبية والعلاقات العاطفية وكتب بايثون البرمجية! والأمر نفسه ينطبق على عالم البرمجيات. تذكرنا هاتان الحكمتان بحقيقة أنه في حال كون الشيفرة معقدة لدرجة تجعل من المستحيل للمبرمجين الآخرين فهمها وتنقيحها، فهي شيفرة سيئة. ولكن بالمقابل فإن سهولة شرح فكرة الشيفرة للآخرين لا يعني بالضرورة أنها ليست سيئة. فللأسف معرفة كيفية جعل الشيفرة بسيطة ما أمكن بطريقة مدروسة ليست بالمهمة السهلة. تعد نطاقات الأسماء عبارة عن حافظات منفصلة للمُعرفات مهمتها تجنب حدوث تعارضات في الأسماء. فمثلًا لكل من ()open و()webbrowser.open الاسم نفسه إلا أنهما تشيران لدالتين مختلفتين. فاستيراد متصفح الويب باستخدام الدالة ()webbrowser.open لا يتعارض ودالة بايثون ()open نظرًا لكون كل منهما ينتمي لنطاق أسماء مختلف، وهما نطاق أسماء الدوال الخاصة ببايثون ونطاق أسماء وحدة متصفح الويب. ومن الضروري في هذا الصدد تذكر الحكمة القائلة أن السطحية أفضل من التداخل، فمع روعة استخدام نطاقات الأسماء، فلا يجب استخدامها إلا بهدف منع تعارضات الأسماء، وليس بغية إضافة تنظيم إضافي ضمن فئات دون مبرر. وككل الآراء في عالم البرمجة، قد لا تتفق والآراء المبينة أعلاه أو قد تعبّر عما هو مخالف لرأيك وموقفك، ولكن تذكر دائمًا بأن الجدالات حيال كيفية كتابة الشيفرات وما يعتبر منها بايثونيًا -وعلى خلاف ما تظنه- نادرًا ما يكون مثمرًا (ما لم تكن تؤلف كتابًا كاملًا مليئًا بالآراء البرمجية). دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن اعتد استخدام المسافات البادئة ذات المعنى لعل مبعث القلق الأكثر شيوعًا الذي نسمعه حول بايثون من المبرمجين المعتادين على استخدام لغات برمجة أخرى هو أن المسافات البادئة الإلزامية في بايثون (والتي تسمى خطأً المسافات الإلزامية) غريبة وغير مألوفة. إلا أن مقدار المسافة البادئة في بداية السطر البرمجي ذو معنى في بايثون، إذ يحدد السطور البرمجية المنتمية لكتلة واحدة من الشيفرة. وقد يبدو توزيع كتل التعليمات ضمن مجموعات بالاعتماد على المسافات البادئة أمرًا غريبًا في بايثون، إذ تبدأ الكتل وتنتهي في لغات البرمجة الأخرى باستخدام الأقواس المعقوصة { و }. ولكن حتى المبرمجين ممن يستخدمون لغات برمجة غير بايثون عادةً ما يبدأون الكتل البرمجية بمسافة بادئة كما هو الحال مع مبرمجي بايثون، ما يجعل من مقروئية شيفراتهم أعلى. فعلى سبيل المثال، لغة جافا لا تتضمّن مفهوم إلزامية استخدام المسافات البادئة، ومع ذلك يميل مستخدموها لاستخدام المسافات البادئة بغية زيادة مقروئية شيفراتهم. يتضمن المثال التالي دالة جافا باسم ()main تحتوي على استدعاء وحيد لدالة ()println: // Java Example public static void main(String[] args) { System.out.println("Hello, world!"); } ستعمل شيفرة جافا السابقة كما يجب حتى في حال عدم استخدام مسافة بادئة لسطر الدالة ()println، وذلك لأن الأقواس المعقوصة تحدد بداية ونهاية الكتل البرمجية في جافا عوضًا عن المسافات البادئة. وعلى خلاف جافا التي جعلت من استخدام المسافات البادئة أمرًا اختياريًا، فرضت بايثون جعل الشيفرات مقروءة دومًا بإلزامية استخدام المسافات البادئة. مع ملاحظة أن بايثون لا تفرض استخدام المسافات البيضاء، إذ أنها لا تفرض أي قيود على استخدام المسافات بيضاء غير المُمثلة لمسافات بادئة (فكلا التعبيرين 2+2 و2 + 2 يعملان في بايثون). وأحد السجالات البرمجية هو وجوب وضع القوس الاستهلالي على نفس السطر مع العبارة البرمجية الاستهلالية أم في السطر التالي، سيجادل كل مبرمج مدافعًا عن أسلوبه المفضل حتى النهاية، الأمر الذي تجنبته بايثون عن عمد من خلال عدم استخدام الأقواس بالمطلق، ما يجعل مبرمجي بايثون الأكثر إنتاجية، ولكم تمنيت أن تتبنى كافّة لغات البرمجة منهجية بايثون في تجميع الكتل البرمجية في الشيفرات. ومع ذلك يتوق البعض إلى الأقواس متمنين إضافتها إلى أحد إصدارات بايثون المستقبلية رغم مدى كونها غير بايثونية، ناهيك عن كون وحدات بايثون المستقبلية تُطبق تحديثاتها وميزاتها على الإصدارات الأقدم، ولدى محاولتك استيراد ميزة استخدام الأقواس في بايثون، ستحصل على المفاجأة التالية: >>> from __future__ import braces SyntaxError: not a chance لن تضاف الأقواس إلى بايثون على المدى المنظور. صيغ شائعة الاستخدام على نحو خاطئ إذا لم تكن لغة بايثون هي أولى اللغات البرمجية التي تعلمتها، فبإمكانك كتابة شيفراتها وفق الاسترتيجيات التي اعتدت استخدامها مع لغات البرمجة الأخرى، أو لربما أنك قد تعلمت طرقًا غير معهودة لكتابة شيفرات بايثون نظرًا لعدم درايتك بوجود ممارساتٍ راسخة مُحبذة، ستعمل شيفرتك المكتوبة بالطرق غير المعهودة، ولكن من الممكن أن توفر بعضًا من الوقت والجهد بتعلمك للمزيد من المنهجيات المعيارية لكتابة شيفرات بايثونية. يشرح هذا القسم أخطاء المبرمجين الشائعة وكيفية كتابة الشيفرات متجنبًا الوقوع بها. استخدم الدالة ()enumerate بدلا من ()range يستخدم بعض المبرمجون الدالتين ()range و()len لدى الحاجة إلى المرور على عناصر قائمة أو غيرها من البنى المتسلسلة بغية توليد الأرقام الصحيحة الدالة على فهرس (ترتيب) العناصر ابتداءً من الصفر ووصولًا إلى ما قبل طول السلسلة، ومن الشائع استخدام متغير باسم i للدلالة على الفهرس في حلقات for التكرارية هذه. فعلى سبيل المثال، بكتابة الشيفرة غير البايثونية في الصدفة التفاعلية سنحصل على الخرج المبين أدناه: >>> animals = ['cat', 'dog', 'moose'] >>> for i in range(len(animals)): ... print(i, animals[i]) ... 0 cat 1 dog 2 moose قد يكون الاصطلاح (()range(len واضح، لكنه لا يرقى لأن يعد مثاليًا نظرًا لصعوبة قراءته. فبدلًا من ذلك من الممكن تمرير القائمة أو السلسلة إلى دالة بايثون ()enumerate، والتي ستعيد عددًا صحيحًا دالًا على رقم الفهرس مع قيمة العنصر الذي يشير إليه كل فهرس. فعلى سبيل المثال، من الممكن كتابة الشيفرة البايثونية التالية وصولًا إلى نفس النتيجة السابقة: >>> # Pythonic Example >>> animals = ['cat', 'dog', 'moose'] >>> for i, animal in enumerate(animals): ... print(i, animal) ... 0 cat 1 dog 2 moose وبذلك ستكون شيفرتك أفضل مع استخدام الدالة ()enumerate عوضًا عن التركيب (()range(len. وفي حال رغبتك بطباعة عناصر القائمة فقط دون رقم فهرس كل منها، فمن الممكن أيضًا المرور على عناصر القائمة بطريقة بايثونية كما يلي: >>> # Pythonic Example >>> animals = ['cat', 'dog', 'moose'] >>> for animal in animals: ... print(animal) ... cat dog moose فكل من استخدام الدالة ()enumerate والمرور المباشر على عناصر السلسلة هي أمور مفضلة على استخدام التركيب التقليدي (()range(len. استخدام التعليمة with بدلا من الدالتين()open و()close تعيد الدالة ()open كائن ملف يتضمّن التوابع اللازمة للقراءة من ملف أو الكتابة فيه، وعند انتهائك يعمل التابع ()close الخاص بكائن الملف على إتاحة الملف للبرامج الأخرى لتقرأ منه أو تكتب فيه. كما من الممكن استخدام كل من هاتين الدالتين منفردة، إلا أن هذه الطريقة ليست بايثونية. فعلى سبيل المثال، لنُدخل الشيفرات التالية في الصدفة التفاعلية بغية كتابة العبارة النصية "!Hello, World" ضمن الملف المسمى spam.txt: >>> # Unpythonic Example >>> fileObj = open('spam.txt', 'w') >>> fileObj.write('Hello, world!') 13 >>> fileObj.close() كتابة الشيفرة بهذه الطريقة قد تودي بالنتيجة إلى إبقاء الملف مفتوحًا وغير متاحًا للبرامج الأخرى، ففي حال حدوث خطأ في كتلة try مثلًا، سيتجاهل الملف استدعاء الدالة ()close كما في المثال التالي: >>> # Unpythonic Example >>> try: ... fileObj = open('spam.txt', 'w') ... eggs = 42 / 0 # A zero divide error happens here. ... fileObj.close() # This line never runs. ... except: ... print('Some error occurred.') ... Some error occurred. فبمجرد الوصول إلى خطأ القسمة على صفر سينتقل التنفيذ مباشرةً إلى الكتلة except، متجاوزًا استدعاء الدالة ()close تاركًا بذلك الملف مفتوحًا، ما قد يؤدي بالنتيجة إلى أخطاء تلف الملفات file corruption لاحقًا، ومن الصعب تعقب الخطأ وتوقّع أن مصدره هو كتلة try. وبدلًا من الطريقة السابقة من الممكن استخدام التعليمة with والتي تستدعي الدالة ()close تلقائيًا بمجرد انتهاء تنفيذ الكتلة with. وفيما يلي مثال مكتوب بطريقة بايثونية يؤدي نفس مهمة المثال الأول من هذه الفقرة: >>> # Pythonic Example >>> with open('spam.txt', 'w') as fileObj: ... fileObj.write('Hello, world!') … فرغم عدم استدعاء الدالة ()close صراحةً، إلا أن التعليمةwith ستستدعيها نلقائيًا بمجرد انتهاء تنفيذ الكتلة البرمجية هذه. استخدم المعامل is بدلًا من == للمقارنة مع القيمة الخالية None يقارن معامل المساواة == قيمتي كائنين، في حين أن معامل التماثل is يقارن تطابق هويات الكائنات. فمن الممكن أن يتضمّن كائنين قيمًا متكافئة ولكن كونهما كائنان منفصلان فهذا يعني أن لكل منهما هويته المنفصلة عن الآخر. وعمومًا في حال مقارنة قيمة ما مع القيمة الخالية None استخدم دائمًا المعامل is عوضًا عن المعامل ==. فقد يُقيّم التعبير spam==None على أنه صحيح True في بعض الحالات حتى في حال كون المتغير spam فارغًا بالمعنى المجرد، الأمر الناتج عن زيادة تحميل المعامل ==. في حين أن التعبير spam is None سيتحقق من كون القيمة في المتغير spam هي حرفيًا None، إذ أنّ None هي القيمة الوحيدة ضمن نمط المعطيات الخالية None Type، فلا يوجد سوى كائن خالٍ None Object واحد في أي برنامج بايثون. فإن عُيّن أحد المتغيرات ليكون خاليًا None، فعندها سيُقيّم التعبير is None دومًا على أنه صحيح True، وفيما يلي مثال على حالة زيادة تحميل المعامل: >>> class SomeClass: ... def __eq__(self, other): ... if other is None: ... return True ... >>> spam = SomeClass() >>> spam == None True >>> spam is None False ورغم كون إمكانية أن يتسبب صنفًا بزيادة تحميل المعامل == بهذه الطريقة أمر نادر الحدوث، إلا أنه سبب وجيه لاصطلاح بايثون باستخدام التعبير is None عوضًا عن None == في هذه الحالات. ونهايةً، لا تستخدم المعامل is مع القيمتين True وFalse بل استخدم المعامل == لمقارنة هذه القيم، من قبيل spam == True أو Spam == False. والطريقة الأكثر شيوعًا في مثل هذه الحالات هي عدم استخدام المعامل والقيمة المنطقية بالمطلق، والاكتفاء بكتابة الشيفرة بالشكل :if spam أو :if not spam بدلًا من كتابة :if spam == True أو :if spam == False. تنسيق السلاسل النصية تظهر السلاسل النصية تقريبًا في كل برنامج بغض النظر عن لغة البرمجة المستخدمة، فهي من أنواع البيانات الشائعة، فمن المتوقع وجود العديد من المنهجيات لمعالجة السلاسل النصية وتنسيقها. يسلط هذا القسم الضوء على اثنين من أفضل الممارسات بهذا الخصوص. استخدم تنسيق السلاسل النصية الخام إذا تضمنت السلاسل عدة خطوط مائلة عكسية تمكننا محارف الهروب escape characters من إدخال النصوص ضمن صياغة السلسلة النصية والتي يستحيل تضمينها فيها دون استخدامها. فعلى سبيل المثال لابد من استخدام محرف الهروب \ في العبارة Ahmad\'s chair لتُفسر علامة الاقتباس الثانية على أنها جزء من السلسلة النصية وليست على أنها علامة انتهاء السلسلة. ولكن ماذا لو أردنا بالفعل تضمين الرمز \ بحد ذاته ضمن السلسلة النصية؟ عندها يجب استخدامه بالشكل \\. السلاسل النصية الخام عبارة عن طريقة لصياغة السلاسل النصية باستخدام البادئة r، وتتميز بأنها لا تعامل الخطوط المائلة العكسية كمحارف هروب، بل تعاملها كأي محرف من السلسلة نفسها. فعلى سبيل المثال، فيما يلي مسار ملف في بيئة ويندوز والذي يتطلب استخدام العديد من الخطوط المائلة العكسية كمحارف هروب لتضمين الفعلية منها في السلسلة النصية، الطريقة التي لا يمكن عدها بايثونية: >>> # Unpythonic Example >>> print('The file is in C:\\Users\\Al\\Desktop\\Info\\Archive\\Spam') The file is in C:\Users\Al\Desktop\Info\Archive\Spam أما باستخدام مفهوم السلاسل النصية الخام (لاحظ البادئة r) فسنحصل بالنتيجة على نفس السلسلة ولكن مع شيفرة ذات مقروئية أعلى: >>> # Pythonic Example >>> print(r'The file is in C:\Users\Al\Desktop\Info\Archive\Spam') The file is in C:\Users\Al\Desktop\Info\Archive\Spam لا يمكن عد السلاسل النصية الخام raw strings كنمط بيانات مستقل عن السلاسل النصية، فما هي سوى طريقة مناسبة لكتابة السلاسل النصية المُجردة المتضمنة للعديد من الخطوط المائلة العكسية، وعادة ما نستخدم السلاسل النصية الخام في كتابة التعابير العادية المستخدمة في تشكيل أنماط البحث أو في كتابة مسارات ملفات ويندوز والتي غالبًا ما تتضمن العديد من الخطوط المائلة العكسية، إذ سيكون من الصعب استخدام محرف هروب لكل منها بالشكل \\. نسق السلاسل النصية باستخدام السلاسل النصية التنسيقية F-Strings يُعرّف تنسيق السلاسل المحرفية أو معالجتها بأنه عملية إنشاء سلاسل محرفية تتضمن سلاسل محرفية أخرى، الأمر الذي مر بالعديد من المراحل خلال تاريخ بايثون. فمن الممكن استخدام المعامل + لربط السلاسل المحرفية معًا، إلا أنها ستعطي بالنتيجة شيفرة مليئة بإشارات التنصيص وعلامات الزائد من قبيل: 'Hello, ' + name + '. Today is ' + day + ' and it is ' + weather + '.' كما من الممكن استخدام مُحدّد التحويل %s الذي يجعل من الصياغة أسهل بعض الشيء بالشكل: 'Hello, %s. Today is %s and it is %s.' % (name, day, weather) وبالنتيجة ستعمل كلا الطريقتين على إدخال السلاسل النصية الموافقة مكان كل من المتغيرات name وday وweather في السلسلة النصية المجردة وصولًا إلى سلسلة نصية جديدة مثل: 'Hello, Al. Today is Sunday and it is sunny.' يعمل التابع ()format الخاص بالسلاسل المحرفية على تمكين لغة تخصيص التنسيق المصغرة Format Specification Mini-Language والتي تتضمن استخدام أزواج الأقواس المعقوصة {} بطريقة مماثلة لفكرة استخدام مُحدّد التحويل s%، إلا أن هذه الطريقة تنطوي على شيء من التعقيد ما قد يُنتج شيفرة بمقروئية منخفضة، لذا لا أشجع استخدامها. إلى أن جاء الإصدار 3.6 من بايثون بميزة السلاسل النصية التنسيقية f-strings (اختصارًا لعبارة format strings) التي توفر طريقة أكثر ملائمة لإنشاء سلاسل نصية تتضمن سلاسل نصية أخرى. وكما هو الحال مع السلاسل النصية الخام والتي نستخدم لإنشائها البادئة r قبل علامة الاقتباس الاستهلالية، نستخدم هنا البادئة f، وفيها نُضمّن أسماء المتغيرات المطلوبة ضمن أقواس معقوصة لاستبدال كل منها لاحقًا بالسلاسل النصية المُخزنة فيها، على النحو: >>> name, day, weather = 'Al', 'Sunday', 'sunny' >>> f'Hello, {name}. Today is {day} and it is {weather}.' 'Hello, Al. Today is Sunday and it is sunny.' كما من الممكن تضمين تعابير برمجية كاملة في الأقواس المعقوصة، كما في المثال: >>> width, length = 10, 12 >>> f'A {width} by {length} room has an area of {width * length}.' 'A 10 by 12 room has an area of 120.' وفي حال رغبتك باستخدام أقواس معقوصة فعلية ضمن السلسلة النصية، يمنك الهروب من اعتبارها دلالة على استبدال المتغير ضمنها بقيمته من خلال استخدام زوج إضافي منها، على النحو: >>> spam = 42 >>> f'This prints the value in spam: {spam}' 'This prints the value in spam: 42' >>> f'This prints literal curly braces: {{spam}}' 'This prints literal curly braces: {spam}' وبما أن هذه الطريقة تتيح كتابة أسماء المتغيرات والتعابير البرمجية ضمن سطر السلسلة النصية الأساسية نفسه، فإن الشيفرة بالنتيجة ستكون بمقروئية أعلى مقارنةً بالطرق القديمة لتنسيق السلاسل النصية. وجود هذه الطرق المتعددة يخالف الحكمة القائلة "لابد من وجود طريقة واحدة واضحة لإنجاز الأمر، ومن المفضل وجود طريقة واحدة فقط" من مبادئ بايثون التوجيهية العشرون، إلا أن السلاسل النصية التنسيقية f-strings قد جاءت كتطوير للغة بايثون (من وجهة نظري)، وكما أن أحد المبادئ التوجيهية يشير إلى أن "الواقع العملي غير مثالي"، فإن كنت تستخدم الإصدار 3.6 من بايثون وما بعده فاستخدم دومًا السلاسل النصية التنسيقية. أما في حال استخدامك للإصدارات الأقدم، فأنصحك باستخدام التابع ()format أو الاعتماد على محدد التحويل s%. إنشاء نسخ ضحلة عن القوائم lists من الممكن إنشاء سلاسل نصية أو قوائم جديدة من تلك الحالية باستخدام صياغة التجزئة، ولرؤية كيفية عملها اكتب التعليمات التالية في الصدفة التفاعلية: >>> 'Hello, world!'[7:12] # Create a string from a larger string. 'world' >>> 'Hello, world!'[:5] # Create a string from a larger string. 'Hello' >>> ['cat', 'dog', 'rat', 'eel'][2:] # Create a list from a larger list. ['rat', 'eel'] نضع رمز النقطتين الرأسيتين : بين فهرسي عنصر البداية والنهاية للسلسلة الجديدة المراد إنشاؤها، ولدى إهمال وضع فهرس البداية قبل النقطتين الرأسيتين كما في المثال '[Hello, world!'[:5، فيُعيّن حينها افتراضيًا إلى القيمة 0، أما إذا أهملنا فهرس النهاية بعد النقطتين الرأسيتين كما في المثال [:cat', 'dog', 'rat', 'eel'][2']، فيُعيّن افتراضيًا إلى فهرس العنصر الأخير من السلسلة الأم. أما إذا أهملت تعيين كلا الفهرسين، فسيتم تعيين فهرس البداية إلى 0 (أي بداية القائمة أو السلسلة) وفهرس النهاية إلى نهاية القائمة، الأمر الذي يمثل طريقة فعالة لإنشاء نسخة عن السلسلة: >>> spam = ['cat', 'dog', 'rat', 'eel'] >>> eggs = spam[:] >>> eggs ['cat', 'dog', 'rat', 'eel'] >>> id(spam) == id(eggs) False ومن الجدير بالملاحظة في المثال السابق أن هويات القائمتين spam وeggs مختلفتين رغم تطابق قيمهما، إذ أن السطر البرمجي [:]eggs = spam ينشئ نسخة ضحلة عن القائمة spam، في حين أن التعليمة eggs=spam ستنسخ فعليًا مرجع القائمة spam وتسنده إلى القائمة eggs، إلا أن استخدام التعليمة [:] قد يبدو غريبًا بعض الشيء، واستخدام الدالة ()copy من وحدة النسخ copy module لإنشاء نسخة ضحلة من القائمة تعد الطريقة الأعلى مقروئية: >>> # Pythonic Example >>> import copy >>> spam = ['cat', 'dog', 'rat', 'eel'] >>> eggs = copy.copy(spam) >>> id(spam) == id(eggs) False فمن المفضل معرفتك لهذه الصياغة الغريبة لحالات مصادفتك لشيفرات بايثون قد استخدمتها، أما في شيفراتك الخاصة، فلا ننصحك باستخدامها. وتذكر أن كلًا من [:] و ()copy.copy تُنشآن نسخًا ضحلة. الخلاصة ما من لغة برمجة إلا ولديها الاصطلاحات والممارسات الأفضل الخاصة بها. وقد ركّز هذا المقال على الطرق العملية التي يستخدمها مبرمجي بايثون لكتابة شيفرات "بايثونية" ما يضمن الاستخدام الأمثل لميزات صياغة بايثون البرمجية. ولعل حجر الأساس وجوهر الشيفرات البايثونية هو "مبادئ بايثون التوجيهية العشرون"، وهي عبارة عن توجيهات عامة للكتابة بلغة بايثون. إلا أن هذه الحكم العشرون اختيارية وليست إلزامية لكتابة شيفرات بايثون، ومع ذلك من الجيد تذكرها دومًا. تثير المسافات البادئة ذات المعنى (والتي يجب عدم الخلط بينها وبين المسافات البيضاء) معظم كم استغراب واحتجاج مبرمجي بايثون المبتدئين، ورغم كون جميع لغات البرمجة تقريبًا تستخدم المسافات البادئة بغية جعل الشيفرات أسهل للقراءة، إلا أن بايثون تفرضها كبديل للأقواس المعقوصة المستخدمة في باقي لغات البرمجة. ورغم كون العديد من مبرمجي بايثون يستخدمون التركيب (()range(len للحلقات التكرارية، إلا أن الدالة ()enumerate توفّر منهجية أوضح للحصول على رقم الفهرس والقيمة الموافقة له لدى المرور على سلسلة ما. وكذلك الأمر بالنسبة للعبارة with الأوضح والأقل تسببًا بالأخطاء للتعامل مع الملفات مقارنةً باستدعاء التابعين ()open و()close يدويًا، إذ تضمن العبارة with استدعاء التابع ()close عند انتهاء التنفيذ وخروجه من الكتلة الخاصة بها. ولدى بايثون العديد من الطرق للتعامل مع السلاسل النصية ومعالجتها، ولعل الطريقة الأقدم هي استخدام محدد التحويل s% لتحديد المواضع المراد تضمينها ضمن السلسلة الرئيسية كجزء منها، أما الطريقة الأحدث والتي غدت موجودة اعتبارًا من الأصدار 3.6 فهي استخدام السلاسل النصية التنسيقية f-strings، وتُستخدم من خلال البادئة f قبل السلسلة النصية المراد صياغتها وبحصر الأجزاء المراد تضمينها في السلسلة ضمن أقواس معقوصة. أما الصيغة [:] المستخدمة لإنشاء نسخ ضحلة عن القوائم قد غدت قديمة نسبيَا وقد لا تعد طريقة بايثونية، إلا أنها قد غدت طريقة شائعة لإنشاء النسخ الضحلة بسرعة. ترجمة -وبتصرف- لجزء من الفصل السادس "كتابة شيفرات بايثون" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: خرافات حول ممارسات تجنب أخطاء محتملة في شيفرات لغة بايثون كيفية تنسيق النصوص في بايثون اكتشاف دلالات الأخطاء في شيفرات لغة بايثون
-
نستكمل في هذا الفيديو الحديث عن الشبكات، وذلك بالتطرق إلى أحد أهم الأجهزة في النظام الشبكي بالكامل وهو المبدل Switch. وكما هو معروف، تضم الشبكات بعض الأجهزة كعناصر أساسية من عناصرها، ولعل من أهم وأشهر هذه الأجهزة هو المبدل نظرًا لما يقوم به من وظائف متعددة لا غنى عنها في أي شبكة. ولكن ما هو المبدل؟ وماهي وظيفته؟ وماهي أنواعه؟ سنتعرف على كل ذلك بالتفصيل في الفيديو الآتي: إذا أردت التعرف أكثر على مجال الشبكات، فننصحك بالانضمام إلى دورة علوم الحاسوب، ولا تنسَ الاستعانة خلال رحلة تعلمك وعملك بتوثيقات موسوعة حسوب المجانية. وإذا أردت متابعة المعلومات البرمجية العلمية مكتوبة فيمكنك الاطلاع على قسم البرمجة في أكاديمية حسوب، كما يمكنك متابعة جديد الفيديوهات التقنية المتاحة على يوتيوب أكاديمية حسوب مجانًا.
-
الشبكة هي عبارة عن مجموعة من الأجهزة المتصلة مع بعضها على اختلاف أنواعها باستخدام تجهيزات وبرمجيات خاصة وظيفتها أن تتيح لهذه الأجهزة التواصل وتبادل المعلومات بشكل سريع وفعال. ويمكن عدّ الشبكة أيضًا على أنها النظام الذي يحكم هذه الأجهزة لتتمكن من مشاركة الموارد فيما بينها. وعلى اختلاف أنواع الشبكات فإنها غالبًا ما تتكون من نفس العناصر الأساسية والتي من الضروري تواجدها في أي نظام شبكي. لكن ما هي عناصر ومكونات الشبكة؟ لمعرفة مكونات الشبكة، تابعوا معنا الفيديو الآتي: إذا أردت التعرف أكثر على مجال الشبكات، فننصحك بالانضمام إلى دورة علوم الحاسوب، ولا تنسَ الاستعانة خلال رحلة تعلمك وعملك بتوثيقات موسوعة حسوب المجانية. وإذا أردت متابعة المعلومات البرمجية العلمية مكتوبة فيمكنك الاطلاع على قسم البرمجة في أكاديمية حسوب، كما يمكنك متابعة جديد الفيديوهات التقنية المتاحة على يوتيوب أكاديمية حسوب مجانًا.
-
تعرفنا في المقال السابق على مفهوم روائح الشيفرات Code Smells في لغة بايثون، والتي تعني دلالات وقوع الأخطاء، فبعض الإشارات قد تدل على وجود أخطاء خفية أو محتملة أو على كون مقروئية الشيفرة ضعيفة. إلا أن بعض دلالات الأخطاء هي في الواقع ليست مؤشرات لوقوع أخطاء بالمطلق، فعالم البرمجة مليء بالنصائح السيئة أو التي عفا عليها الزمن والتي أصبحت خارج السياق الحالي أو تلك التي ما زالت موجودة رغم انتهاء صلاحية فائدتها، وألقي اللوم في هذا الصدد على مؤلفي الكتب التقنية ممن يقدمون آرائهم الشخصية على أنها أفضل الممارسات. لا بد وأنه قد تم تقديم بعضًا من الممارسات التالية لك على أنها من دلالات وقوع الأخطاء، رغم كون معظمها ممارسات جيدة، ما دفعنا لتسميتها خرافات دلالات الأخطاء، فهي تحذيرات حريٌ بك تجاهلها، لا بل يجب عليك تجاهلها، دعونا نطلع على البعض منها. خرافة أن الدالة يجب أن تتضمن تعليمة قيمة معادة return وحيدة وفي نهايتها أتت فكرة "مُدخل وحيد – مُخرج وحيد" من نصيحة مُساءة الفهم من أيام لغات البرمجة assembly و FORTRAN، إذ كانت هذه اللغات تسمح بإدخال إجراء فرعي (وهي بنية شبيهة بالدالة) عند أي نقطة من الشيفرة، حتى في وسطها، ما يجعل من الصعب تنقيح الأجزاء التي تم تنفيذها من هذا الإجراء الفرعي، إلا أن الدوال الحالية لا تعاني من هذه المشكلة، إذ يبدأ تنفيذ الدالة من بدايتها دومًا، ومع ذلك بقيت النصيحة قائمة ليكون مفادها "يجب أن تتضمن الدوال والتوابع تعليمة return واحدة والتي يجب أن تكون تحديدًا في نهاية الدالة أو التابع". وخلال محاولة تنفيذ هذه النصيحة (وجود تعليمة return واحدة ضمن الدالة أو التابع)، ستضطر غالبًا إلى استخدام سلسلة معقدة من الجمل الشرطية، والتي ستكون أكثر إرباكًا من وجود عدة تعليمات return ضمن بناء الدالة أو التابع. خرافة أن الدالة يجب أن تتضمن تعليمة try واحدة على الأكثر لعل النصيحة التي مفادها "أن الدوال والتوابع يجب أن تؤدي مهمة واحدة فقط" جيدة إجمالًا، طالما أنها لم تُفهم على أن التعامل مع الاستثناءات يجب أن يتم في دالة منفصلة. لنأخذ الدالة التالية التي تبين ما إذا كان الملف المراد حذفه غير موجود أصلًا كمثال: >>> import os >>> def deleteWithConfirmation(filename): ... try: ... if (input('Delete ' + filename + ', are you sure? Y/N') == 'Y'): ... os.unlink(filename) ... except FileNotFoundError: ... print('That file already did not exist.') … إذ يجادل مؤيدو خرافة دلالة الخطأ آنفة الذكر بأنه طالما أن الدالة معنية بتأدية مهمة واحدة، وطالما أن التعامل مع الأخطاء هو مهمة واحدة، لذا يجب تقسيم الدالة السابقة إلى دالتين. كما يجادلون بأنه عند استخدام التعليمة try-except ضمن الدالة، فيجب أن تكون هي التعليمة الأولى ضمنها، حاصرةً كامل شيفرة الدالة لتبدو كما يلي: >>> import os >>> def handleErrorForDeleteWithConfirmation(filename): ... try: ... _deleteWithConfirmation(filename) ... except FileNotFoundError: ... print('That file already did not exist.') ... >>> def _deleteWithConfirmation(filename): ... if (input('Delete ' + filename + ', are you sure? Y/N') == 'Y'): ... os.unlink(filename) … وهي شيفرة معقدة بلا ضرورة، إذ تم تعيين الدالة ()_deleteWithConfirmation لتكون خاصة باستخدام البادئة _ للدلالة على عدم إمكانية استدعائها مباشرةً، وإنما بشكل غير مباشر من خلال استدعاء الدالة ()handleErrorForDeleteWithConfirmation، إذ أن اسم هذه الدالة الجديدة غير ملائم، فنحن نستدعيها بنية حذف ملف، لا بنية التعامل مع الأخطاء الناتجة أثناء حذف ملف. إذًا يجب أن تكون الدوال صغيرة وبسيطة، ولكن هذا لا يعني أنها يجب أن تكون محدودة لأداء "أمر واحد" (بالمعنى الذي تفهمه)، فمن الجيد أن تتضمن الدوال عدة تعليمات try-except ودون أن تحصر إحداها كامل شيفرة الدالة. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن خرافة أن الوسيط المنطقي خيار سيئ يطلق على الوسطاء من النوع قيم منطقية Boolean الممررة لدى استدعاء الدوال أو التوابع اسم وسطاء الأعلام أو الرايات flag arguments، والراية في البرمجة هي قيمة تشير إلى عملية إعداد أو ضبط وفق النظام الثنائي، كما في السمتين enabled الدالة على التمكين وdisabled الدالة على إلغاء التمكين، إذ غالبًا ما يعبّر عن الراية بقيمة منطقية، وبذلك يمكننا التعبير عن هذه الإعدادات بأنها إما معيّنة True أو ملغاة False. وقد أتى الاعتقاد المغلوط بأن استخدام وسطاء الرايات لاستدعاء الدوال هو إجراء سيئ، استنادًا على الادعاء القائل بأنه بناءً على قيمة الراية فإن الدالة ستؤدي مهمتين مختلفتين تمامًا، كما في المثال التالي: def someFunction(flagArgument): if flagArgument: # Run some code... else: # Run some completely different code… في الواقع، إن كانت دالتك تبدو بهذا الشكل، فمن الأفضل حتمًا إنشاء دالتين منفصلتين بدلًا من إنشاء وسيط مهمته تحديد أي نصف من شيفرة الدالة سيعمل، إلا أن معظم الدوال ذات وسطاء الرايات ليست بهذا الشكل، فمثلًا من الممكن تمرير قيمة منطقية للوسيط الافتراضي القابل للعكس للدالة ()sorted بغية تحديد طريقة ترتيب الفرز، ففي مثل هذه الحالة من غير المجدي تقسيم الدالة إلى دالتين الأولى باسم ()sorted والأخرى باسم ()reverseSorted، ناهيك عن عبء التكرار المطلوب. وبالنتيجة فإن فكرة أن وسطاء الرايات سيئة دومًا هي محض خرافة. خرافة أن المتغيرات العامة خيار سيئ تمثّل كل من الدوال والتوابع ما يشبه البرامج الصغيرة ضمن برنامجك الرئيسي، إذ أنها تتضمن شيفرات فيها متغيرات محلية تُنسى بمجرد إعادة الدالة لقيمتها، بما يشبه كيفية نسيان متغيرات البرامج وتحرير الذاكرة بمجرد انتهائها، إلا أن الدوال معزولة، فإما أن تعمل شيفراتها بالشكل الصحيح أو أن تواجه أخطاءً اعتمادًا على الوسطاء الممررة إليها لدى استدعائها. إلا أن الدوال والتوابع التي تستخدم المتغيرات العامة تفقد جزءًا من هذه الانعزالية المفيدة، ففي حين أن كل متغير عام تستخدمه في الدالة سيعمل كمُدخل جديد إليها بما يشبه فكرة الوسطاء، إلا أن زيادة عدد الوسطاء يعني زيادة التعقيدية، ما يعني بدوره احتمالية أعلى لوقوع الأخطاء، فعند ظهور خطأ في دالة ما ناتج عن قيمة خاطئة لمتغير عام، فإن هذه القيمة قد تكون مُعينة في أي جزء من البرنامج الرئيسي، وفي هذه الحالة لا يكفي تحليل الشيفرة الخاصة بالدالة أو السطر من البرنامج الذي يتم فيه استدعاؤها بحثًا عن السبب المحتمل لهذه القيمة الخاطئة، بل لا بد من مراجعة كامل شيفرة البرنامج، ولهذا السبب يجب أن تقلل من استخدامك للمتغيرات العامة. فعلى سبيل المثال، لنتمعن في الدالة ()calculateSlicesPerGuest (التي تعني حساب عدد قطع الحلوى لكل ضيف) من البرنامج partyPlanner.py (منسق الحفلة) المُفترض، إذ ضمّنا فيه أرقام الأسطر لتقديم فكرة عن حجم البرنامج الكبير: 1504. def calculateSlicesPerGuest(numberOfCakeSlices): 1505. global numberOfPartyGuests 1506. return numberOfCakeSlices / numberOfPartyGuests وبفرض أننا واجهنا لدى تشغيل البرنامج الاستثناء التالي: Traceback (most recent call last): File "partyPlanner.py", line 1898, in <module> print(calculateSlicesPerGuest(42)) File "partyPlanner.py", line 1506, in calculateSlicesPerGuest return numberOfCakeSlices / numberOfPartyGuests ZeroDivisionError: division by zero يعرض البرنامج خطأ القسمة على صفر، وهو ناتج عن سطر القيمة المعادة return numberOfCakeSlices / numberOfPartyGuests، وبالتالي لا بد من ان قيمة المتغير numberOfPartyGuests تساوي الصفر ما سبب ظهور هذا الخطأ، والسؤال: أين تم إسناد قيمة الصفر لهذا المتغير؟ فهو متغير عام، وبالتالي من الوارد أن يتم هذا الأمر في أي مكان ضمن آلاف الأسطر التي يتكون منها هذا البرنامج، لكننا نعلم من معلومات متتبع الأخطاء أن استدعاء الدالة ()calculateSlicesPerGuest قد تم في السطر رقم 1898 من البرنامج. بالذهاب إلى هذا السطر، يمكننا التحقق من قيمة الوسيط الممرر كقيمة للمتغير numberOfPartyGuests، إلا أن هذا لا يعني بالضرورة العثور على سبب الخطأ، فالمتغير numberOfPartyGuests كما ذكرنا سابقًا عام، وقد تكون القيمة الصفرية قد أُسندت إليه في أي سطر من البرنامج قبل استدعاء الدالة. ومن الجدير بالملاحظة أن الثوابت العامة لا تعتبر إجراء برمجي ضعيف، كون قيمها لا تتغير أثناء تنفيذ البرنامج، فهي لا تضيف على الشيفرة مزيدًا من التعقيدية كما تفعل المتغيرات العامة، فالثوابت غير مقصودة بالجملة "المتغيرات العامة خيار سيئ". مما لا شك فيه أن استخدام المتغيرات العامة يزيد من عبء التنقيح اللازم وصولًا إلى القيمة المسببة للاستثناء، وهذا ما يجعل فكرة الاستخدام الواسع للمتغيرات العامة غير محبذة، ولكن القول بأن كل المتغيرات العامة سيئة هي بالطبع خرافة، فالمتغيرات العامة قد تكون مفيدة في البرامج الأصغر أو لتتبع الإعدادات المطبقة على كامل البرنامج. وإجمالًا إن كنت قادرًا على تجنب استخدام متغير عام، فهي دلالة على وجوب عدم استخدامه، أما التعميم القائل "المتغيرات العامة سيئة" فهو رأي مسرف في التبسيط. خرافة أن التعليقات غير ضرورية في الواقع، وجود تعليقات سيئة هو أمرٌ أسوأ من حالة عدم وجود تعليقات بالمطلق، فالتعليق المُتضمن لمعلومات قديمة أو مشوشة سيزيد من عبء العمل على المبرمج بدلًا من مساعدته في تحقيق فهم أفضل، إلا أنّ هذه المشكلة المحتملة في التعليقات قد اتُخذت ذريعةً للتعميم بأن التعليقات ككل سيئة، ويجادل أصحاب هذا الرأي بأنه بدلًا من إضاعة الوقت بالتعليقات، يجب استبدالها بشيفرات عالية المقروئية (لا تحتاج لتعليقات أصلًا) لدرجة أنهم يرون بأن البرامج يجب ألا تحتوي على تعليقاتٍ بالمطلق. تُكتب التعليقات باللغة الإنجليزية أو باللغة التي يتقنها المبرمج أيًا كانت، سامحةً للمبرمجين بتبادل المعلومات بطريقة لا يمكن لأسماء المتغيرات والدوال والأصناف وحدها فعلها، إلا أن كتابة تعليق مختصر مفيد ليس بالأمر السهل، فالتعليقات كالشيفرات، تتطلب منك إعادة الكتابة والمراجعة والتكرار عدة مرات وصولًا لأفضل صيغة. وبما أننا نستطيع فهم الشيفرات التي كتبناها مباشرة بعد الفروغ من كتابتها، فإننا قد نشعر بلا جدوى كتابة التعليقات معتبرين إياها عمل إضافي لا طائل منه، ما يجعل المبرمجين بالنتيجة على استعداد لتقبل وجهة النظر القائلة بأن "التعليقات غير ضرورية". ولعل الحالة الأكثر شيوعًا هي وجود برامج بتعليقات قليلة أو بدون تعليقات أكثر من تلك المحتوية على تعليقات كثيرة جدًا أو تعليقات مُضللة. وإجمالًا فإن رفضك للتعليقات يشبه تمامًا اختيارك للسباحة عبر المحيط الأطلسي كون السفر بالطائرة عبره آمن "فقط" بنسبة 99.999991 بالمائة. الخاتمة تشير رائحة الشيفرة أو ما أسميناه دلالات الأخطاء على وجود طريقة أفضل لكتابة شيفراتك، وهي لا تعني بالضرورة وجوب إجراء تغييرات، لكنها ترشدك لإلقاء نظرة ثانية تأكيدية. ولكن ليست كل دلالات الأخطاء فعلية، فبعضها لا يتعدى كونه خرافات، كالنصائح البرمجية التي لم تعد صالحة أو تلك التي أثبتت أنها تعود على المبرمج بنتائج عكسية، كخرافة وجوب استخدام تعليمة return أو كتلة try-except واحدة ضمن بناء الدالة، مع عدم استخدام وسطاء الأعلام أو المتغيرات العامة بالمطلق، ناهيك عن الاعتقاد القائل بأن التعليقات غير ضرورية. تذكر دائمًا بأنه لا يوجد مقياس موضوعي لأفضل الممارسات، فمع اكتسابك للمزيد من الخبرة، ستخرج باستنتاجات عديدة حول ما يجعل شيفرتك أكثر مقروئية وموثوقية. ترجمة -وبتصرف- للجزء الثاني من الفصل الخامس Finding Code Smells من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigarti. اقرأ أيضًا المقال السابق: اكتشاف دلالات الأخطاء في شيفرات لغة بايثون ثلاثة أخطاء عليك تفاديها عند تعلم البرمجة بلغة بايثون التعامل مع رسائل الأخطاء في بايثون
-
مما لا شك فيه أن الشيفرة التي تسبب توقف عمل البرنامج هي خاطئة حُكمًا، والسؤال: هل هذا النوع من الأعطال هو الدليل الوحيد على وجود مشاكل في البرنامج؟ بالطبع لا، فبعض الإشارات قد تدل على وجود أخطاء خفية أو على كون مقروئية الشيفرة ضعيفة. وكما هو الحال مع رائحة الغاز التي قد تشير إلى وجود تسرّب في الغاز أو رائحة الدخان التي قد تشير لنشوب حريق، "فرائحة" الشيفرة Code Smells هي نمط للشيفرة المصدرية يشير لوجود أخطاء محتملة، ولا يعني بالضرورة وجود مشكلة، إلا أنه يدل على ضرورة التحقق من الشيفرة. سنقدم في هذا المقال عددًا من "روائح" الشيفرات الأكثر شيوعًا التي يحتمل أن تحمل أخطاءً، وانطلاقًا من مبدأ درهم وقاية خير من قنطار علاج، سيستغرق تجنب وقوع الأخطاء وقتًا وجهدًا أقل من مواجهتها وفهمها وإصلاحها لاحقًا، فلكل مبرمج مغامراته في قضاء ساعات بالتنقيح ليكتشف لاحقًا أن إصلاح الخطأ يشمل سطر برمجي واحد من الشيفرة، ولهذا السبب حتى أبسط دلالة على خطأ محتمل يجب أن تستوقفك، موجهةً إياك لإجراء المزيد من التحقق والتأكد من كونك لا تتسبب بحدوث مشاكل مستقبلية. وبالتأكيد لا تعني "رائحة" الشيفرة وجود مشكلة بالضرورة، وبالنتيجة وكما اعتدنا فالربان هو أنت، والقرار حول التعامل مع هذه الرائحة أو إهمالها هو قرار شخصي يعود إليك. قبل التعرف على كيفية اكتشاف أخطاء الشيفرات في لغة بايثون، ندعوك للتعرف على الأخطاء البرمجية عامةً أولًا والتعرف على كيفية التعامل معها: والآن دعونا نتحدث عن دلالات وجود الأخطاء في شيفرات لغة بايثون. تكرار الشيفرات إحدى أكثر دلالات الأخطاء (روائح الشيفرات) شيوعًا هي الشيفرات المكررة، وهي بالتعريف أي شيفرة مصدرية مُنشأة باستخدام نسخ ولصق أجزاءً من شيفرة أُخرى ضمن شيفرتك الحالية عدة مرات، فمثلًا يتضمن البرنامج التالي شيفراتٍ مكررة، إذ نلاحظ أنها تسأل المستخدم عن حاله (?How are you feeling) ثلاث مرات: print('Good morning!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') print('Good afternoon!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') print('Good evening!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') وتعد الشيفرات المكررة مشكلةً كونها تجعل من تعديل الشيفرة أصعب، فتعديل واحدة من النسخ المكررة يتطلب منك تعديل باقي النسخ جميعها، وفي حال نسيانك لإجراء التعديل في إحدى النسخ أو في حال إجراء تعديلات مختلفة عن بعضها للنسخ، فسينتهي الأمر يظهر أخطاء عند التنفيذ. والحل للشيفرات المكررة يكون بإلغاء تكرارها، بجعلها تظهر مرة واحدة ضمن الشيفرة وتكرارها بالعدد المطلوب باستخدام تابع ما أو حلقة، ففي المثال التالي تخلصنا من التكرار عبر حصر الجزء المكرر من الشيفرة ضمن تابع، لنستدعي التابع عدة مرات على التوالي لاحقًا: def askFeeling(): print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') print('Good morning!') askFeeling() print('Good afternoon!') askFeeling() print('Good evening!') askFeeling() أما في المثال التالي، فتخلصنا من التكرار عبر حصر الجزء المكرر ضمن حلقة: for timeOfDay in ['morning', 'afternoon', 'evening']: print('Good ' + timeOfDay + '!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') كما من الممكن دمج كلا تقنيتي التخلص من التكرار باستخدام دالة وحلقة معًا، بالشكل: def askFeeling(timeOfDay): print('Good ' + timeOfDay + '!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') for timeOfDay in ['morning', 'afternoon', 'evening']: askFeeling(timeOfDay) ونلاحظ أن الشيفرة المسؤولة عن عرض رسائل الترحيب "!Good morning/afternoon/evening" أي صباح/ظهر/مساء الخير الناتجة بعض التخلص من التكرار متشابهة ولكنها غير متطابقة، ففي التطوير الثالث لحل المشكلة ثبتنا الأجزاء المتطابقة من الشيفرة لتجنب التكرار، في حين يحل المعامل timeOfDay ومتغير الحلقة timeOfDay محل الأجزاء المتغيرة، وبذلك وبعد التخلص من التكرار عبر إزالة النسخ الزائدة، ما علينا سوى التعديل في مكان واحد متى ما احتجنا ذلك. وكما هو الحال مع كافة دلالات وقوع الأخطاء، فإن التخلص من التكرار ليست بالقاعدة الصارمة التي عليك اتباعها دومًا، فالمعيار هو طول القسم المكرر أو عدد مرات تكرار النسخ، فبزيادة أي منهما تصبح الحاجة إلى إلغاء التكرار أكثر إلحاحًا، فمثلًا لا نعد تكرار الشيفرات لمرة أو اثنتين دلالة لوقوع الأخطاء، وأنا شخصيًا أفكر بإزالة التكرار في حال وجود ثلاث أو أربع نسخ مكررة في برنامجي فأكثر. ففي بعض الأحيان قد تكون الشيفرة أبسط من أن تستحق عناء إلغاء التكرار، فلو قارنا الشيفرة الأولى من هذا القسم بالأخيرة، ورغم كون الشيفرة الأولى المكررة أطول، إلا أنها بسيطة ومباشرة، وتقوم الشيفرة الأخيرة بعد إزالة التكرار عمليًا بالوظيفة نفسها إلا أنها تتضمن حلقة ومتغير لها باسم timeOfDay، بالإضافة إلى تابع ذي معامل باسم timeOfDay أيضًا. يعد تكرار الشيفرات من دلالات وقوع الأخطاء لأنه يجعل من شيفرتك أصعب للتعديل، ففي حال وجود العديد من التكرارات في برنامجك، يكون الحل بحصر الجزء المكرر ضمن حلقة أو دالة ليظهر لمرة واحدة فقط. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن الأرقام السحرية ليس عجبًا إن قلنا أنّ البرمجة تتضمن استخدام الأرقام، إلا أن بعض الأرقام التي تظهر ضمن شيفرتك المصدرية قد تكون مصدر إرباك لمبرمج آخر يقرؤها (أو حتى لك أنت بعد مرور عدة أسابيع على كتابتك لها) وهي ما ندعوه بالأرقام السحرية magic numbers أي تلك الأرقام في الشيفرة التي قد تبدو عشوائية أو عديمة السياق والمعنى، فعلى سبيل المثال، لاحظ الرقم 604800 في السطر البرمجي التالي: expiration = time.time() + 604800 تعيد الدالة ()time.time رقمًا صحيحًا يمثل الوقت الحالي، ويمكننا فهم أن المتغير expiration والذي يعني وقت الانتهاء يمثل لحظة زمنية ما بعد مرور زمن قدره 604800 ثانية، إلا أن هذا الرقم هو مصدر الإرباك الأساسي، وأول ما سيتبادر إلى الذهن "ما مغزى تاريخ انتهاء الصلاحية هذا؟"، وفي الواقع، تعليق بسيط قد يحل المشكلة، بالشكل: expiration = time.time() + 604800 # Expire in one week. يمكن عد التعليق السابق حلًا جيدًا، إلا أن الأفضل هو استبدال الأرقام السحرية بثوابت، وتعرّف الثوابت بأنها متغيرات تُكتب بأحرف كبيرة دلالةً على أن قيمها يجب ألا تتغير عن القيمة البدائية المُسندة لها، وعادةً ما نصرح عن الثوابت في قسم التصريحات العامة، أي في بداية ملف الشيفرة، كما في الشكل: # Set up constants for different time amounts: SECONDS_PER_MINUTE = 60 SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY --snip-- expiration = time.time() + SECONDS_PER_WEEK # Expire in one week. إذ يجب استخدام ثوابت مختلفة للأرقام السحرية المستخدمة لأغراض مختلفة، حتى إن كانت قيمة الرقم نفسها، فعلى سبيل المثال، تضم أوراق اللعب 52 ورقة كما تضم السنة 52 أسبوعًا، ولكن في حال استخدامك لهذين المقدارين في شيفرتك، عليك التمييز بينهما كما في المثال: NUM_CARDS_IN_DECK = 52 NUM_WEEKS_IN_YEAR = 52 print('This deck contains', NUM_CARDS_IN_DECK, 'cards.') print('The 2-year contract lasts for', 2 * NUM_WEEKS_IN_YEAR, 'weeks.') ولدى تشغيل الشيفرة السابقة، سيبدو الخرج بالشكل: This deck contains 52 cards. The 2-year contract lasts for 104 weeks. إذ أن استخدام ثوابت منفصلة يساعدك على تغيير قيمة كل منها على حدى مستقبلًا، ومن الجدير بالملاحظة أن قيمة الثابت يجب ألا تتغير أثناء تنفيذ البرنامج، ولكن هذا لا يعني أن المبرمج غير قادر على تحديث أو تغيير قيمته ضمن الشيفرة المصدرية، فمثلًا في حال تضمن إصدار جديد من الشيفرة السابقة على ورقة لعب إضافية وهي المهرج (جوكر)، فعندها من الممكن تغيير قيمة الثابت الدال على عدد أوراق اللعب لتصبح 53 بالشكل: NUM_CARDS_IN_DECK = 53 NUM_WEEKS_IN_YEAR = 52 كما من الممكن إسقاط مصطلح الرقم السحري على بعض القيم غير الرقمية، فمن الممكن مثلًا استخدام قيم من سلاسل نصية كثوابت، فلنأخذ مثلًا الشيفرة التالية التي تطلب من المستخدم إدخال اتجاه من الاتجاهات الأربعة لتعيد رسالة تحذيرية في حال كون الاتجاه المدخل هو الشمال north، فإن الخطأ الطباعي في كتابة كلمة "شمال" على الصورة "nrth" ستؤدي لوقوع مشكلة ستمنع البرنامج من عرض رسالة التحذير المطلوبة: while True: print('Set solar panel direction:') direction = input().lower() if direction in ('north', 'south', 'east', 'west'): break print('Solar panel heading set to:', direction) 1 if direction == 'nrth': print('Warning: Facing north is inefficient for this panel.') والواقع أن هذا الخطأ من أنواع الأخطاء التي يصعب اكتشافها، نظرًا لكون السلسلة النصية nrth الواردة في السطر رقم 1 صحيحة من وجهة نظر بايثون رغم كونها خاطئة لغويًا، وبالتالي لن يتوقف البرنامج عن العمل، ولكن سنلاحظ عدم ظهور رسالة التحذير المتوقعة، أما في حال وقوعنا بنفس هذا الخطأ الطباعي مع استخدام الثوابت، فسيتوقف البرنامج عارضًا رسالة خطأ مفادها عدم وجود ثابت باسم NRTH كما يلي: # Set up constants for each cardinal direction: NORTH = 'north' SOUTH = 'south' EAST = 'east' WEST = 'west' while True: print('Set solar panel direction:') direction = input().lower() if direction in (NORTH, SOUTH, EAST, WEST): break print('Solar panel heading set to:', direction) 1 if direction == NRTH: print('Warning: Facing north is inefficient for this panel.') إن السطر رقم 1 من الشيفرة السابقة مع الخطأ الطباعي في كلمة NRTH سيثير لدى تشغيله استثناء NameError جاعلًا مصدر الخطأ جليًا على الفور: Set solar panel direction: west Solar panel heading set to: west Traceback (most recent call last): File "panelset.py", line 14, in <module> if direction == NRTH: NameError: name 'NRTH' is not defined إذًا تعد الأرقام السحرية من دلالات وقوع الأخطاء كونها لا تعبّر عن الغرض من استخدامها، جاعلةً من الشيفرة أقل مقروئية وأصعب للتطوير وعرضة لأخطاء إملائية يصعب اكتشافها، ويكون الحل باستخدام الثوابت بدلًا منها. إلغاء الشيفرات بتحويلها إلى تعليقات ومفهوم الشيفرة الميتة لعل تحويل الشيفرات إلى تعليقات بحيث لا تُنفّذ يمثّل إجراءً مؤقتًا جيدًا، لا سيما حين ترغب بتجاهل بعض الأسطر بغية اختبار آلية عمل باقي الأسطر وحدها، وبتحويل الأسطر المرغوب بتجاهلها يعد إجراء مناسب من حيث سهولة إعادتها إلى العمل جددًا لاحقًا، ولكن ماذا لو بقي السطر البرمجي كتعليق؟ سيكون مصدرًا للغموض في شيفرتك من حيث سبب إزالته أو تحت أي شروط قد نصبح بحاجة لوجوده مجدًدا. لنأخذ المثال التالي: doSomething() #doAnotherThing() doSomeImportantTask() doAnotherThing() تثير هذه الشيفرة العديد من التساؤلات، لم حوّلت الدالة ()doAnotherThing إلى تعليق؟ هل من المتوقع أن نحتاجها لاحقًا؟ لمَ لم يتم إقصاؤها لدى استدعائها للمرة الثانية؟ هل كانت هذه الشيفرة في الأصل تستدعي الدالة ()doAnotherThing مرتين؟ أم أنه استدعاء واحد أصلًا وتم نقله إلى ما بعد استدعاء الدالة ()doSomeImportantTask؟ هل من سبب واضح لعدم حذف هذه الدالة المستبعدة؟ في الواقع، ما من إجابات متاحة بسهولة لكل هذه التساؤلات. فالشيفرة الميتة Dead code بالتعريف هي كل شيفرة لا يمكن تنفيذها منطقيًا، كالشيفرات المتوضعة ضمن بناء الدالة ولكن بعد تعليمة الإعادة، أو الشيفرات المصورة ضمن جملة شرطية ذات شرط غير محقق على الدوام، أو الشيفرات ضمن دالة لا يتم استدعاؤها أبدًا، وكمثال عملي حول الشيفرات الميتة، اكتب المثال التالي ضمن الصدفة التفاعلية: >>> import random >>> def coinFlip(): ... if random.randint(0, 1): ... return 'Heads!' ... else: ... return 'Tails!' ... return 'The coin landed on its edge!' ... >>> print(coinFlip()) Tails! في الشيفرة السابقة، يمكن عد القيمة المعادة "!The coin landed on its edge" كشيفرة ميتة، إذ أن الشيفرة ستعيد قيم لحالات تحقق وعدم تحقق الشرط قبل أن يتمكن التنفيذ من بلوغ هذا السطر. قد تسبب الشيفرات الميتة الضياع لأن المبرمج القارئ لهذه الشيفرة سيعتبرها كجزء فعال من البرنامج في حين أن تأثيرها على الخرج لا يتعدى تأثير التعليقات عليه. يُستثنى من دلالات الأخطاء الشيفرة النائب Stubs، وهي عبارة عن مواضع مؤقتة للشيفرات المستقبلية، كالدوال والأصناف التي لم يتم تطبيقها بعد، فبدلًا من استخدام شيفرات حقيقية، يتضمن النائب عبارة مرور pass فقط، والتي لا تقوم بأي دور فعليًا (تسمى أيضًا العبارة عديمة الدور no operation أو no-op)، فالتعليمة pass موجودة لتستخدمها في الأماكن المفروض استخدام تعليمات فيها، في حين أنك لا ترغب بإضافتها الآن، لتنشئ بدلًا من ذلك شيفرة نائب مؤقتة، كما في المثال: >>> def exampleFunction(): ... pass … إن استدعاء الدالة السابقة لن يقوم بأي وظيفة، سوى الإشارة إلى أنه ستتم إضافة شيفرات له لاحقًا. وكبديل عن استدعاء تابع لا يقوم بأي دور، يمكنك جعله يعرض بالنيابة (عن دوره المتوقع المستقبلي) رسالة مفادها أن هذه الدالة ليست جاهزة للاستدعاء بعد باستخدام التعليمة raise NotImplementedError، بالشكل: >>> def exampleFunction(): ... raise NotImplementedError ... >>> exampleFunction() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in exampleFunction NotImplementedError ظهور الرسالة NotImplementedError سيمثل تنبيه لحالة استدعاء البرنامج لدالة أو تابع نائب عن طريق الصدفة. إذًا تعد كل من الشيفرات الملغاة بتحويلها إلى تعليقات والشيفرات الميتة من مؤشرات وقوع أخطاء، إذ أنها قد تضيع المبرمج بظنها جزء يجب تنفيذه من الشيفرة، واحذف بدلًا من ذلك هذه الشيفرات واستخدم نظام إدارة شيفرة مثل Git أو Subversion مما يتيح لك تتبع التغييرات دائمًا، فباستخدام هذه الأنظمة يمكنك حذف أي أجزاء تريد من الشيفرة وإعادتها لاحقًا بسهولة متى رغبت. التنقيح باستخدام دالة الطباعة التنقيح باستخدام دالة الطباعة هو الإجراء المتمثل باستدعاء الدالة ()print مؤقتًا ضمن البرنامج بغية عرض قيم المتغيرات قبل تشغيل البرنامج، وعادةً ما تمر هذه العملية بالخطوات التالية: ملاحظة وجود خطأ في البرنامج. إضافة بعض الاستدعاءات للدالة ()print لبعض المتغيرات في محاولة معرفة ما تحتويه. إعادة تشغيل البرنامج. إضافة المزيد من الاستدعاءات للدالة ()print لأن الاستدعاءات السابقة لم تعرض ما يكفي من معلومات. إعادة تشغيل البرنامج. تكرار الخطوتين السابقتين عدة مرات إلى حين اكتشاف موضع الخطأ. إعادة تشغيل البرنامج. إدراك أنك قد نسيت حذف بعضًا من استدعاءات الدالة ()print، فتحذف ما تبقى منها. إن التنقيح باستخدام دالة الطباعة مغرٍ ببساطته، إلا أنه يتطلب في الغالب إعادة تشغيل البرنامج مراتٍ ومرات قبل استعراض المعلومات التي تحتاجها فعلًا لإصلاح الخطأ، ويكون الحل البديل باستخدام منقح الأخطاء أو إنشاء مجموعة من الملفات السجل logfiles لبرنامجك، فباستخدام منقح الأخطاء يمكنك تنفيذ شيفرتك سطرًا تلو الآخر فاحصًا أي متغير تريد، وقد يبدو استخدام منقح الأخطاء هذا أبطأ من مجرد إضافة استدعاءات لدالة الطباعة ببساطة، إلا أنّه يوفّر عليك الوقت الضائع بالتشغيل المتكرر. في حين أن ملفات السجل قادرة على توفير كمية كبيرة من المعلومات حول برنامجك عبر مقارنة ملف سجل حالي بآخر سابق، وتوفر الوحدة logging في بايثون آلية سهلة لإنشاء ملفات السجل باستخدام شيفرة بسيطة مكونة من ثلاث سطور فقط: import logging logging.basicConfig(filename='log_filename.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') logging.debug('This is a log message.') فبعد استيراد وحدة السجل logging وضبط إعداداتها الرئيسية، يصبح من الممكن استدعاء التابع ()logging.debug لكتابة معلومات السجل ضمن مستند نصي، في حين أن الدالة ()print تعرض معلوماتها على شاشة الخرج. وعلى النقيض من التنقيح باستخدام دالة الطباعة، فإن استدعاء الدالة ()logging.debug يجعل من الواضح أي أجزاء من الخرج هي معلومات تنقيحية وأيها خرج البرنامج الفعلي. المتغيرات ذات اللاحقات الرقمية قد تحتاج أثناء كتابة البرامج إلى عدة متغيرات لتخزن النوع نفسه من البيانات، وفي هذه الحالات قد تميل لاستخدام الاسم نفسه مع إضافة لاحقة رقمية إلى الاسم، فعلى سبيل المثال في حال كنت تتعامل مع نموذج تسجيل دخول يطلب من مستخدميه إدخال كلمة مرورهم مرتين للتأكد من خلوها من الأخطاء، فقد تخزّن السلاسل النصية الممثلة لكلمتي المرور هاتين ضمن متغيرين بالأسماء password1 وpassword2. وإلى جانب أن هذه الأسماء لا تعبّر عن مضمون المتغيرات أو الاختلافات فيما بينها، فهي لا تشير أيضًا إلى عدد المتغيرات المشابهة الإجمالي، تاركةً القارئ في حيرة متسائلًا: أيوجد متغير أيضًا باسم password3 أو password4 ربما؟ فحاول دائمًا استخدام أسماء معبرة ومميزة للمتغيرات ولا تتكاسل بمجرد إضافة لاحقة رقمية لاسم المتغير السابق، ولعل أفضل تسمية للمتغيرين في مثالنا هذا هي password للأول وconfirm_password للثاني. لنأخذ مثالًا آخر، بفرض أنه لدينا تابع يتعامل مع إحداثيات بداية ونهاية، فقد تسمي المعاملات حينها x1 و y1 و x2 و y2 على التوالي، إلا أن هذه الأسماء لا تقدّم معلوماتٍ كما لو أسميتها start_x و start_y و end_x و end_y، ناهيك عن كون الاسمين start_x و start_y أكثر ترابطًا من x1 وy1 كتعبير عن تمثيلهما لإحداثيات نقطة البداية start. أما في حال تجاوز عدد المتغيرات ذات اللواحق الرقمية لاثنين، فعندها من المستحسن استخدام بنية معطيات كالقائمة list أو المجموعة set لتخزين هذه البيانات ضمن هيكلية، فمثلًا لو كان لدينا مجموعة من المتغيرات بالأسماء pet1Name و pet1Name وpet1Name وهكذا فمن الممكن تخزين قيمها ضمن قائمة واحدة باسم petNames. ومن الجدير بالملاحظة أن اللاحقة الرقمية لا تعد دلالة على الخطأ في أي متغير منتهٍ برقم، فمثلًا يعتبر الاسم enableIPV6 مثاليًا لمتغير، لأن الرقم 6 هو جزء من اسم البروتوكول "IPV6" ولا يمثّل لاحقة رقمية. أما في حال كنت ممن يستخدمون اللواحق الرقمية لتسمية سلسلة من المتغيرات، فمن المفضّل بدلًا من ذلك تخزين قيمها ضمن بنية معطيات ما، كقائمة أو قاموس أو غيرها. الأصناف التي يجب أن تكون مجرد دوال أو توابع اعتاد المبرمجون ممن يستخدمون لغات برمجة مثل جافا Java على إنشاء الأصناف بغية تنظيم شيفرات برامجهم، لنأخذ على سبيل المثال الصنف التالي المسمى Dice (بمعنى حجر النرد) والمتضمّن للتابع ()roll (والمقصود به رمي حجر النرد): >>> import random >>> class Dice: ... def __init__(self, sides=6): ... self.sides = sides ... def roll(self): ... return random.randint(1, self.sides) ... >>> d = Dice() >>> print('You rolled a', d.roll()) You rolled a 1 فقد تبدو الشيفرة السابقة بأنها شيفرة عالية التنظيم، ولكن ماذا لو فكرنا باحتياجاتنا الفعلية من هذه الشيفرة؟ أليست مجرد الحصول على رقم عشوائي محصور بين 1 و 6، وبالتالي من الممكن استبدال كامل الصنف السابق باستدعاء بسيط لتابع بالشكل: >>> print('You rolled a', random.randint(1, 6)) You rolled a 6 فبالمقارنة مع لغات البرمجة الأخرى، تستخدم لغة بايثون منهجية غير رسمية في تنظيم الشيفرات، لأن شيفراتها لا تتطلب تضمينها في صنف أو أي بنية متداولة أخرى، فإن كنت تُنشئ الكائنات بغية استدعاء تابع وحيد ضمنها، أو تُنشئ الأصناف لتحتوي فقط على توابع ساكنة غير مرتبطة بالكائنات، فكل هذه الإجراءات من دلالات الأخطاء التي تشير لأفضلية كتابة دوال اعتيادية بدلًا منها. إذ أننا نستخدم في بايثون الوحدات لتجميع الدوال مع بعضها البعض بدلًا من استخدام الأصناف، ذلك لأن الأصناف بحد ذاتها يجب أن تتواجد ضمن وحدات بطبيعة الحال، ما يجعل تضمين الدوال في أصناف مجرد إضافة غير ضرورية لمستوى تنظيمي جديد لشيفرتك، وتناقش الفصول من 15 حتى 17 في كتابنا هذا مبادئ التصميم كائني التوجّه تفصيليًا، كما تحدث Jack Diederich’s في خطابه ضمن مؤتمر بايثون لعام 2012 تحت عنوان Stop Writing Classes "كفوا عن كتابة الأصناف" حول العديد من الطرق التي يستخدمها المبرمجون، مضيفين بذلك تعقيدًا غير ضروريًا لشيفراتهم المكتوبة في بايثون. بنى اشتمالات القوائم المتداخلة يعد اشتمال القوائم List comprehensions طريقة مختصرة لإنشاء قائمة ذات قيم معقدة، فمثلًا لإنشاء قائمة سلاسل محرفية متضمنةً الأرقام من 0 حتى 100 باستثناء مضاعفات العدد 5، فعادةً ما ستستخدم حلقة for بالشكل: >>> spam = [] >>> for number in range(100): ... if number % 5 != 0: ... spam.append(str(number)) ... >>> spam ['1', '2', '3', '4', '6', '7', '8', '9', '11', '12', '13', '14', '16', '17', --snip-- '86', '87', '88', '89', '91', '92', '93', '94', '96', '97', '98', '99'] وبدلًا من ذلك، يمكنك الحصول على نفس النتيجة مُستخدمًا سطرًا برمجيًا واحدًا بالاعتماد على صيغة اشتمال القائمة: >>> spam = [str(number) for number in range(100) if number % 5 != 0] >>> spam ['1', '2', '3', '4', '6', '7', '8', '9', '11', '12', '13', '14', '16', '17', --snip-- '86', '87', '88', '89', '91', '92', '93', '94', '96', '97', '98', '99'] كما تمتلك لغة بايثون صيغًا لاشتمال كل من القواميس والمجموعات: 1 >>> spam = {str(number) for number in range(100) if number % 5 != 0} >>> spam {'39', '31', '96', '76', '91', '11', '71', '24', '2', '1', '22', '14', '62', --snip-- '4', '57', '49', '51', '9', '63', '78', '93', '6', '86', '92', '64', '37'} 2 >>> spam = {str(number): number for number in range(100) if number % 5 != 0} >>> spam {'1': 1, '2': 2, '3': 3, '4': 4, '6': 6, '7': 7, '8': 8, '9': 9, '11': 11, --snip-- '92': 92, '93': 93, '94': 94, '96': 96, '97': 97, '98': 98, '99': 99} أنشأنا في السطر ذو الرقم 1 بنية اشتمال مجموعة باستخدام الأقواس المعقوصة بدلًا من المعقوفة لتُنتج مجموعة من القيم، أما في السطر ذو الرقم 2 فأنشأنا بنية اشتمال قاموس باستخدام محرف النقطتين الرأسيتين لفصل المفتاح عن القيم ضمن الاشتمال. وبذلك نجد أن بنى الاشتمال مختصرة وقادرة على زيادة مقروئية شيفراتك، ومن الجدير بالملاحظة أن الاشتمالات تولّد قائمة أو مجموعة أو قاموس اعتمادًا على كائن تكراري (ففي مثالنا السابق حصلنا على القيمة المعادة من الكائن range من خلال الاستدعاء (range(100)، فالقوائم والمجموعات والقواميس بطبيعتها كائنات تكرارية، ما يعني إمكانية استخدام اشتمالات متداخلة كما في المثال التالي: >>> nestedIntList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]] >>> nestedStrList = [[str(i) for i in sublist] for sublist in nestedIntList] >>> nestedStrList [['0', '1', '2', '3'], ['4'], ['5', '6'], ['7', '8', '9']] إلا أن بنى اشتمالات القوائم المتداخلة (أو اشتمالات المجموعات أو القواميس المتداخلة) تنطوي على تعقيدية عالية ضمن جزء صغير من الشيفرة، جاعلةً من الشيفرة أصعب للقراءة، لذا من المفضّل توسيع اشتمال القائمة التالي واستبداله بحلقة for أو اثنتين: >>> nestedIntList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]] >>> nestedStrList = [] >>> for sublist in nestedIntList: ... nestedStrList.append([str(i) for i in sublist]) ... >>> nestedStrList [['0', '1', '2', '3'], ['4'], ['5', '6'], ['7', '8', '9']] كما من الممكن أن تتضمن الاشتمالات على عدة تعابير for، رغم أن هذا الإجراء قد ينطوي أيضًا على تقليل مقروئية الشيفرة، فمثلًا الاشتمال التالي يعطي بالنتيجة قائمةً واحدة انطلاقًا من مجموعة قوائم متداخلة: >>> nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]] >>> flatList = [num for sublist in nestedList for num in sublist] >>> flatList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] إذ يتضمن الاشتمال السابق تعبيري for، ما يجعله صعبًا للفهم حتى على أمهر مطوري بايثون، في حين أن توسعة الاشتمال السابق باستبداله بحلقتي for سيعطي نفس الناتج ولكن بطريقة أسهل وأوضح للقراءة والفهم: >>> nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]] >>> flatList = [] >>> for sublist in nestedList: ... for num in sublist: ... flatList.append(num) ... >>> flatList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] إذًا تعد الاشتمالات كاختصارات قادرة على اختصار الشيفرات شريطة عدم المبالغة والذهاب نحو خيار الاشتمالات المتداخلة صعبة القراءة والفهم. كتل الاستثناءات except الفارغة ورسائل الأخطاء الضعيفة لعل التقاط الاستثناءات واحدة من الطرق الرئيسية لضمان استمرار عمل برامجك حتى في حال ظهور المشاكل، فبمجرد ظهور استثناء دون وجود كتلة استثناء (القسم except فارغ) للتعامل معه سيتوقف برنامج بايثون عن العمل مباشرةً، ما قد يسبب خسارتك لعملك غير المحفوظ أو ترك الملفات دون اكتمالها. ومن الممكن تجنّب هذه الأعطال بتزويد شيفرتك بكتلة استثناء متضمنةً شيفرة للتعامل مع الخطأ، ولكن قد يكون من الصعب اتخاذ القرار الأنسب حول كيفية التعامل مع الخطأ، ما يجعل المبرمجين يميلون لترك كتلة الاستثناء فارغة باستخدام تعليمة pass ضمنها، ففي المثال التالي استخدمنا التعليمة pass لإنشاء كتلة استثناء لا تقوم فعليًا بأي عمل: >>> try: ... num = input('Enter a number: ') ... num = int(num) ... except ValueError: ... pass ... Enter a number: forty two >>> num 'forty two' لن تتوقف الشيفرة السابقة عن العمل في حال تمرير القيمة النصية 'forty two' إلى الدالة ()int، ذلك لأنه قد تم التعامل مع خطأ القيمة ValueError الذي تنتجه الدالة ()int في حالة تمرير نص بدلًا من رقم إليها من خلال عبارة الاستثناء، إلا أن عدم القيام بأي إجراء حيال الخطأ والاكتفاء بالهروب منه باستخدام استثناء فارغ قد يكون أسوأ من توقف البرنامج نتيجة هذا الخطأ، إذ أن البرامج بتوقفها تتفادى إكمال التنفيذ ببيانات خاطئة أو بنى غير مكتملة، التي قد تؤدي لأخطاء أكبر لاحقًا. على سبيل المثال، برنامجنا السابق لا يتوقف عن العمل حتى في حال تمرير محارف غير عددية إلى الدالة ()int، وبذلك يصبح المتغير num متضمنًا لسلسلة نصية بدلًا من رقم صحيح، ما قد يؤدي لوقوع أخطاء في أي مكان يُستخدم فيه هذا المتغير، فنستنتج أن عبارة الاستثناء هذه لا تقوم بما هو أكثر من إخفاء الخطأ بدلًا من مواجهته والتعامل معه. كما أن التعامل مع الاستثناءات برسائل أخطاء ضعيفة هو أيضًا من دلالات الوقوع في الأخطاء، كما في المثال التالي: >>> try: ... num = input('Enter a number: ') ... num = int(num) ... except ValueError: ... print('An incorrect value was passed to int()') ... Enter a number: forty two An incorrect value was passed to int() إن الشيفرة السابقة لن تتوقف عن العمل، وهو أمر جيد، إلا أنها بالمقابل لا تعطي المستخدم معلوماتٍ كافية لإرشاده حول كيفية إصلاح الخطأ، فيجب ألا ننسى أن رسائل الخطأ موجهة للمستخدمين وليست للمبرمجين، ففي رسالة الخطأ السابقة ناهيك كونها تتضمن معلومات تقنية لن يفهمها المستخدم العادي، كالإشارة إلى الدالة ()int، إلا أنها لا تخبر المُستخدم بما عليه فعله لإصلاح الخطأ، فالأصل أن تشرح رسالة الخطأ ما حدث مُرشدةً المستخدم لكيفية التعامل معه. قد يكون من الأسهل على المبرمج كتابة رسالة خطأ سريعة بتوصيف غير مفيد حيال ما حدث، بدلًا من كتابة الخطوات التفصيلية التي من شأنها مساعدة المستخدم على إصلاح المشكلة. ولكن تذكر دائمًا أنه لا يمكن اعتبار شيفرتك مكتملة ما لم تأخذ في الحسبان كل الاستثناءات الممكنة وكيفية التعامل معها. الخاتمة تشير رائحة الشيفرة أو ما أسميناه دلالات الأخطاء على وجود طريقة أفضل لكتابة شيفراتك، وهي لا تعني بالضرورة وجوب إجراء تغييرات، لكنها ترشدك لإلقاء نظرة ثانية تأكيدية. ولعل أشهر دلالات الأخطاء هي الشيفرات المكررة، والتي تشير إلى ضرورة تجنب التكرار عبر حصر الكود المكرر ضمن دالة أو حلقة، وبذلك نضمن بأن أي تغييرات مستقبلية ستجرى لمرة واحدة فقط في مكانٍ واحد. المصدر الثاني لدلالات الأخطاء هو الأرقام السحرية والتي تعرف بأنها أرقام تبدو عشوائية أو ليست ذات معنى ضمن الشيفرة والتي يمكن استبدالها باستخدام ثوابت بأسماء معبرة. كذلك الأمر بالنسبة للشيفرات الملغاة بجعلها تعليقات والشيفرات الميتة التي لن تُنفذ أبدًا من قبل الحاسوب، إلا أنها قد تشكل مصدر إرباك للمبرمج القارئ للشيفرة، فالأفضل حذف هذا النوع من الشيفرات والاعتماد على نظام إدارة شيفرات مثل Git في حال الرغبة بإعادة أي منها إلى الشيفرة مستقبلًا. أما تنقيح الأخطاء المعتمد على دالة الطباعة فيعني استدعاء الدالة ()print بغية عرض معلومات تنقيحية، ورغم سهولة هذه الطريقة، إلا أنه من الأسرع الاعتماد على منقح الأخطاء وملفات السجلات في تشخيص الأخطاء. ولعل الإجراء الأفضل للتعامل مع المتغيرات ذات اللواحق الرقمية مثل x1 و x2 و x3 وهكذا، هو تضمين قيمها في متغير واحد ضمن قائمة. وعلى خلاف لغات البرمجة الأخرى مثل لغة جافا، فإننا في لغة بايثون نستخدم الوحدات بدلًا من الأصناف في تجميع الدوال، فالصنف المتضمن لتابع وحيد أو لتوابع ساكنة هو أحد دلالات الأخطاء، إذ من المحبذ تضمين هذه الشيفرة في وحدة بدلًا من الصنف. أما بالنسبة لبنى اشتمال القوائم ورغم كونها طريقة مختصرة في إنشاء قوائم القيم، إلا أن تداخل هذه البنى يجعلها غير مقروءة. بالإضافة لكون أي استثناء يتم التعامل معه بكتلة استثناء فارغة هو من دلالات الأخطاء، حيث أنك ببساطة تخفي الخطأ بدلًا من التعامل معه، ولا تنسى ظان عدم وجود رسالة خطأ أفضل للمستخدم من رسالة قصيرة مبهمة. مما لا شك فيه أنك لن تطبيق كامل النصائح البرمجية الموصوفة كدلالات على وقوع الأخطاء الواردة في هذا المقال، وتذكر دائمًا بأنه لا يوجد مقياس موضوعي لأفضل الممارسات، فمع اكتسابك للمزيد من الخبرة، ستخرج باستنتاجات عديدة حول ما يجعل شيفرتك أكثر مقروئية وموثوقية، وهذا لا يلغي أن التوصيات الواردة في هذا المقال تغطي مسائل بالغة الأهمية يجب مراعاتها. ترجمة -وبتصرف- للجزء الأول من الفصل الخامس Finding Code Smells من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigarti. اقرأ أيضًا المقال السابق: اختيار أسماء برمجية مفهومة في بايثون حل المشكلات وأهميتها في احتراف البرمجة
-
يُعد فلاسك إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا عدة أدوات وميزات من شأنها إنشاء تطبيقات ويب في لغة بايثون. أمّا SQLAlchemy فهي أداة في محرك قواعد البيانات SQL تؤمن وصولًا فعالًا وعالي الأداء إلى قواعد البيانات العلاقية، كما توفّر طرقًا للتخاطب مع العديد من محركات قواعد البيانات، مثل SQLite و MySQL و PostgreSQ، مانحةً إمكانية الوصول إلى آليات SQL الخاصة بقواعد البيانات، كما توفّر رابط الكائن بالعلاقات Object Relational Mapper -أو اختصارًا ORM- الذي يتيح إمكانية إنشاء الاستعلامات والتعامل مع البيانات باستخدام توابع وكائنات بسيطة في بايثون. تعدّ Flask-SQLAlchemy إضافةً لفلاسك تسهّل استخدام SQLAlchemy ضمنها وتؤمّن الأدوات والوسائل المناسبة للتعامل مع قاعدة البيانات في تطبيقات فلاسك من خلال SQLAlchemy. سنستخدم في هذا المقال كلًا من إطار العمل فلاسك والإضافة Flask-SQLAlchemy لإنشاء نظام إدارة موظفين مع قاعدة بيانات تضم جدولًا خاصًّا بكل موظف، ليتضمّن هذا الجدول المعرّف الفريد ID للموظف واسمه الأوّل ونسبته وبريده الإلكتروني الفريد، إضافةً إلى عددٍ صحيح يدل على عمره وقيمة تاريخ تشير إلى تاريخ انضمامه إلى الشركة (تاريخ تعيّينه) وقيمةٍ منطقية تُحدّد ما إذا كان هذا الموظف متواجد حاليًا في مكتبه أم لا. إذ سنستخدم صَدَفَة فلاسك للاستعلام عن جدول ما والحصول على سجلاتٍ معيّنة وفقًا لقيمة عمود منه (مثل البريد الإلكتروني)، أي سنجلب سجلات الموظفين المُحقّقة لشروط مُحدّدة، كأن نجلب فقط الموظفين المتواجدين حاليًا أو أن نُنشئ قائمةً بالموظفين غير المتواجدين، كما سنرتّب النتائج وفقًا لقيم أحد الأعمدة، بما يتضمّن تحديد نتائج الاستعلام وحساب عددها، وسنستخدم نهايةً الترقيم Pagination بغية عرض عدد مُحدّد من الموظفين في الصفحة الواحدة ضمن تطبيق الويب. مستلزمات العمل قبل المتابعة في هذا المقال لا بدّ من: توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو "flask_app". الفهم الجيد لأساسيات فلاسك، مثل مفهوم الوجهات ودوال العرض، ويمكنك في هذا الصدد الاطلاع على المقالين كيفية بناء موقعك الإلكتروني الأول باستخدام إطار عمل فلاسك Flask من لغة بايثون وكيفية استخدام القوالب في تطبيقات فلاسك Flask لفهم مبادئ فلاسك. فهم أساسيات لغة HTML. فهم أساسيات الإضافة Flask-SQLAlchemy، مثل إعداد قاعدة البيانات وإنشاء نماذجها وإدخال البيانات إليها، وننصحك في هذا الصدد بقراءة المقال استخدام الإضافة Flask-SQLAlchemy للتخاطب مع قواعد البيانات في تطبيقات فلاسك لفهم مبادئ هذه الإضافة. الخطوة 1: إعداد قاعدة البيانات والنموذج سنثبّت في هذه الخطوة كافّة الحزم اللازمة، كما سنعدّ كلًا من تطبيق فلاسك وقاعدة البيانات Flask-SQLAlchemy ونموذج الموظف المُمثّل للجدول employee، حيث سنخزّن بيانات الموظف، كما سنضيف وجهة وصفحة، حيث سنعرض كافّة الموظفين ضمن الصفحة الرئيسية للتطبيق. لذا، وبعد التأكّد من كون البيئة الافتراضية مُفعّلة، سنستخدم أمر تثبيت الحزم pip لتثبيت كل من فلاسك والإضافة Flask-SQLAlchemy على النحو التالي: (env)user@localhost:$ pip install Flask Flask-SQLAlchemy بمجرّد انتهاء التثبيت بنجاح، سيظهر في السطر الأخير من الخرج ما يشبه التالي: Successfully installed Flask-2.1.2 Flask-SQLAlchemy-2.5.1 Jinja2-3.1.2 MarkupSafe-2.1.1 SQLAlchemy-1.4.37 Werkzeug-2.1.2 click-8.1.3 greenlet-1.1.2 itsdangerous-2.1.2 الآن وبعد تثبيت الحزم اللازمة، سننشئ ملفًا جديدًا باسم "app.py" ضمن المجلد "flask.app" والذي سيحتوي على الشيفرات الخاصة بإعداد قاعدة البيانات ووجهات فلاسك اللازمة لعمل التطبيق: (env)user@localhost:$ nano app.py ونكتب ضمنه الشيفرة التالية، التي ستُعِدّ قاعدة بياناتٍ من نوع SQLite ونموذج قاعدة بيانات للموظف مُمثّلًا للجدول employee الذي سنستخدمه لتخزين بيانات كل موظف: import os from flask import Flask, render_template, request, url_for, redirect from flask_sqlalchemy import SQLAlchemy basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] =\ 'sqlite:///' + os.path.join(basedir, 'database.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) class Employee(db.Model): id = db.Column(db.Integer, primary_key=True) firstname = db.Column(db.String(100), nullable=False) lastname = db.Column(db.String(100), nullable=False) email = db.Column(db.String(100), unique=True, nullable=False) age = db.Column(db.Integer, nullable=False) hire_date = db.Column(db.Date, nullable=False) active = db.Column(db.Boolean, nullable=False) def __repr__(self): return f'<Employee {self.firstname} {self.lastname}>' نحفظ الملف ونغلقه. استوردنا في الشيفرة السابقة الوحدة os التي تُمكنّنا من الوصول إلى واجهات نظام التشغيل المختلفة، والتي سنستخدمها في بناء مسار ملف قاعدة البيانات database.db، كما استوردنا من حزمة فلاسك كافّة المُساعدات اللازمة للتطبيق، مثل الصنف فلاسك Flask المُستخدم في إنشاء النسخة الفعلية من التطبيق، والدالة ()render_template لتصيير قوالب HTML، والكائن request المسؤول عن التعامل مع الطلبات، والدالة ()url_for لبناء روابط الوجهات، والدالة ()redirect لإعادة توجيه المُستخدمين من صفحة لأُخرى. استوردنا بعد ذلك الصنف SQLAlchemy من الإضافة Flask-SQLAlchemy، والذي يسمح لنا بالوصول إلى جميع الدوال والأصناف الخاصة بالأداة SQLAlchemy، إضافةً إلى المُساعدات والآليات التي تدعم عمل فلاسك مع هذه الأداة، والتي سنستخدمها لإنشاء كائن قاعدة بيانات يتصل مع تطبيق فلاسك. بهدف إنشاء مسار لملف قاعدة البيانات، حدّدنا المجلد الحالي ليكون المجلد الرئيسي، كما استخدمنا الدالة ()os.path.abspath للحصول على المسار المطلق لمجلد الملف الحالي، إذ يتضمّن المتغير الخاص __file__ اسم مسار الملف الحالي app.py، وخزنّا المسار المطلق للمجلد الأساسي ضمن المتغير basedir. بعد ذلك، أنشأنا نسخةً فعليةً من تطبيق فلاسك باسم app والتي سنستخدمها لضبط مفتاحي إعدادات خاصّين بالإضافة Flask-SQLAlchemy، هما: SQLALCHEMY_DATABASE_URI: وهو معرّف الموارد الموحّد الخاص بقاعدة البيانات database URI، وهو مسؤول عن تحديد قاعدة البيانات المراد إنشاء اتصال معها، وفي هذه الحالة تكون صيغة هذا المعرّف على النحو sqlite://path/to/database.db، إذ تُستخدم الدالة ()op.path.join لتحقيق الربط المدروس ما بين المجلد الأساسي الذي أنشاناه وخزنّا مساره في المتغير basedir، وبين اسم الملف database.db، الأمر الذي يسمح بالاتصال مع ملف قاعدة البيانات "database.db" الموجود في المجلد "flask.app"، إذ سيُنشَئ هذا الملف فور تهيئة قاعدة البيانات. SQLALCHEMY_TRACK_MODIFICATIONS: وهو إعداد لتمكين أو إلغاء تمكين ميزة تتبع تغيرات الكائنات، وقد عيّناه إلى القيمة false لإلغاء التتبع وبالتالي تحقيق استخدام أقل لموارد الذاكرة. بعد الانتهاء من ضبط إعدادات الأداة SQLAIchemy من خلال تعيين معرّف الموارد الموحّد لقاعدة البيانات وإلغاء تمكين ميزة تتبّع التغييرات، أنشأنا كائن قاعدة بيانات باستخدام صنف الأداة SQLAIchemy، ممررين إليه نسخة التطبيق بغية ربط تطبيق فلاسك مع الأداة SQLAIchemy، مخزنين كائن قاعدة البيانات هذا ضمن متغير باسم db والذي سنستخدمه في التخاطب مع قاعدة البيانات. وبعد إعداد كل من نسخة التطبيق الفعلية وكائن قاعدة البيانات، أنشأنا نموذج قاعدة بيانات باسم Employee الوارث للصنف db.Model، إذ يُمثّل هذا النموذج جدول الموظفين employee المُتضمّن للأعمدة التالية: id: وهو معرّف الموظف، ويحتوي على بياناتٍ من نوع رقم صحيح، ويمثّل المفتاح الأساسي. firstname: الاسم الأول للموظف، وهو سلسلة نصية بعدد محارف أعظمي يساوي 100، وتشير التعليمة nullable=False إلى أن هذا العمود لا يجب أن يحتوي على قيمٍ فارغة. lastname: الاسم الأخير للموظف، وهو سلسلة نصية بعدد محارف أعظمي يساوي 100، وتشير التعليمة nullable=False إلى أن هذا العمود لا يجب أن يحتوي على قيمٍ فارغة. email: عنوان البريد الإلكتروني للموظف، وهو سلسلة نصية بعدد محارف أعظمي يساوي 100، وتشير التعليمة unique=True إلى أنّ البريد الإلكتروني يجب أن يكون فريدًا لكل موظف، كما تشير التعليمة nullable=False إلى أن هذا العمود لا يجب أن يحتوي على قيمٍ فارغة. age: عمر الموظف، ويحتوي على بياناتٍ من نوع رقم صحيح. hire_date: يحتوي على تاريخ تعيين الموظف، وقد عيّنا نمط العمود ليكون db.date بغية التصريح عن كون هذا العمود يحتوي على تواريخ. active: يتضمّن قيمة منطقية تدل على ما إذا كان الموظف متواجد حاليًا في مكتبه أم لا. تمكّننا الدالة الخاصة __repr__ من تمثيل كل كائن بصيغة سلسلة نصية، الأمر الذي يساعد في التعرّف على هذا الكائن لأغراض التنقيح، واستخدمنا في حالتنا الاسم الأوّل والأخير للموظف لتمثيل كائنات الموظفين. الآن وبعد الانتهاء من إعداد كل من الاتصال مع قاعدة البيانات ونموذج الموظف، سنكتب برنامجًا في بايثون لإنشاء قاعدة البيانات والجدول employee وملء هذا الجدول ببعضٍ من بيانات الموظفين. لذا، سننشئ ملفًا جديدًا باسم "init_db.py" ضمن المجلد "flask_app": (env)user@localhost:$ nano init_db.py ونكتب ضمنه الشيفرة التالية بغية حذف أي جداول حالية في قاعدة البيانات والبدء بقاعدة بيانات فارغة، كما سننشئ الجدول employee وسنملؤه ببيانات تسعة موظفين: from datetime import date from app import db, Employee db.drop_all() db.create_all() e1 = Employee(firstname='John', lastname='Doe', email='jd@example.com', age=32, hire_date=date(2012, 3, 3), active=True ) e2 = Employee(firstname='Mary', lastname='Doe', email='md@example.com', age=38, hire_date=date(2016, 6, 7), active=True ) e3 = Employee(firstname='Jane', lastname='Tanaka', email='jt@example.com', age=32, hire_date=date(2015, 9, 12), active=False ) e4 = Employee(firstname='Alex', lastname='Brown', email='ab@example.com', age=29, hire_date=date(2019, 1, 3), active=True ) e5 = Employee(firstname='James', lastname='White', email='jw@example.com', age=24, hire_date=date(2021, 2, 4), active=True ) e6 = Employee(firstname='Harold', lastname='Ishida', email='hi@example.com', age=52, hire_date=date(2002, 3, 6), active=False ) e7 = Employee(firstname='Scarlett', lastname='Winter', email='sw@example.com', age=22, hire_date=date(2021, 4, 7), active=True ) e8 = Employee(firstname='Emily', lastname='Vill', email='ev@example.com', age=27, hire_date=date(2019, 6, 9), active=True ) e9 = Employee(firstname='Mary', lastname='Park', email='mp@example.com', age=30, hire_date=date(2021, 8, 11), active=True ) db.session.add_all([e1, e2, e3, e4, e5, e6, e7, e8, e9]) db.session.commit() استوردنا في الشيفرة السابقة الصنف ()date من الوحدة datetime لاستخدامه في ضبط تواريخ تعيّين الموظفين. استوردنا أيضًا كلًا من كائن قاعدة البيانات والنموذج Employee، واستدعينا الدالة ()db.drop_all بغية حذف كافّة الجداول الموجودة أصلًا في قاعدة البيانات متجنبين بذلك فرصة وجود جدول مملوء باسم "employee" سابقًا فيها، الأمر الذي قد يسبب أخطاءً. من الجدير بالملاحظة أنّ البرنامج "init_db.py" سيحذف كامل محتويات قاعدة البيانات في كل مرة نشغّله فيها، وللمزيد حول كيفية إنشاء وتعديل وحذف جداول قاعدة البيانات، ننصحك بقراءة المقال استخدام الإضافة Flask-SQLAlchemy للتخاطب مع قواعد البيانات في تطبيقات فلاسك. استنسخنا بعد ذلك النموذج Emolyee عدّة مرات بغية تمثيل كافّة الموظفين الذين سنستعلم عنهم في هذا المقال، ثم أضفنا هذه النسخ إلى قاعدة البيانات باستخدام الدالة ()db.session.add_all، ونهايةً استخدمنا التابع ()db.session.commit لتأكيد العمليات وتطبيق التغييرات على قاعدة البيانات. نحفظ الملف ونغلقه. سننفذ الآن البرنامج "init_db.py": (env)user@localhost:$ python init_db.py للاطلاع على البيانات المُضافة إلى قاعدة البيانات، نفتح صَدفة فلاسك بعد التأكّد من كون البيئة الافتراضية مُفعّلة بغية الاستعلام عن كافّة الموظفين وعرض بيانات كل منهم، على النحو التالي: (env)user@localhost:$ flask shell ثمّ نشغّل الشيفرة التالية المسؤولة عن الاستعلام عن كافّة الموظفين وعرض بياناتهم: >>> from app import db, Employee >>> >>> >>> employees = Employee.query.all() >>> >>> for employee in employees: >>> print(employee.firstname, employee.lastname) >>> print('Email:', employee.email) >>> print('Age:', employee.age) >>> print('Hired:', employee.hire_date) >>> if employee.active: >>> print('Active') >>> else: >>> print('Out of Office') >>> print('----') استخدمنا في الشيفرة السابقة التابع ()all من السمة query للحصول على كافّة الموظفين، واستخدمنا حلقةً تكراريةً للمرور على كافّة نتائج الاستعلام؛ أمّا بالنسبة للعمود "active"، فقد استخدمنا جملةً شرطيةً لإظهار الحالة الراهنة للموظف، سواءٌ كان متواجدًا "Active" أم خارج مكتبه "Out of Office". نحصل على خرجٍ على النحو التالي: John Doe Email: jd@example.com Age: 32 Hired: 2012-03-03 Active ---- Mary Doe Email: md@example.com Age: 38 Hired: 2016-06-07 Active ---- Jane Tanaka Email: jt@example.com Age: 32 Hired: 2015-09-12 Out of Office ---- Alex Brown Email: ab@example.com Age: 29 Hired: 2019-01-03 Active ---- James White Email: jw@example.com Age: 24 Hired: 2021-02-04 Active ---- Harold Ishida Email: hi@example.com Age: 52 Hired: 2002-03-06 Out of Office ---- Scarlett Winter Email: sw@example.com Age: 22 Hired: 2021-04-07 Active ---- Emily Vill Email: ev@example.com Age: 27 Hired: 2019-06-09 Active ---- Mary Park Email: mp@example.com Age: 30 Hired: 2021-08-11 Active ---- وبذلك نجد أنه قد جرى عرض كافة الموظفين المُضافين إلى قاعدة البيانات على نحوٍ سليم. نُغلق الآن صَدَفة فلاسك: >>> exit() أمّا الآن، فسننشئ وجهة فلاسك المسؤولة عن عرض الموظفين، لذا سنفتح الملف app.py لتحريره: (env)user@localhost:$ nano app.py ونكتب الوجهة التالية في نهايته: ... @app.route('/') def index(): employees = Employee.query.all() return render_template('index.html', employees=employees) نحفظ الملف ونغلقه. ستستعلم الوجهة السابقة عن كافّة الموظفين، وتُصيّر قالب HTML للصفحة الرئيسية للتطبيق باسم "index.html" مُمررةً إليه نتائج الاستعلام. ننشئ الآن مجلدًا للقوالب باسم "templates" وقالبًا رئيسيًا باسم "base.html": (env)user@localhost:$ mkdir templates (env)user@localhost:$ nano templates/base.html ونكتب التالي ضمن القالب الرئيسي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %} {% endblock %} - FlaskApp</title> <style> .title { margin: 5px; } .content { margin: 5px; width: 100%; display: flex; flex-direction: row; flex-wrap: wrap; } .employee { flex: 20%; padding: 10px; margin: 5px; background-color: #f3f3f3; inline-size: 100%; } .name { color: #00a36f; text-decoration: none; } nav a { color: #d64161; font-size: 3em; margin-left: 50px; text-decoration: none; } .pagination { margin: 0 auto; } .pagination span { font-size: 2em; margin-right: 10px; } .page-number { color: #d64161; padding: 5px; text-decoration: none; } .current-page-number { color: #666 } </style> </head> <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% block content %} {% endblock %} </div> </body> </html> نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة كتلة عنوان، كما أضفنا بعضًا من تنسيقات CSS لتنسيق مظهر المكونات. أضفنا أيضًا شريط تصفّح بعنصرين، الأوّل للصفحة الرئيسية للتطبيق، والثاني لصفحة عرض معلومات حول التطبيق إلا أننا سنتركها غير فعالة حاليًا، إذ سيُستخدم شريط التصفّح هذا في التطبيق في كافّة القوالب الوارثة للقالب الرئيسي، كما ستُستبدل كتلة المحتوى من القالب الرئيسي بالمحتوى الموافق لكل صفحة موظف. نفتح الآن قالب الصفحة الرئيسية "index.html" الذي صيّرناه في الملف "app.py": (env)user@localhost:$ nano templates/index.html ونكتب ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1 class="title">{% block title %} Employees {% endblock %}</h1> <div class="content"> {% for employee in employees %} <div class="employee"> <p><b>#{{ employee.id }}</b></p> <b> <p class="name">{{ employee.firstname }} {{ employee.lastname }}</p> </b> <p>{{ employee.email }}</p> <p>{{ employee.age }} years old.</p> <p>Hired: {{ employee.hire_date }}</p> {% if employee.active %} <p><i>(Active)</i></p> {% else %} <p><i>(Out of Office)</i></p> {% endif %} </div> {% endfor %} </div> {% endblock %} مررنا في الشيفرة السابقة على كافّة الموظفين باستخدام حلقةٍ تكرارية، لنعرض معلومات كل منهم، إذ أضفنا العنوان التوضيحي "Active" في حال كون الموظف متواجد حاليًا، وفيما عدا ذلك يُعرَض العنوان "Out of Office" دلالةً على عدم تواجده. نحفظ الملف ونغلقه. الآن ومع وجودنا ضمن المجلد "flask_app" ومع كون البيئة الافتراضية مُفعّلة، سنُعلم فلاسك بالتطبيق المراد تشغيله (وهو في حالتنا الملف "app.py") باستخدام متغير البيئة FLASK_APP، في حين يحدّد متغير البيئة FLASK_ENV وضع التشغيل وهنا قد اخترنا وضع development ما يعني أنّ التطبيق سيعمل في وضع التطوير مع تشغيل مُنقّح الأخطاء، وللمزيد من المعلومات حول مُنقّح الأخطاء في فلاسك ننصحك بقراءة المقال كيفية التعامل مع الأخطاء في تطبيقات فلاسك، ولتنفيذ ما سبق سنشغّل الأوامر التالية: (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development والآن سنشغّل التطبيق باستخدام الأمر flask run: (env)user@localhost:$ flask run وبعد التأكد من كون خادم التطوير ما يزال قيد التشغيل، نذهب إلى الرابط التالي باستخدام المتصفح: http://127.0.0.1:5000/ فيظهر الموظفون المضافون إلى قاعدة البيانات في صفحة مشابهة للصورة التالية: أما الآن، نترك الخادم قيد التشغيل، ونفتح نافذة طرفية جديدة استعدادًا للخطوة التالية، ومع نهاية هذه الخطوة نكون قد عرضنا الموظفين المدخلين في قاعدة البيانات على الصفحة الرئيسية للتطبيق، وسنستخدم في الخطوة التالية صَدَفة فلاسك للاستعلام عن الموظفين باستخدام توابع مختلفة. الخطوة 2: الاستعلام عن السجلات سنستخدم في هذه الخطوة صَدَفة فلاسك للاستعلام عن السجلات، لنجلب النتائج ونرشّحها باستخدام توابع مختلفة تحت شروط مختلفة. سنضبط بدايةً متغيرات البيئة FLASK_APP و FLASK_ENV أثناء كون البيئة البرمجية مُفعّلة، ثم نفتح صَدَفة فلاسك: (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development (env)user@localhost:$ flask shell ثم نستورد كلًا من الكائن db والنموذج Employee: >>> from app import db, Employee استرجاع كافة السجلات كما رأينا في الخطة السابقة، من الممكن استخدام التابع ()all من السمة query للحصول على كافّة سجلات الجدول، على النحو التالي: >>> all_employees = Employee.query.all() >>> print(all_employees) وعندها سيكون الخرج قائمةً بالكائنات المُمثلّة لكل الموظفين: [<Employee John Doe>, <Employee Mary Doe>, <Employee Jane Tanaka>, <Employee Alex Brown>, <Employee James White>, <Employee Harold Ishida>, <Employee Scarlett Winter>, <Employee Emily Vill>, <Employee Mary Park>] استرجاع السجل الأول من المُمكن استخدام التابع ()first للحصول على السجل الأوّل بصورةٍ مشابهة لما سبق، على النحو التالي: >>> first_employee = Employee.query.first() >>> print(first_employee) وعندها سيكون الخرج هو الكائن المُتضمّن لبيانات الموظف الأوّل: <Employee John Doe> استرجاع سجل وفق معرفه تُعرَّفُ السجلات في معظم جداول قواعد البيانات باستخدام معرّفٍ فريد ID، وتتيح الإضافة Flask-SQLAlchemy جلبَ سجلٍ ما وفقًا لمعرّفه باستخدام التابع ()get على النحو التالي: >>> employee5 = Employee.query.get(5) >>> employee3 = Employee.query.get(3) >>> print(f'{employee5} | ID: {employee5.id}') >>> print(f'{employee3} | ID: {employee3.id}') ويكون الخرج بالشكل: <Employee James White> | ID: 5 <Employee Jane Tanaka> | ID: 3 استرجاع سجل أو عدة سجلات وفقا لقيمة عمود من الممكن استخدام تابع الترشيح ()filter_by بغية الحصول على سجل محدّد وفقًا لقيمة أحد أعمدته، فمثلًا لجلب سجل ما وفقًا لقيمة معرفّه بما يشبه مبدأ عمل التابع ()get، نكتب: >>> employee = Employee.query.filter_by(id=1).first() >>> print(employee) ويكون الخرج بالشكل: <Employee John Doe> استخدمنا التابع ()first في الشيفرة السابقة لأنّ التابع ()filter_by قد يعيد عدّة نتائج، وبذلك نجلب الأولى منها فقط. ملاحظة: في حال رغبتك بجلب سجلٍ ما وفقًا لمعرّفه، فإن المنهجية الأفضل هي استخدام التابع ()get. من المُمكن أيضًا جلب موظف ما وفقًا لعمره، كما يلي: >>> employee = Employee.query.filter_by(age=52).first() >>> print(employee) فيكون الخرج بالشكل: <Employee Harold Ishida> لنأخذ مثالًا على حالة تتضمن فيها نتائج الاستعلام عدّة سجلات، إذ سنستعلم عندها وفقًا لاسم الموظف الأول firstname، وبقيمة "Mary"، وهنا توجد موظفتان بهذا الاسم في قاعدة البيانات لدينا: >>> mary = Employee.query.filter_by(firstname='Mary').all() >>> print(mary) فيكون الخرج بالشكل: [<Employee Mary Doe>, <Employee Mary Park>] استخدمنا التابع ()all في الشيفرة السابقة للحصول على قائمة نتائج الاستعلام كاملةً، كما من الممكن استخدام التابع ()first للحصول على النتيجة الأولى فقط، على النحو التالي: >>> mary = Employee.query.filter_by(firstname='Mary').first() >>> print(mary) ويكون الخرج عندها بالشكل: <Employee Mary Doe> وبذلك نكون قد جلبنا سجلاتٍ وفقًا لقيم أعمدة فيها، أما في الخطوة التالية فسنستعلم عن السجلات مُستخدمين شروطًا منطقية. الخطوة 3: ترشيح السجلات باستخدام الشروط المنطقية نحتاج غالبًا في تطبيقات الويب المعقدة كاملة الميزات إلى الاستعلام عن السجلات من قاعدة البيانات تحت شروطٍ معقدة، كأن نجلب الموظفين اعتمادًا على مجموعة من الشروط التي تأخذ في الحسبان مواقعهم وتوافرهم ودورهم الوظيفيّ ومسؤولياتهم. ستتدرب في هذه الخطوة على كيفية استخدام المعاملات الشرطية، كما ستستخدم تابع الترشيح ()filter من سمة الاستعلام query بغية ترشيح نتائج الاستعلامات وفقًا لشروط منطقية تربط فيما بينها معاملاتٍ مُختلفة، إذ يمكن مثلًا استخدام المعاملات المنطقية لجلب قائمة بالموظفين غير المتواجدين في مكاتبهم حاليًا، أو أولئك الذين يستحقون ترقيةً، أو حتى لتوفير جدولة لمواعيد إجازات الموظفين وغيرها. معامل التساوي لعلّ أبسط معامل منطقي يمكن أن نستخدمه هو معامل المساواة ==، والذي يعمل بآلية مُشابهة للتابع ()filter_by، فللحصول مثلًا على كافّة السجلات التي قيمة عمود الاسم الأول firstname فيها تساوي "Mary"، يمكننا استخدام التابع ()filter على النحو التالي: >>> mary = Employee.query.filter(Employee.firstname == 'Mary').all() >>> print(mary) استخدمنا في الشيفرة السابقة التعليمة Model.column == value وسيطًا للتابع ()filter، ويعدّ التابع ()filter_by اختصارًا لهذه الطريقة، وستكون النتيجة نفسها كما في حالة استخدام التابع ()filter_by تحت نفس الشرط، وهي: [<Employee Mary Doe>, <Employee Mary Park>] وكما هو الحال لدى استخدام التابع ()filter_by، من الممكن جلب نتيجة الاستعلام الأولى فقط باستخدام التابع ()first، على النحو التالي: >>> mary = Employee.query.filter(Employee.firstname == 'Mary').first() >>> print(mary) فيصبح الخرج على الشّكل التالي: <Employee Mary Doe> معامل عدم التساوي يتيح التابع ()filter إمكانية استخدام معامل عدم التساوي =! من بايثون في الحصول على السجلات، إذ يمكن مثلًا استخدام المنهجية التالية بغية الحصول على قائمة بالموظفين غير المتواجدين في مكاتبهم حاليًا: >>> out_of_office_employees = Employee.query.filter(Employee.active != True).all() >>> print(out_of_office_employees) فيكون الخرج بالشكل التالي: [<Employee Jane Tanaka>, <Employee Harold Ishida>] استخدمنا في الشيفرة السابقة الشرط Employee.active != True لترشيح نتائج الاستعلام. معامل الأصغر تماما من الممكن استخدام المعامل > للحصول على سجل وفقًا لشرط كون قيمة أحد أعمدته أصغر تمامًا من قيمة معينة، إذ يمكن مثلًا الحصول على قائمة بالموظفين الذين أعمارهم أصغر تمامًا من 32 عامًا على النحو التالي: >>> employees_under_32 = Employee.query.filter(Employee.age < 32).all() >>> >>> for employee in employees_under_32: >>> print(employee.firstname, employee.lastname) >>> print('Age: ', employee.age) >>> print('----') فيكون الخرج بالشكل التالي: Alex Brown Age: 29 ---- James White Age: 24 ---- Scarlett Winter Age: 22 ---- Emily Vill Age: 27 ---- Mary Park Age: 30 ---- كما من الممكن استخدام المعامل => للحصول على السجلات الأصغر من قيمة معينة أو تساويها، فمثلًا لتضمين الموظفين الذين أعمارهم تساوي 32 عامًا بنتائج الاستعلام السابق، نكتب: >>> employees_32_or_younger = Employee.query.filter(Employee.age <=32).all() >>> >>> for employee in employees_32_or_younger: >>> print(employee.firstname, employee.lastname) >>> print('Age: ', employee.age) >>> print('----') فيصبح الخرج بالشّكل: John Doe Age: 32 ---- Jane Tanaka Age: 32 ---- Alex Brown Age: 29 ---- James White Age: 24 ---- Scarlett Winter Age: 22 ---- Emily Vill Age: 27 ---- Mary Park Age: 30 ---- معامل الأكبر تماما من الممكن استخدام المعامل < للحصول على سجل وفقًا لشرط كون قيمة أحد أعمدته أكبر تمامًا من قيمة معينة. فعلى سبيل المثال، للحصول على قائمة بالموظفين الذين أعمارهم أكبر تمامًا من 32 عامًا، نكتب: >>> employees_over_32 = Employee.query.filter(Employee.age > 32).all() >>> >>> for employee in employees_over_32: >>> print(employee.firstname, employee.lastname) >>> print('Age: ', employee.age) >>> print('----') فيكون الخرج بالشكل: Mary Doe Age: 38 ---- Harold Ishida Age: 52 ---- كما من الممكن استخدام المعامل =< للحصول على السجلات الأكبر من قيمة معينة أو تساويها. فمثلًا لتضمين الموظفين الذين أعمارهم تساوي 32 عامًا بنتائج الاستعلام السابق، نكتب: >>> employees_32_or_older = Employee.query.filter(Employee.age >=32).all() >>> >>> for employee in employees_32_or_older: >>> print(employee.firstname, employee.lastname) >>> print('Age: ', employee.age) >>> print('----') فيصبح الخرج بالشّكل: John Doe Age: 32 ---- Mary Doe Age: 38 ---- Jane Tanaka Age: 32 ---- Harold Ishida Age: 52 ---- تابع الانتماء In تتيح الإضافة SQLAlchemy إمكانية الحصول على السجلات التي توافق قيمة أحد أعمدتها قيمةً ضمن قائمة معطاة وذلك باستخدام التابع ()_in على العمود المطلوب على النحو التالي: >>> names = ['Mary', 'Alex', 'Emily'] >>> employees = Employee.query.filter(Employee.firstname.in_(names)).all() >>> print(employees) فيكون الخرج بالشكل: [<Employee Mary Doe>, <Employee Alex Brown>, <Employee Emily Vill>, <Employee Mary Park>] استخدمنا في الشيفرة السابقة شرطًا وفق الصيغة (Model.column.in_(iterable، إذ تشير العبارة iterable لأي نوع تكراري من الكائنات (أي كائن يمكن المرور على عناصره، مثل القائمة). لنأخذ مثال آخر، إذ من الممكن استخدام دالة بايثون ()range للحصول على الموظفين المُنتمين لمجال معيّن من العمر، والاستعلام التالي يجلب كافّة الموظفين الذين أعمارهم في الثلاثينات: >>> employees_in_30s = Employee.query.filter(Employee.age.in_(range(30, 40))).all() >>> for employee in employees_in_30s: >>> print(employee.firstname, employee.lastname) >>> print('Age: ', employee.age) >>> print('----') فيكون الخرج بالشّكل: John Doe Age: 32 ---- Mary Doe Age: 38 ---- Jane Tanaka Age: 32 ---- Mary Park Age: 30 ---- تابع عدم الانتماء Not in من الممكن استخدام تابع عدم الانتماء ()not_in بصورةٍ مشابهة لآلية استخدام التابع السابق ()_in بغية الحصول على السجلات التي لا تنتمي قيمة أحد أعمدتها لمجموعة قيم ضمن كائن تكراري ما، كما في المثال التالي: >>> names = ['Mary', 'Alex', 'Emily'] >>> employees = Employee.query.filter(Employee.firstname.not_in(names)).all() >>> print(employees) فيكون الخرج بالشّكل: [<Employee John Doe>, <Employee Jane Tanaka>, <Employee James White>, <Employee Harold Ishida>, <Employee Scarlett Winter>] إذ حصلنا على كافّة الموظفين عدا أولئك الذي أسماؤهم الأولى موجودة ضمن القائمة المُسمّاة names. معامل And من الممكن ربط عدّة شروط منطقية مع بعضها بعضًا باستخدام الدالة ()_db.and التي تعمل بنفس آلية المعامل and في بايثون. لنفرض مثلًا أننا نريد الحصول على جميع الموظفين الذين تبلغ أعمارهم 32 عامًا والمتواجدين في مكاتبهم حاليًا، ففي هذه الحالة، سنتحقق بدايةً من الموظفين البالغة أعمارهم 32 عامًا باستخدام التابع ()filter_by (أو التابع ()filter إذا أردت)، على النحو: >>> for employee in Employee.query.filter_by(age=32).all(): >>> print(employee) >>> print('Age:', employee.age) >>> print('Active:', employee.active) >>> print('-----') فيكون الخرج على النحو التالي: <Employee John Doe> Age: 32 Active: True ----- <Employee Jane Tanaka> Age: 32 Active: False ----- حصلنا في الخرج السابق على موظَفيْن تبلغ أعمارهما 32 عامًا، أحدهما متواجد في مكتبه حاليًا والآخر قد غادره، وللحصول على الموظفين البالغة أعمارهم 32 عامًا والمتواجدين في مكاتبهم حاليًا، فلا بدّ من استخدام شرطين ضمن التابع ()filter، هما: Employee.age == 32 Employee.active == True ولربط الشرطين معًا، من الممكن استخدام الدالة ()_db.and كما يلي: >>> active_and_32 = Employee.query.filter(db.and_(Employee.age == 32, >>> Employee.active == True)).all() >>> print(active_and_32) فيكون الخرج على النحو التالي: [<Employee John Doe>] استخدمنا في الشيفرة السابقة الصيغة ((filter(db.and_(condition1, condition2 للربط بين الشرطين. ومع تطبيق التابع ()all على نتائج الاستعلام نحصل على قائمة بكافّة النتائج المُحقّقة لكلا الشرطين، كما من الممكن استخدام التابع ()first للحصول على النتيجة الأولى فقط من نتائج الاستعلام. >>> active_and_32 = Employee.query.filter(db.and_(Employee.age == 32, >>> Employee.active == True)).first() >>> print(active_and_32) فيكون الخرج على النحو التالي: <Employee John Doe> لنأخذ مثالًا أكثر تعقيدًا، لنفرض أننا نريد الاستعلام عن الموظفين الذين جرى تعيينهم خلال فترة زمنية معينة، عندها من الممكن استخدام الدالة ()db.and مع الدالة ()date، كما في المثال التالي الهادف للحصول على كافّة الموظفين المُعينين خلال عام 2019: >>> from datetime import date >>> >>> hired_in_2019 = Employee.query.filter(db.and_(Employee.hire_date >= date(year=2019, month=1, day=1), Employee.hire_date < date(year=2020, month=1, day=1))).all() >>> >>> for employee in hired_in_2019: >>> print(employee, ' | Hired: ', employee.hire_date) فيكون الخرج على النحو التالي: <Employee Alex Brown> | Hired: 2019-01-03 <Employee Emily Vill> | Hired: 2019-06-09 استوردنا في الشيفرة السابقة الدالة ()date، كما رشّحنا نتائج الاستعلام باستخدام الدالة ()_db.and بغية ربط شرطين، هما: (Employee.hire_date >= date(year=2019, month=1, day=1: وهو مُحقّق "True" من أجل الموظفين المُعينين بدءًا من الأول من كانون الثاني لعام 2019 فما بعد. (Employee.hire_date < date(year=2020, month=1, day=1: وهو مُحقّق "True" من أجل الموظفين المُعينين قبل الأول من كانون الثاني لعام 2020. حصلنا من خلال الربط بين الشرطين على الموظفين المُعينين بدءًا من بداية عام 2019 وحتى نهايته. معامل Or تعمل الدالة ()_db.or على ربط شرطين منطقيين كما تفعل الدالة ()_db.and، إلا أنها تشابه آلية عمل المعامل or في بايثون، إذ تعمل على جلب كافّة السجلات التي تحقق شرطًا على الأقل من الشرطين المُمررين إليها، فعلى سبيل المثال، للحصول على الموظفين البالغة أعمارهم 32 عامًا أو 52 عامًا، من الممكن الربط بين هذين الشرطين باستخدام الدالة ()_db.or بالشكل: >>> employees_32_or_52 = Employee.query.filter(db.or_(Employee.age == 32, Employee.age == 52)).all() >>> >>> for e in employees_32_or_52: >>> print(e, '| Age:', e.age) فيكون الخرج على النحو التالي: <Employee John Doe> | Age: 32 <Employee Jane Tanaka> | Age: 32 <Employee Harold Ishida> | Age: 52 يمكن استخدام التابعين ()startswith (الذي يشترط على السلسة النصية البدء بمحرف معيّن) و ()endswith (الذي يشترط على السلسة النصية الانتهاء بمحرف معيّن) على السلاسل النصية في الشروط المُمرّرة إلى التابع ()filter، فعلى سبيل المثال، لجلب كافة الموظفين الذين يبدأ اسمهم الأول بالحرف 'M' وأولئك الذين ينتهي اسمهم الأخير بالحرف 'e' نكتب: >>> employees = Employee.query.filter(db.or_(Employee.firstname.startswith('M'), Employee.lastname.endswith('e'))).all() >>> >>> for e in employees: >>> print(e) فنحصل على الخرج التالي: <Employee John Doe> <Employee Mary Doe> <Employee James White> <Employee Mary Park> ربطنا في الشيفرة السابقة الشرطين التاليين: ('Employee.firstname.startswith('M: للحصول على الموظفين الذين يبدأ اسمهم الأول بالحرف 'M'. ('Employee.lastname.endswith('e: للحصول على الموظفين الذين ينتهي اسمهم الأخير بالحرف 'e'. وبذلك أصبح من الممكن الآن ترشيح نتائج الاستعلام باستخدام الشروط المنطقية في تطبيقات Flask-SQLAlchemy، أمّا في الخطوة التالية فسنعمل على ترتيب وتحديد وحساب عدد النتائج التي نحصل عليها من قاعدة البيانات. الخطوة 4: ترتيب وتحديد وحساب عدد النتائج غالبًا ما نضطر إلى ترتيب السجلات عند عرضها في تطبيقات الويب، فمن الممكن مثلًا أن يكون لدينا صفحةٌ لعرض التعيينات الأخيرة في كل قسم، وهذا يجعل باقي أعضاء فريق العمل يتعرّفون على زملائهم الجدد؛ كما من الممكن ترتيب عرض الموظفين بحيث يظهر الموظفين الأقدم أولًا بهدف معرفة أطول الموظفين خدمةً؛ وقد تحتاج أيضًا في بعض الحالات إلى تحديد النتائج، كما في حال رغبتك بعرض أحدث ثلاث موظفين ضمن شريط جانبي صغير، وغالبًا ما نضطر إلى حساب عدد نتائج الاستعلام، لعرض مثلًا عدد الموظفين المتواجدين في مكاتبهم حاليًا. ستتعرف في هذه الخطوة على كيفية ترتيب وتحديد وحساب عدد نتائج الاستعلامات. ترتيب النتائج نستخدم التابع ()order_by لترتيب النتائج وفقًا لقيم عمود ما، فعلى سبيل المثال، لترتيب النتائج حسب الاسم الأول للموظف نكتب: >>> employees = Employee.query.order_by(Employee.firstname).all() >>> print(employees) فنحصل على الخرج التالي: [<Employee Alex Brown>, <Employee Emily Vill>, <Employee Harold Ishida>, <Employee James White>, <Employee Jane Tanaka>, <Employee John Doe>, <Employee Mary Doe>, <Employee Mary Park>, <Employee Scarlett Winter>] نلاحظ من الخرج السابق أنّ النتائج مُرتّبة أبجديًا وفق الاسم الأول للموظف، كما من الممكن ترتيب النتائج وفقًا لقيم عمود آخر، كأن نستخدم الاسم الأخير للترتيب، كما يلي: >>> employees = Employee.query.order_by(Employee.lastname).all() >>> print(employees) فيصبح الخرج على النحو التالي: [<Employee Alex Brown>, <Employee John Doe>, <Employee Mary Doe>, <Employee Harold Ishida>, <Employee Mary Park>, <Employee Jane Tanaka>, <Employee Emily Vill>, <Employee James White>, <Employee Scarlett Winter>] كما من الممكن ترتيب الموظفين وفقًا لتواريخ تعيّينهم، على النحو: >>> em_ordered_by_hire_date = Employee.query.order_by(Employee.hire_date).all() >>> >>> for employee in em_ordered_by_hire_date: >>> print(employee.firstname, employee.lastname, employee.hire_date) فيكون الخرج على النحو التالي: Harold Ishida 2002-03-06 John Doe 2012-03-03 Jane Tanaka 2015-09-12 Mary Doe 2016-06-07 Alex Brown 2019-01-03 Emily Vill 2019-06-09 James White 2021-02-04 Scarlett Winter 2021-04-07 Mary Park 2021-08-11 نلاحظ من الخرج السابق ترتيب تواريخ التعيين تصاعديًا من الأقدم إلى الأحدث، ومن الممكن عكس هذا الترتيب وجعله تنازلي أي من الأحدث إلى الأقدم باستخدام التابع ()desc على النحو التالي: >>> em_ordered_by_hire_date_desc = Employee.query.order_by(Employee.hire_date.desc()).all() >>> >>> for employee in em_ordered_by_hire_date_desc: >>> print(employee.firstname, employee.lastname, employee.hire_date) فيصبح الخرج كما يلي: Mary Park 2021-08-11 Scarlett Winter 2021-04-07 James White 2021-02-04 Emily Vill 2019-06-09 Alex Brown 2019-01-03 Mary Doe 2016-06-07 Jane Tanaka 2015-09-12 John Doe 2012-03-03 Harold Ishida 2002-03-06 كما من الممكن ربط كلا تابعي الترشيح ()filter والترتيب ()order_by معًا بغية ترتيب النتائج بعد ترشيحها، إذ تجلب الشيفرة التالية مثلًا كافّة الموظفين المُعينين خلال عام 2021 ثمّ ترتبهم حسب أعمارهم: >>> from datetime import date >>> hired_in_2021 = Employee.query.filter(db.and_(Employee.hire_date >= date(year=2021, month=1, day=1), Employee.hire_date < date(year=2022, month=1, day=1))).order_by(Employee.age).all() >>> >>> for employee in hired_in_2021: >>> print(employee.firstname, employee.lastname, >>> employee.hire_date, '| Age', employee.age) فيكون الخرج كما يلي: Scarlett Winter 2021-04-07 | Age 22 James White 2021-02-04 | Age 24 Mary Park 2021-08-11 | Age 30 استخدمنا في الشيفرة السابقة الدالة ()_db.and للربط بين شرطين مطلوب تحققهما معًا، الأول لتحديد الموظفين المُعينين بدءًا من الأول من كانون الثاني لعام 2021 فما بعد: Employee.hire_date >= date(year=2021, month=1, day=1) والثاني لتحديد الموظفين المُعينين قبل اليوم الأول من عام 2022: Employee.hire_date < date(year=2022, month=1, day=(1) ومن ثمّ استخدمنا التابع ()order_by لترتيب الموظفين المُحققين للشرطين السابقين وفق أعمارهم. تحديد النتائج قد نحصل في معظم حالات الاستعلام عن جداول قواعد البيانات في التطبيقات الحقيقية على ملايين النتائج الموافقة، ما يجعل من تحديد عدد النتائج برقم معيّن أمرًا ضروريًا، ولتحقيق ذلك في تطبيقات Flask-SQLAlchemy، نستخدم التابع ()limit. يبين المثال التالي كيفية الاستعلام عن البيانات في جدول الموظفين employee، ليعيد أول ثلاث نتائج موافقة فقط: >>> employees = Employee.query.limit(3).all() >>> print(employees) فيكون الخرج على النحو التالي: [<Employee John Doe>, <Employee Mary Doe>, <Employee Jane Tanaka>] كما من الممكن استخدام التابع ()limit مع توابع أُخرى، مثل filter و order_by، إذ يمكن مثلًا الحصول على آخر موظَفيْن مُعيَنيْن لعام 2021 باستخدام التابع ()limit على النحو التالي: >>> from datetime import date >>> hired_in_2021 = Employee.query.filter(db.and_(Employee.hire_date >= date(year=2021, month=1, day=1), Employee.hire_date < date(year=2022, month=1, day=1))).order_by(Employee.age).limit(2).all() >>> >>> for employee in hired_in_2021: >>> print(employee.firstname, employee.lastname, >>> employee.hire_date, '| Age', employee.age) فنحصل على الخرج التالي: Scarlett Winter 2021-04-07 | Age 22 James White 2021-02-04 | Age 24 استخدمنا في الشيفرة السابقة نفس الاستعلام الوارد في القسم السابق ولكن مع استدعاء للتابع (limit(2. حساب عدد النتائج يمكننا استخدام التابع ()count لحساب عدد نتائج استعلام ما، فعلى سبيل المثال، للحصول على عدد الموظفين الموجودين في قاعدة البيانات نكتب: >>> employee_count = Employee.query.count() >>> print(employee_count) فنحصل على الخرج التالي: 9 كما من الممكن استخدام التابع ()count جنبًا إلى جنب مع توابع استعلام أُخرى مثل ()limit. للحصول على عدد الموظفين المُعينين خلال عام 2021 مثلًا نكتب: >>> from datetime import date >>> hired_in_2021_count = Employee.query.filter(db.and_(Employee.hire_date >= date(year=2021, month=1, day=1), Employee.hire_date < date(year=2022, month=1, day=1))).order_by(Employee.age).count() >>> print(hired_in_2021_count) فنحصل على الخرج التالي: 3 استخدمنا في هذه الشيفرة نفس الاستعلام السابق الخاص بالحصول على الموظفين المُعينين خلال عام 2021 ولكن مع استخدام التابع ()count للحصول على عددهم (عدد السجلات الموافقة) وهو 3. ومع نهاية هذه الخطوة نكون قد تعرّفنا على كيفية ترتيب وتحديد وحساب عدد نتائج استعلام في Flask-SQLAlchemy، أمّا في الخطوة التالية فسنتعرف على كيفية تقسيم نتائج الاستعلام على صفحاتٍ مُتعدّدة، إضافةً إلى كيفية إنشاء نظام ترقيم في تطبيقات فلاسك. الخطوة 5: كيفية عرض قوائم السجلات الطويلة على صفحات متعددة سنعمل في هذه الخطوة على تعديل الوجهة الرئيسية للتطبيق لتجعل الصفحة الرئيسية تعرض سجلات الموظفين على صفحاتٍ مُتعدّدة، ما يجعل من تصفّح قائمة الموظفين أسهل. سنستخدم بدايةً صَدَفة فلاسك لنوضّح كيفية استخدام ميزة الترقيم pagination في الإضافة Flask-SQLAlchemy، لذا نفتح صَدَفة فلاسك (إن لم تكن مفتوحة أصلًا): (env)user@localhost:$ flask shell وبفرض أنّنا نريد توزيع سجلات جدول الموظفين على صفحاتٍ مُتعدّدة، من خلال عرض عنصرين اثنين فقط في كل صفحة، فمن الممكن إنجاز ذلك باستخدام تابع الاستعلام ()paginate كما يلي: >>> page1 = Employee.query.paginate(page=1, per_page=2) >>> print(page1) >>> print(page1.items) فيكون الخرج على النحو التالي: <flask_sqlalchemy.Pagination object at 0x7f1dbee7af80> [<Employee John Doe>, <Employee Mary Doe>] استخدمنا في الشيفرة السابقة معامل الصفحة page من تابع الاستعلام ()paginate بغية تحديد الصفحة المراد الوصول إليها، وهي الصفحة الأولى في حالتا، أمّا المعامل per_page فيُحدّد عدد العناصر المعروضة في كل صفحة، وقد عيّنا قيمته لتكون "2" لتعرض كل صفحة عنصرين فقط، أمّا المتغير page1 فما هو إلا كائن ترقيم يمكّننا من الوصول إلى السمات والتوابع المُستخدمة في إدارة عملية الترقيم. استخدمنا السمة items للوصول إلى عناصر الصفحة؛ أما للوصول إلى الصفحة التالية، فمن الممكن استخدام التابع ()next من كائن الترقيم على النحو التالي، إذ ستكون النتيجة المُعادة كائن ترقيم أيضًا: >>> page2 = page1.next() >>> >>> print(page2.items) >>> print(page2) فيكون الخرج على النحو التالي: [<Employee Jane Tanaka>, <Employee Alex Brown>] <flask_sqlalchemy.Pagination object at 0x7f1dbee799c0> كما من الممكن الحصول على كائن ترقيم خاص بالصفحة السابقة باستخدام التابع ()prev، ففي المثال التالي، نحصل على كائن الترقيم الخاص بالصفحة الرابعة، ومنه نصل إلى كائن ترقيم الصفحة السابقة، أي الصفحة رقم 3، على النحو التالي: >>> page4 = Employee.query.paginate(page=4, per_page=2) >>> print(page4.items) >>> page3 = page4.prev() >>> print(page3.items) فيكون الخرج كما يلي: [<Employee Scarlett Winter>, <Employee Emily Vill>] [<Employee James White>, <Employee Harold Ishida>] كما من الممكن الوصول إلى رقم الصفحة الحالية باستخدام السمة page على النحو: >>> print(page1.page) >>> print(page2.page) فنحصل على الخرج التالي: 1 2 كما من الممكن استخدام السمة pages من كائن الترقيم للحصول على العدد الإجمالي من الصفحات، ففي المثال التالي، تعيد كلًا من التعليمتين page1.pages و page2.pages نفس القيمة لأن العدد الإجمالي للصفحات ثابت: >>> print(page1.pages) >>> print(page2.pages) ويكون الخرج على النحو التالي: 5 5 أمّا للحصول على العدد الكلي من العناصر، فنستخدم السمة total من كائن الترقيم، على النحو التالي: >>> print(page1.total) >>> print(page2.total) فنحصل على الخرج التالي: 9 9 نلاحظ في الخرج السابق وطالما أنّنا استعلمنا عن كافّة الموظفين، سيكون العدد الإجمالي للعناصر في كائن الترقيم هو 9، نظرًا لوجود تسعة موظفين في قاعدة البيانات. وفيما يلي قائمة ببعض السمات الأخرى التي تملكها كائنات الترقيم: prev_num: رقم الصفحة السابقة. next_num: رقم الصفحة التالية. has_next: تعيد القيمة "True" في حال وجود صفحة تالية. has_prev: تعيد القيمة "True" في حال وجود صفحة سابقة. per_page: عدد العناصر في الصفحة. كما يمتلك كائن الترقيم تابعًا باسم ()iter_pages للمرور على أرقام الصفحات، إذ من الممكن مثلًا طباعة أرقام الصفحات على النحو: >>> pagination = Employee.query.paginate(page=1, per_page=2) >>> >>> for page_num in pagination.iter_pages(): >>> print(page_num) فيكون الخرج كما يلي: 1 2 3 4 5 وفيما يلي مثال لتوضيح كيفية الوصول إلى جميع الصفحات وعناصرها باستخدام كل من كائن الترقيم والتابع ()iter_pages: >>> pagination = Employee.query.paginate(page=1, per_page=2) >>> >>> for page_num in pagination.iter_pages(): >>> print('PAGE', pagination.page) >>> print('-') >>> print(pagination.items) >>> print('-'*20) >>> pagination = pagination.next() فنحصل على الخرج التالي: PAGE 1 - [<Employee John Doe>, <Employee Mary Doe>] -------------------- PAGE 2 - [<Employee Jane Tanaka>, <Employee Alex Brown>] -------------------- PAGE 3 - [<Employee James White>, <Employee Harold Ishida>] -------------------- PAGE 4 - [<Employee Scarlett Winter>, <Employee Emily Vill>] -------------------- PAGE 5 - [<Employee Mary Park>] -------------------- أنشأنا في الشيفرة السابقة كائن ترقيم يبدأ من الصفحة الأولى، ثمّ مررنا على الصفحات باستخدام حلقة for تكرارية عن طريق تابع الترقيم ()iter_pages، لنطبع رقم وعناصر الصفحة، ثم أسندنا قيمة كائن الترقيم الخاص بالصفحة التالية إلى كائن الترقيم pagination باستخدام التابع ()next. يمكن استخدام كلًا من تابعي الترشيح ()filter والترتيب ()order_by جنبًا إلى جنب مع تابع الترقيم ()paginate لترقيم نتائج الاستعلام بعد ترشيحها وترتيبها، فعلى سبيل المثال، يمكننا الحصول على الموظفين ممن تجاوزوا الثلاثين من عمرهم ونرتّب النتائج وفقًا للعمر وننفّذ على النتائج عملية ترقيم كما يلي: >>> pagination = Employee.query.filter(Employee.age > 30).order_by(Employee.age).paginate(page=1, per_page=2) >>> >>> for page_num in pagination.iter_pages(): >>> print('PAGE', pagination.page) >>> print('-') >>> for employee in pagination.items: >>> print(employee, '| Age: ', employee.age) >>> print('-'*20) >>> pagination = pagination.next() فنحصل على خرجٍ بالشّكل التالي: PAGE 1 - <Employee John Doe> | Age: 32 <Employee Jane Tanaka> | Age: 32 -------------------- PAGE 2 - <Employee Mary Doe> | Age: 38 <Employee Harold Ishida> | Age: 52 -------------------- الآن وبعد أن تعرفنا على آلية عمل ميزة الترقيم في Flask-SQLAlchemy بوضوح، سنحرّر صفحة التطبيق الرئيسية لعرض الموظفين على صفحاتٍ مُتعدّدة، وهذا ما يجعل تصفحهم أسهل على المُستخدم. نخرج من صَدَفة فلاسك: >>> exit() سنستخدم معاملات الروابط URL المعروفة باسم سلاسل الاستعلام عن الروابط URL query strings للوصول إلى صفحاتِ الموظفين المُختلفة، وهي طريقة لتمرير المعلومات إلى التطبيق من خلال الروابط، إذ تُمرّر المعاملات إلى التطبيق بعد الرمز ? في الرابط. على سبيل المثال، من الممكن استخدام الروابط التالية لتمرير معامل الصفحة page بقيمٍ مُختلفة: http://127.0.0.1:5000/?page=1 http://127.0.0.1:5000/?page=3 إذ يمرِّر هنا الرابط الأوّل القيمة "1" إلى معامل الرابط page، بينما يمرّر الرابط الثاني القيمة "3" لنفس المعامل. نفتح الآن الملف app.py: (env)user@localhost:$ nano app.py ونعدّل الوجهة الرئيسية index لتصبح على النحو التالي: @app.route('/') def index(): page = request.args.get('page', 1, type=int) pagination = Employee.query.order_by(Employee.firstname).paginate( page, per_page=2) return render_template('index.html', pagination=pagination) حصلنا في الشيفرة السابقة على قيمة معامل الرابط للصفحة المسمّى page باستخدام كل من الكائن request.args وتابعه ()get. فعلى سبيل المثال، نأخذ من الجزء page=1?/ من الرابط القيمة "1" الخاصّة بمعامل الرابط للصفحة المسمّى page، لتُمرّر هذه القيمة "1" مثل قيمة افتراضية، ونمرّر نمط البيانات int من بايثون مثل وسيط لمعامل النوع type لضمان كون القيمة المُمررة من النوع عدد صحيح. ثمّ أنشأنا الكائن pagination، ورتّبنا نتائج الاستعلام وفق الاسم الأول للموظف، كما مرّرنا قيمة معامل الرابط page إلى التابع ()paginate، ووزّعنا النتائج على عنصرين لكل صفحة عبر تمرير القيمة "2" إلى المعامل per_page، ومرّرنا نهايةً كائن الترقيم المُنشأ pagination إلى القالب المُصيّر index.html. نحفظ الملف ونغلقه. أمّا الآن، سنحرر القالب index.html بغية عرض عناصر الترقيم: (env)user@localhost:$ nano templates/index.html سنعدّل وسم التقسيم div الخاص بالمحتوى ليتضمّن عنوانًا من المستوى الثاني h2 للدلالة على الصفحة الحالية، كما سنعدّل حلقة for التكرارية لتمر على الكائن pagination.items بدلًا من الكائن employees الذي لم يعد موجودًا. <div class="content"> <h2>(Page {{ pagination.page }})</h2> {% for employee in pagination.items %} <div class="employee"> <p><b>#{{ employee.id }}</b></p> <b> <p class="name">{{ employee.firstname }} {{ employee.lastname }}</p> </b> <p>{{ employee.email }}</p> <p>{{ employee.age }} years old.</p> <p>Hired: {{ employee.hire_date }}</p> {% if employee.active %} <p><i>(Active)</i></p> {% else %} <p><i>(Out of Office)</i></p> {% endif %} </div> {% endfor %} </div> نحفظ الملف ونغلقه. الآن، سنضبط كلًا من متغيري البيئة FLASK_APP و FLASK_ENV، كما سنشغّل خادم التطوير (وذلك في حال عدم إنجاز هذه الخطوات مُسبقًا): (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development (env)user@localhost:$ flask run ننتقل الآن إلى الصفحة الرئيسية للتطبيق باستخدام قيمٍ مُختلفة لمعامل الرابط page، على النحو التالي: http://127.0.0.1:5000/ http://127.0.0.1:5000/?page=2 http://127.0.0.1:5000/?page=4 http://127.0.0.1:5000/?page=19 فسيظهر في كل مرة صفحة جديدة، يتضمّن كل منها عنصرين مُختلفين بما يُشبه النتيجة التي رأيناها سابقًا ضمن صَدَفة فلاسك. وفي حال كون الرقم المُمرّر إلى معامل الرابط غير موجود، فعندها سيُعرض خطأ HTTP من النوع "404 Not Found"، كما هو الحال مع الرابط الأخير من قائمة الروابط أعلاه. أمّا الآن، فسننشئ عنصر واجهة مستخدم widget خاص بالترقيم لنسمح للمستخدم بالتنقّل بين الصفحات، إذ سنستخدم بعض سمات وتوابع كائن الترقيم بغية عرض أرقام كافّة الصفحات، إذ سيمثّل كل رقم رابطًا إلى صفحته الموافقة، كما سنعرض الزر >>> للرجوع في حال كانت الصفحة الحالية تمتلك صفحة سابقة، وزر <<< للانتقال إلى الصفحة التالية في حال كانت الصفحة الحالية تمتلك صفحة تليها. وسيبدو عنصر واجهة المستخدم الخاص بالترقيم بالشكل التالي: ولإضافته، نفتح الملف index.html: (env)user@localhost:$ nano templates/index.html ونعدّله بإضافة وسم تقسيم div جديد أسفل وسم div السابق الخاص بالمحتوى، على النحو: <div class="content"> {% for employee in pagination.items %} <div class="employee"> <p><b>#{{ employee.id }}</b></p> <b> <p class="name">{{ employee.firstname }} {{ employee.lastname }}</p> </b> <p>{{ employee.email }}</p> <p>{{ employee.age }} years old.</p> <p>Hired: {{ employee.hire_date }}</p> {% if employee.active %} <p><i>(Active)</i></p> {% else %} <p><i>(Out of Office)</i></p> {% endif %} </div> {% endfor %} </div> <div class="pagination"> {% if pagination.has_prev %} <span> <a class='page-number' href="{{ url_for('index', page=pagination.prev_num) }}"> {{ '<<<' }} </a> </span> {% endif %} {% for number in pagination.iter_pages() %} {% if pagination.page != number %} <span> <a class='page-number' href="{{ url_for('index', page=number) }}"> {{ number }} </a> </span> {% else %} <span class='current-page-number'>{{ number }}</span> {% endif %} {% endfor %} {% if pagination.has_next %} <span> <a class='page-number' href="{{ url_for('index', page=pagination.next_num) }}"> {{ '>>>' }} </a> </span> {% endif %} </div> نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة الشرط if pagination.has_prev بغية إضافة رابط لزر الرجوع إلى الصفحة السابقة >>> وذلك فقط في حال لم تكن الصفحة الحالية هي الصفحة الأولى، إذ ربطنا الزر مع الصفحة السابقة من خلال استدعاء الدالة التالية: url_for('index', page=pagination.prev_num) وفيها يُربط الزر بدالة العرض الرئيسية index ممررين القيمة pagination.prev_num إلى معامل الرابط page. لعرض روابط لكل أرقام الصفحات المتوفرة، مررنا باستخدام حلقة تكرارية على كافّة عناصر التابع ()pagination.iter_pages الذي يزودنا برقم صفحة عند كل تكرار، واستخدمنا الشرط if pagination.page != number لنتحقق ما إذا كان رقم الصفحة الحالي مغاير للرقم في التكرار الحالي؛ فعند تحقق الشرط، ننشئ رابطًا يُمكّن المستخدم من تغيّير الصفحة الحالية إلى صفحة أخرى، وإلّا فإنّنا نعرض رقم الصفحة دون رابط، إذ تكون الصفحة الحالية في هذه الحالة موافقة للرقم الحالي في التكرار، وهذا ما سيساعد المستخدمين على معرفة رقم الصفحة الحالية من خلال عنصر واجهة المستخدم الخاص بالترقيم. استخدمنا نهايةً الشرط pagination.has_next لنتحقّق من وجود صفحة تلي الصفحة الحالية، وفي حال تحقُّق هذا الشرط، نُنشئ رابطًا إليها من خلال استدعاء التابع وربطه بالزر <<<: url_for('index', page=pagination.next_num) الآن وبالانتقال إلى الصفحة الرئيسية للتطبيق عبر المتصفح باستخدام الرابط: http://127.0.0.1:5000/ سنجد عنصر واجهة المستخدم الخاص بالترقيم يعمل بنجاح، كما في الشكل التالي: استخدمنا هنا الرمز <<< للدلالة على الانتقال إلى الصفحة التالية، والرمز >>> للدلالة على الانتقال إلى الصفحة السابقة، ولكن من الممكن استخدام أي رمز آخر مثل > و <، أو حتى صور باستخدام وسم الصورة <img>. مع نهاية هذه الخطوة نكون قد عرضنا بيانات الموظفين على صفحاتٍ مُتعدّدة، وتعلّمنا كيفية التعامل مع مفهوم الترقيم في Flask-SQLAlchemy، ومن الآن أصبح بإمكانك استخدام عنصر واجهة المستخدم الخاص بالترقيم الذي أنشأناه في أي تطبيقات فلاسك أخرى تُنشئها. الخاتمة استخدمنا في هذا المقال الإضافة Flask-SQLAlchemy لإنشاء نظام إدارة موظفين، إذ استعرضنا كيفية الاستعلام عن جدول وترشيح نتائجه وفق قيم أحد أعمدته أو وفق شروطٍ منطقية بسيطة ومُركّبة، كما بيّنا كيفية ترتيب نتائج الاستعلامات وتحديدها وحساب عددها، وأنشأنا أيضًا نظام ترقيم بغية عرض عددٍ مُحدّد من السجلات في كل صفحة من صفحات تطبيق الويب، مع إمكانية التنقّل بين هذه الصفحات. ترجمة -وبتصرف- للمقال How To Query Tables and Paginate Data in Flask-SQLAlchemy لصاحبه Abdelhadi Dyouri. اقرأ أيضًا ملء قاعدة البيانات ببيانات تجريبية والطرق المبدئية للحصول عليها في SQLAlchemy إنشاء جدولي المقالات والمُستخدمين في قاعدة البيانات تجهيز جدولي المقالات والمستخدمين باستعمال إضافة Flask-SQLAlchemy
-
"المسألتان الأصعب في علم الحاسوب هما ثلاثة! اختيار الأسماء وحذف أو استبدال محتويات الذاكرة المخبئية cache invalidation وتجنب أخطاء التكرار المنطقية off-by-one" هي إحدى أقدم الدعابات البرمجية، المنسوبة إلى ليون بامبريك والمستوحاة من اقتباس لفيل كارلتون، إلا أنها تشير إلى حقيقة جوهرية، مفادها أن ابتكار أسماء جيدة للمتغيرات والدوال والأصناف وغيرها مما يسمى إجمالًا بالمعرفات ليست بالمهمة السهلة. باختصار، الأسماء القادرة على توصيف وظيفة ومضمون الأشياء مهمة لمقروئية الشيفرة البرمجية، إلا أن القول أسهل من الفعل في عملية اختيار الأسماء، فلنفرض مثلًا أنك تنتقل إلى منزل جديد، فإن كتابة كلمة "أغراض أو متاع" على كافة الصناديق المنقولة سيكون اسمًا مختصرًا ولكنه ليس معبّرًا عن محتويات كل صندوق. أو لدى اختيار اسم معبّر لكتاب برمجي، فقد يكون "ابتكر ألعاب الكمبيوتر بنفسك باستخدام بايثون" ولكنه ليس مختصرًا. ما لم تكن تكتب شيفرة تجريبية لا تنوي الاحتفاظ بها بعد اختبارها وتشغيلها لمرة واحدة، يجب عليك التفكير في اختيار الأسماء الجيدة في برنامجك. فإن استخدمت أسماء بسيطة من قبيل a وb وc للمتغيرات، فستبذل مستقبلًا جهدًا إضافيًا لتذكّر ما كان الغرض من استخدام كل من هذه المتغيرات. ومما لا شك فيه أن اختيار الأسماء هو قرار شخصي أنت المسؤول عنه، إذ لا يمكن لأداة تنسيق آلي من قبيل المنسّق Black، الذي أتينا على شرحه في المقال السابق، أن يختار الأسماء للمتغيرات الخاصة بك. يقدم هذا المقال بعض الإرشادات للمساعدة في اختيار الأسماء المناسبة وتجنب الأسماء الرديئة. وكما اعتدنا، فإن هذه الارشادات ليست صارمة، وكنت أنت الربان مقررًا متى وكيف ستطبقها على شيفراتك. المتغيرات ذات الأسماء المؤقتة البديلة عادة ما نستخدم المتغيرات ذات الأسماء المؤقتة البديلة Metasyntactic في المواد التعليمية أو في أمثلة الشيفرات البرمجية كعنصر نائب، إذ نحتاج إلى مجرد اسم فريد للمتغير. فغالبًا ما تسمى المتغيرات في أمثلة بايثون بأسماء عشوائية من قبيل spam و eggs حين يكون اختيار الاسم غير مهم، ولهذا السبب نستخدم في كتابنا هذا النوع من الأسماء في أمثلة الشيفرات التي لا يكون مطلوبًا منك استخدامها في برامجك الفعلية، إذ اقتبسنا هذه الأسماء من مشهد تمثيلي لمجموعة مونتي بايثون البريطانية للكوميديا السيريالية بعنوان البريد العشوائي "Spam (Monty Python sketch)". كما أن أسماء foo وbar شائعة أيضًا للمتغيرات المؤقتة البديلة كأسماء للمتغيرات أو الدوال التي لا تحمل معنى مهم، وهي مشتقة من كلمة FUBAR، اختصارا إلى مصطلح في الجيش الأمريكي يعود إلى الحرب العالمية الثانية، والذي يشير إلى أن الموقف "لا يتطلّب حتى اعتراف". التنسيق المتعلق بحالة الحروف يستخدم المبرمجون طرقًا متعددة لتسمية المعرفات المكونة من عدة كلمات، لاسيما أن هذه الأسماء حساسة لحالة الحروف ولا يمكن أن تتضمن فراغات وفق قواعد بايثون. وإحدى هذه الطرق هي طريقة الثعبان مع الحروف الصغيرة snake_case، وفيها نفصل بين الكلمات في اسم المعرف الواحد بشرطة سفلية، والتي تبدو كثعبان متمدد بين الكلمات، وتملي هذه الطريقة على استخدام الحروف الصغيرة بالكامل، عدا في حالات التصريح عن الثوابت، إذ تُستخدم حينها طريقة الثعبان مع أحرف كبيرة UPPER_SNAKE_CASE في الغالب. أمّا طريقة سنام الجمل camelCase، ففيها نفصل بين الكلمات عن طريق كتابة أول حرف من كل كلمة بشكل كبير ما عدا أول كلمة، إذ تملي هذه الطريقة بأن يكون أول حرف من الكلمة الأولى صغيرًا، ويعود سبب تسميتها بهذا الاسم لتشابه شكل الحروف الكبيرة مع سنام الجمل. وطريقة باسكال PascalCase، والتي تُنسب تسميتها إلى استخدامها في لغة البرمجة باسكال، فهي مشابهة لطريقة camelCase إلا أن أول كلمة تبدأ أيضا بحرف كبير. إن حالة الحروف هي قضية تنسيقية تم تغطيتها في المقال السابق من هذه السلسلة. والطرق الأكثر استخدامًا في تسمية المعرفات هي نمط الثعبان snake_case وسنام الجمل camelCase، وكلاهما جيدتان ولا أفضلية لإحداهما على الأخرى طالما أنك ملتزم باستخدام إحداهما فقط في مشروعك كاملًا. اصطلاحات التسمية وفق دليل بايثون الإرشادي PEP 8 تقدم وثيقة PEP 8 بعضًا من التوصيات حيال اصطلاحات التسميات في بايثون، ومنها: كل الحروف يجب أن تكون وفق نظام الآسكي ASCII، أي حروف إنجليزية بحالتيها الكبيرة والصغيرة دون إشارات الحركات. يجب تسمية الوحدات بأسماء قصيرة وبحروف صغيرة. يجب تسمية الأصناف وفق طريقة PascalCase. يجب تسمية الثوابت وفق طريقة UPPER_SNAKE_CASE. يجب تسمية الدوال والتوابع والمتغيرات وفق طريقة snake_case بحروف صغيرة. يجب تسمية أول وسيط في التوابع باستخدام حروف صغيرة. يجب تسمية أول وسيط في تابع الصنف بالاسم cls وبحروف صغيرة. يجب أن تبدأ السمات الخاصة في الصنف دائما بعلامة الشرطة السفلية. يجب ألا تبدأ السمات العامة في الصنف بعلامة الشرطة السفلية أبدًا. يمكنك التغاضي عن هذه القواعد وفق الضرورات، فمثلًا وعلى الرغم من كون اللغة الإنجليزية هي المألوفة برمجيًا، إلا أنك تستطيع استخدام محارف أي لغة أخرى، فمثلًا 'コンピューター = 'laptop هي شيفرة برمجية صالحة نحويًا في بايثون، وكذلك يمكن استخدام اللغة العربية كما يلي: コンピューター = 'laptop' المنتج = 'laptop' أما عن الأسماء في هذه السلسلة هذا فقد اختيرت وفقًا للدليل PEP 8، إذ اعتمدنا طريقة camelCase تحديدًا وليس طريقة snake_case. ويتضمّن PEP 8 تنويهًا مفاده أن المبرمج ليس مضطرًا لاتباع هذه الاصطلاحات بصرامة، فالعامل الأهم للمقروئية هو وحدة الطريقة المستخدمة في التسمية في كامل البرنامج وليس الطريقة بحد ذاتها. كما يمكنك الاطلاع على قسم "اصطلاحات التسمية" في دليل PEP 8 كاملًا بزيارة python.org. اختيار الطول المناسب للاسم لعل من البديهي أن الأسماء يجب ألا تكون طويلة أو قصيرة جدًا، إذ أن الأسماء الطويلة ستكون مملة لكتابتها في كل مرة، في حين قد تكون الأسماء القصيرة غامضة ومحيرة، وبما أن الشيفرات تُقرأ أكثر بكثير مما تُكتب، فمن الأسلم الوقوع في خطأ استخدام الأسماء الطويلة لا القصيرة للمتغيرات، ولنلقي النظر فيما يلي على أمثلة لأسماء قصيرة جدًا وأخرى طويلة جدًا. أسماء قصيرة جدا أحد أكثر الأخطاء شيوعًا لدى اختيار الأسماء هو أن تكون قصيرة جدًا، ففي حين أنها قد تكون مفهومة وذات معنى لحظة كتابتها، إلا أن المعنى الدقيق لها سيُفقد بعد بضعة أيام أو أسابيع، إذ لن تتذكر ما كنت تقصده حينها، وفيما يلي سنستعرض بعضًا من أنماط الأسماء القصيرة. الأسماء المكونة من حرف أو اثنين، كالاسم g الذي يشير عادةً لكلمة تبدأ بهذا الحرف، وما أكثرها! فرغم كون الاختصارات والأسماء المكونة من حرف واحد أو حرفين سهلة للكتابة بالنسبة لك، إلا أنها صعبة القراءة والفهم لغيرك. الاختصارات، فاختصار مثل mon يشير إلى عدد كبير من الكلمات مثل monitor و month و monster وغيرها الكثير. الأسماء المكونة من كلمة واحدة قد تكون غامضة أيضًا، كالاسم start والذي يعني "بداية"، ولكن بداية ماذا؟ إذ يفتقد هذا النوع من الأسماء إلى السياق الواضح عند قراءة الشيفرة من قبل غير كاتبها. فبالنتيجة، قد تكون الأسماء المكونة من حرف أو اثنين والاختصارات والأسماء المكونة من كلمة واحدة مفهومة بالنسبة لك بوصفك كاتبها، ولكن عليك أن تتذكر بأن مبرمجًا آخر (أو حتى أنت نفسك بعد مرور بضعة أسابيع) سيواجه صعوبةً في فهم معناها. وفيما يلي بعض الاستثناءات التي يكون من المناسب فيها استخدام الأسماء القصيرة، فمثلًا من الشائع استخدام i كاسم لمتغير حلقة for التكرارية التي تمر على نطاق من الأعداد أو مؤشرات قائمة ما، كما من الشائع استخدام كلًا من الحرفين j و k (كونهما الحرفان التاليان للحرف i أبجديًا) في حال استخدامك لعدّة حلقات متداخلة، كما في المثال: >>> for i in range(10): ... for j in range(3): ... print(i, j) ... 0 0 0 1 0 2 1 0 --snip-- كما أنّ استخدام الحرفين x و y في تمثيل الإحداثيات الديكارتية يمثّل استثناءً آخر. وفيما عدا ذلك لا أنصح باستخدام أسماء بحرفٍ واحد على الإطلاق، فرغم كونها جذابة للاستخدام، كأن تستخدم الاسم w للدلالة على العرض width والاسم h للدلالة على الارتفاع height والاسم n للدلالة على عدد ما number، إلا أنها لن تكون واضحة ومفهومة للآخرين. لا تختصر حروفا من شيفرتك المصدرية كانت طريقة اختصار الأحرف من الكلمات شائعة الاستخدام في الفترة ما قبل تسعينيات القرن الماضي في لغة البرمجة C، كأن نستخدم الاسم memcpy للدلالة على نسخة ذاكرة memory copy أو strcmp للدلالة على المقارنة بين السلاسل النصية string compare، إلا أن هذه الطريقة تعد نمطًا غير مقروء من أنماط التسمية ويجب عدم استخدامها في أيامنا، فإن كان من الصعب نطق هذا الاسم، فما بالك بفهمه؟ لن يكون سهلًا بالطبع. كما يمكنك استخدام الجمل القصيرة في التسمية بغية زيادة مقروئية شيفرتك، كأن تستخدم الإنجليزية العادية، فمثلًا الاسم number_of_trials الذي يشير إلى عدد المحاولات، ذو مقروئية أعلى من الاسم number_trials. أسماء طويلة جدا عمومًا، كلما كان مجال الاسم أكبر زادت أهمية أن يكون توصيفيًا واضحًا، فمثلًا اسم قصير مثل payment بمعنى "المدفوعات" قد يكون كافيًا في حال استخدامه كاسم لمتغير محلي ضمن دالة واحدة وقصيرة، إلا أن نفس الاسم لن يكون واضحًا بما يكفي في حال استخدامه لمتغير عام ضمن شيفرة مكونة مثلًا من 10000 سطر برمجي. مثل هذا البرنامج الطويل قد يعالج العديد من البيانات المتعلقة بالمدفوعات وبالتالي لن يكون الاسم payment وحده ذو معنى، وفي هذه الحالة قد يكون اسم مثل salesClientMonthlyPayment للدلالة على مدفوعات مبيعات العملاء الشهرية أو annual_electric_bill_payment للدلالة على مدفوعات فواتير الكهرباء السنوية، أنسب لمثل هذه الحالات، إذ أن استخدام المزيد من الكلمات في الاسم يجعل السياق المقصود أوضح مُساهمًا في إزالة الغموض أو الالتباس. فمن المفضّل دائمًا اعتماد الأسماء التوصيفية، لا الأسماء الأقل قدرة على عكس مضمونها، مع وجود إرشادات تبين الحالات التي يكون فيها استخدام أسماء طويلة أمرًا ليس ذو أهمية. استخدام البادئات في الأسماء إن استخدام البادئات للأسماء قد يمثّل إضافة لتفصيل غير ضروري، فلو كان المتغير عبارة عن سمة لأحد الأصناف، فإن استخدام البادئة قد يوفّر معلوماتٍ لا نحتاج وجودها أصلًا ضمن اسم المتغير، فمثلًا لو كان لدينا صنف للقطط Cat ذو سمة لوزن القطة weight، فمن الجلي أن المقصود بالوزن هو وزن القطة، ففي مثل هذه الحالة، يعد استخدام اسم مثل catWeight زيادة غير ضرورية. ومن الممارسات المشابهة والتي قد عفا عليها الزمن هي اتباع الطريقة الهنغارية في ترميز الأسماء، والتي تنص على تضمين اختصار لنمط البيانات الذي سيتضمنه المتغير ضمن اسمه، فمثلًا وفق هذه الطريقة يشير اسم المتغير strName على أنّ هذا المتغير يتضمّن بيانات من النوع سلسلة نصية، والاسم iVacationDays يشير إلى أن المتغير يتضمّن بيانات من النوع عدد صحيح. أما في وقتنا الحاضر فإن اللغات الحديثة وبيئات التطوير المتكاملة قادرة على تزويد المبرمج بهذه المعلومات دون الحاجة لاستخدام البادئات، جاعلةً من الطريقة الهنغارية إجراءً غير ضروريًا، فإن كنت ممن اعتادوا تضمين نمط البيانات ضمن الأسماء، ابدأ بالتخلي عن هذه الطريقة. ولكن ومن ناحية أخرى، فإنّه من المفيد استخدام بادئات من قبل is بمعنى "أهو؟ أو هو يكون" و has بمعنى "لديه أو ذي" ضمن أسماء المتغيرات المُتضمنة لقيم منطقية أي من النوع Boolean، أو ضمن أسماء الدوال والتوابع التي تعيد قيمًا منطقية، لما لذلك من دور في زيادة المقروئية، فمثلًا في المثال التالي، لاحظ استخدام متغير باسم is_vehicle بمعنى "فإنه مركبة" وتابع باسم ()has_key بمعنى "يملك المفتاح": if item_under_repair.has_key('tires'): is_vehicle = True نلاحظ أن استخدام الأسماء السابقة قد دعمَ المقروئية بشكل كبير، جاعلًا الشيفرة قابلة للقراءة وكأنها مكتوبة باللغة الإنجليزية العادية، لتصبح الجملة بما معناه: "إذا كان العنصر الذي تتم صيانته يملك المفتاح المسمى 'إطارات'، فإنه من الصحيح كون هذا العنصر عبارة عن مركبة". وبشكل مشابه، فإن إضافة وحدات القياس لأسماء المتحولات قد يحسّن من مقروئية الشيفرات مضيفًا معلومات مفيدة، فمثلًا إن كان لدينا متغير يتضمن قيمة الكتلة محتويًا على بيانات من نوع الفاصلة العائمة، فقد يعتري القارئ بعض الغموض حول وحدة القياس المعتمدة، أهي الكيلوغرام أم الباوند أم الطن؟ وبما أن وحدات القياس هذه ليست من أنماط البيانات، فإن إضافة أي من kg أو lbs أو tons كبادئة أو لاحقة لا يعد اتباعًا للطريقة الهنغارية، ففي حال لم نكن نستخدم نمط بيانات خاص بالكتلة يتضمن معلومات حول وحدة قياسها، فمن المفضّل تسمية المتغير باسم من قبيل weight_kg. فمثلًا، فُقد مسبار المريخ الفضائي الآلي المداري عام 1999 بسبب اختلاف وحدات قياس الحسابات الناتجة عن البرمجيات التي أنتجتها شركة Lockheed Martin والتي استخدمت وحدات القياس البريطانية، وتلك الناتجة عن أنظمة وكالة الفضاء الدولية (ناسا) والتي استخدمت وحدات القياس المترية، ما أدى كنتيجة إلى تحديد المسار بشكل خاطئ، وخسائر قدرت بنحو 125 مليون دولار أمريكي. اللاحقات الرقمية المتتالية إن استخدامك للاحقات الرقمية المتتالية في أسماء المعرفات يشير عادةً إلى أحد أمرين، إما أنك بحاجة لتغيير نمط بيانات المتغير المسمّى بهذه الطريقة، أو أن الاسم بحاجة إلى المزيد من التفصيل، فالأرقام وحدها لا تقدّم ما يكفي من معلوماتٍ كافية للتمييز بين الأسماء. فمثلًا، لدى اختيار أسماء للمتغيرات من قبيل payment1 وpayment1 وpayment3، دلالةً على عدة مدفوعات، فإن هذه الأسماء لا تقدم لقارئ الشيفرة أي معلومات حول الفرق ما بين قيم المدفوعات، والإجراء الأفضل في مثل هذه الحالة هو استبدال المتغيرات الثلاث ببنية معطيات واحدة كقائمة أو صف باسم payments متضمنةً لثلاث قيم. كذلك الأمر بالنسبة للدوال ذات الأسماء من قبيل (makePayment1(amount و(makePayment2(amount وهكذا، والتي يجب استبدالها بدالة وحيدة تقبل وسيطًا من النوع عدد صحيح، ليصبح استدعاؤها بالشكل: (makePayment(1, amount و(makePayment(2, amount وهكذ. أما في حال كون تلك الدوال ذات وظائف مختلفة، بما يفرض استخدام دوال مستقلة، فمن الحري بنا في هذه الحالة استبدال الرقم في الاسم بما يوضح دور الدالة، مثل: (makeLowPriorityPayment(amount لتدل على المدفوعات ذات الأولوية الأقل (makeHighPriorityPayment(amount للدلالة على المدفوعات ذات الأولوية الأعلى (make1stQuarterPayment(amount للدلالة على مدفوعات الربع الأول من السنة (make2ndQuarterPayment(amount للدلالة على مدفوعات الربع الثاني من السنة أما في حال وجود سبب وجيه لاستخدام أسماء بلاحقات رقمية متتالية فلا بأس في ذلك، وإن كان ذلك فقط من باب الاستسهال فعليك العمل على تغييرها. استخدم أسماء يسهل البحث عنها لابد وأنك تستخدم الاختصار CTRL-F ضمن المحرر أو بيئة التطوير المتكاملة باحثًا عن أماكن الإشارة إلى متغير أو دالة ما ضمن شيفرتك، باستثناء حالة كون الشيفرة صغيرة، ولكن ماذا لو كان هذا الاسم قصيرًا وعامًا، من قبيل num أو a، فسينتهي البحث مع العديد من التطابقات الخاطئة، لذا وبغية جعل الاسم يسهل إيجاده مباشرةً، ابتكر أسماءً فريدة بجعلها أطول ومتضمنةً بعض التفاصيل. تمتلك بعض بيئات التطوير المتكاملة ميزات إعادة بناء الأسماء القادرة على التمييز بين الأسماء وفقًا لطريقة استخدامها ضمن البرنامج، ولعل أداة إعادة التسمية "rename" هي إحدى هذه الميزات الشائعة، والقادرة على التمييز بين المتغيرات المسماة مثلًا num وnumber، أو بين متغير محلي باسم num وآخر عام بنفس الاسم، ومع ذلك تبقى مهمة اختيار الأسماء من مسؤولياتك كما لو كانت هذه الميزات غير موجودة أصلًا. فمع التزامك بهذه القواعد، سيصبح اختيار أسماء توصيفية دقيقة بدلًا من تلك العامة المختصرة أمرًا بديهيًا بالنسبة لك، فلا تقتصر أهمية اختيار الأسماء المناسبة على تحسين المقروئية وحسب، بل تجعل من مهمة البحث عن المتغير ضمن الشيفرة أمرًا أسهل، فاختيارك مثلًا للاسم email سيكون مبهمًا، في حين أن إضافة بعض التفاصيل من قبيل emailAddress أو downloadEmailAttachment أو emailMessage أو replyToAddress سيكون أفضل بكثير لكلا الجانبين آنفي الذكر. تجنب النكات والتورية والإشارات الثقافية خلال عملي -يقول المؤلف- في إحدى شركات تطوير البرمجيات، صادفتني دالة باسم ()gooseDownload، ولم يكن لدي أي تصور حول ما تعنيه، "تنزيل الإوزة!" إذ لم يكن للمنتج الذي نعمل عليه أي صلة بالطيور أو تحميل الطيور، ولدى إيجادي لشريك العمل الأقدم الذي كتب هذه الدالة، شرح لي بأن كلمة إوزة هنا يُقصد بها فعل وليس اسم، كما في الجملة goose the engine (بمعنى تهيئة محرك السيارة بإعطاءه دفعة كبيرة من الوقود مُصدرًا صوتًا يشبه صوت الإوزة). لم تكن لدي أدنى فكرة حيال ما تعنيه هذه الجملة، ما اضطره لأن يشرح لي بأن تنزيل الإوزة يقصد بها هيّء المحرك، وهو مصطلح في لغة السيارات يعني الضغط لأسفل على دواسة الوقود لجعل المحرك يعمل أسرع، بالمقابل كان الهدف من هذه الدالة جعل التنزيلات أسرع، أومأتُ مؤيدًا حينها وعدت إلى مكتبي، وبعد عدة سنوات إذ غادر هذا الشخص الشركة، كان أول ما فعلته هو تغيير اسم دالته لتصبح ()increaseDownloadSpeed بمعنى زيادة سرعة التنزيل. فقد تميل باللاشعور خلال اختيار الأسماء في برامجك إلى استخدام النكات أو التورية أو بعض الإشارات الثقافية لإضافة بعض الخفة لشيفرتك، يجب عدم القيام بذلك، إذ قد يصعب فهم وجه النكتة نصيًا وغالبًا ما ستكون غير طريفة مستقبلًا، كذلك الأمر بالنسبة للتورية، إذ من السهل عدم ملاحظة المقصود منها، لتتعامل مع تقارير أخطاء متكررة من شركائك في العمل ممن ظنوا بأن توريتك ما هي سوى خطأ طباعي. وبالمثل، قد تقف الإشارات الثقافية لدى التسمية عائقًا أمام فهم مقاصد الشيفرة بوضوح، ولا سيما في أيامنا هذه، إذ جعل الإنترنت مشاركة الشيفرات مع الأشخاص حول العالم أسهل من أي وقتٍ مضى، ممن قد لا يكونوا بالضرورة طليقين باللغة الإنجليزية أو قادرين على استيعاب نكاتها، فمثلًا كما أشرنا سابقًا في هذا المقال إلى استخدام أسماء من قبيل spam و eggs في توثيقات بايثون إشارةً إلى المشهد التمثيلي العائد لفرقة مونتي بايثون، إذ من المقبول استخدامها في الأسماء المؤقتة للمتغيرات فقط، في حين لا يوصى باستخدامها في الشيفرات الفعلية. ولعل الإجراء الأفضل هو كتابة شيفرتك بطريقة تجعل أي متحدث غير ناطق بالإنجليزية يفهمها بسهولة، فلتكن طريقتك مهذبة ومباشرة وبعيدة عن روح الدعابة، فبلا شك قد ظن زميلي السابق في العمل أن طرفته حول الإوزة ()gooseDownload مضحكة، ولكن في الواقع العدو الأكبر للنكات؛ شرحها. لا تستخدم الأسماء المحجوزة يجب عليك ألا تستخدم كلمات بايثون المحجوزة كأسماء لمتغيراتك، فمثلًا لو اخترت أحد الاسمين set أو list لأحد المتغيرات، فإنك قد استخدمت أسماءً محجوزة للدوال ()set و()list في بايثون، ما قد يؤدي لظهور أخطاء لاحقًا في شيفرتك، ففي حين أن الدالة ()list مسؤولة عن إنشاء القوائم، إلا أن استخدام اسمها المحجوز لغير هذا الغرض سيؤدي لظهور أخطاء، كما في المثال: >>> list(range(5)) [0, 1, 2, 3, 4] 1 >>> list = ['cat', 'dog', 'moose'] 2 >>> list(range(5)) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'list' object is not callable``` فلو أسندنا قيمة قائمة إلى متغير باسم list، فإننا نخسر بذلك الدالة الأصلية ()list، وبمجرد استدعاء هذه الدالة سنحصل على خطأ TypeError. ولمعرفة فيما إذا كان اسمًا معينًا محجوزًا في بايثون أم لا، يكفي كتابته ضمن الصدفة التفاعلية أو أن نحاول استيراده، فلو حصلنا على أحد الخطأين NameError أو ModuleNotFoundError، فهذا يعني أن الاسم غير محجوز، فمثلًا الأسماء open و test محجوزة في بايثون، في حين spam و eggs ليست كذلك: >>> open <built-in function open > >>> import test >>> spam Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'spam' is not defined >>> import eggs Traceback (most recent call last): File "<stdin>", line 1, in <module> ModuleNotFoundError: No module named 'eggs' ومن أشهر الكلمات المحجوزة التي يستخدمها المبرمجون خطأً هي: all any data email file format hash id input list min max object open random set str sum test type فلا تستخدم هذه الأسماء للمعرفات بأنواعها. ومن الأخطاء الشائعة أيضًا، تسمية ملفات بايثون (ذات اللاحقة .py) بنفس أسماء الوحدات الخارجية، فعلى سبيل المثال في حال تثبيتك للوحدة الخارجية Pyperclip مع وجود ملف بايثون بنفس الاسم أي pyperclip.py، فإن تعليمة استيراد pyperclip ستسورد ملف بايثون صاحب هذا الاسم بدلًا من الوحدة الخارجية، وفي حال استدعائك لإحدى دوال الوحدة pyperclip كالدالة ()copy أو ()paste فستظهر رسالة خطأ مفادها أن هذه الدوال غير موجودة. >>> # Run this code with a file named pyperclip.py in the current folder. >>> import pyperclip # This imports your pyperclip.py, not the real one. >>> pyperclip.copy('hello') Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: module 'pyperclip' has no attribute 'copy' فكن حريصًا على عدم استخدام الكلمات المحجوزة في شيفراتك، وهو احتمال وارد لدى ظهور رسائل أخطاء غير متوقعة وغير مرتبطة بسمة معينة. أسوأ أسماء المتغيرات على الإطلاق يعد الاسم data اسمًا مريعًا للمتغيرات، فما من متغير إلا ويحتوي على بيانات "data"، الأمر نفسه ينطبق على استخدام الاسم var بما يشبه تسميتك لقطتك الأليفة بالاسم "قطة"، كما يعد الاسم temp شائعًا للمتغيرات المتضمنة لبيانات مؤقتة، إلا أنه لا يعد خيارًا جيدًا، فمن وجهة نظر تأملية كل المتغيرات مؤقتة. وفي الواقع يكثر استخدام هذه الأسماء الآنفة الذكر رغم غموضها، لذا تجنب استخدامها في شيفراتك. فلطفًا لو احتجت لمتغير يعبّر عن التباين الاحصائي لبيانات درجة الحرارة، اختر الاسم temperatureVariance غير مباليًا بدلالات الثلاثي الشهير tempVarData. الخاتمة رغم كون اختيار الأسماء ليس ذو أثرٍ مباشر على الخوارزميات أو علوم الحاسوب، إلا أنه أمر ذو دور حيوي في كتابة شيفرات عالية المقروئية. وبالنتيجة فإن اختيار الأسماء في شيفراتك هو أمر عائد إليك أولًا وأخيرًا، ومع ذلك كن متيقظًا للإرشادات العديدة حول هذا الأمر، إذا يوصي الدليل PEP 8 بالعديد من الاصطلاحات، كاستخدام الأحرف الصغيرة لأسماء الوحدات وطريقة PascalCase لأسماء الأصناف، كما أن الأسماء لا يجب أن تكون قصيرة أو طويلة جدًا، ومع ذلك يبقى الخطأ باتجاه الأسماء الأطول الأكثر تفصيلًا أفضل من استخدام أسماء قصيرة مبهمة. فالاسم يجب أن يكون مختصرًا مفيدًا، وتعد سهولة إيجاد المتغير لدى البحث عنه باستخدام الاختصار CTRL-F إشارةً لدقة اسم المتغير وفرادته، فدائمًا أثناء اختيار اسم ما فكر بدرجة قابلية إيجاده بسهولة عند البحث عنه كمعيار لكونك تبالغ باختصاره، وضع في الحسبان دومًا مدى قدرة مبرمج غير طليق بالإنجليزية على فهم الاسم، مُبتعدًا عن استخدام النكات والتوريات والإشارات الثقافية، بل اختر دائمًا أسماء مهذبة ومباشرة بعيدة عن روح الدعابة. ورغم كون العديد من الاقتراحات الواردة في هذا المقال لا تتعدى كونها إرشادات، إلا أنه من الضروري تجنب استخدام الأسماء المحجوزة في بايثون ومكتباتها المعيارية فاستخدامها قد يؤدي إلى ظهور أخطاء مبهمة في شيفرتك. لا يهم الحواسيب كون الأسماء معبرة أم مبهمة، وكل ما في الأمر أن الاختيار الدقيق لهذه الأسماء يجعل من الشيفرات أسهل للقراءة والفهم للبشر وليست أسهل للتنفيذ من وجهة نظر الحاسوب، فالمقروئية العالية لشيفراتك تجعلها أسهل للفهم، وبالتالي أسهل للتعديل، ما يجعلها بدوره أسهل من ناحية إصلاح الأخطاء وإضافة الميزات الجديدة، ولعل استخدام أسماء مفهومة يمثل خطوة أساسية خلال إنشاء البرمجيات عالية الجودة. ترجمة -وبتصرف- للفصل الرابع Code Formatting With Black من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigarti. اقرأ أيضًا مدخل إلى لغة بايثون البرمجية أساسيات البرمجة بلغة بايثون التعامل مع القوائم والسلاسل النصية في لغة بايثون اكتشاف دلالات الأخطاء في شيفرات لغة بايثون
-
يُعد فلاسك إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا عدة أدوات وميزات من شأنها إنشاء تطبيقات ويب في لغة بايثون. أمّا SQLAlchemy، فهي أداةٌ في محرك قواعد البيانات SQL تؤمن وصولًا فعالًا وعالي الأداء إلى قواعد البيانات العلاقية، كما توفّر طرقًا للتخاطب مع العديد من محركات قواعد البيانات، مثل SQLite و MySQL و PostgreSQ، مانحةً إمكانية الوصول إلى آليات SQL الخاصة بقواعد البيانات، كما توفّر رابط الكائنات بالعلاقات Object Relational Mapper -أو اختصارًا ORM- الذي يتيح إمكانية إنشاء الاستعلامات والتعامل مع البيانات باستخدام توابع وكائنات بسيطة في بايثون. تعدّ Flask-SQLAlchemy إضافة لفلاسك تسهّل من استخدام SQLAlchemy ضمن فلاسك مؤمنةً الأدوات والوسائل المناسبة للتعامل مع قاعدة البيانات في تطبيقات فلاسك من خلال SQLAlchemy. تُعرَّف علاقة قاعدة البيانات متعدّد-إلى-متعدّد many-to-many على أنها علاقةٌ بين جدولين، إذ يمكن لأي سجلٍ من الجدولين الارتباط مع عدّة سجلات من الآخر، فمثلًا في تطبيق مدونة يمكن لجدول التدوينات posts أن يرتبط بعلاقة من نوع متعدّد-إلى-متعدّد مع جدول أسماء المؤلفين (كُتَّاب التدوينات)، بمعنى أن كل تدوينة قد ترتبط بعدّة أسماء مؤلفين، وأيضًا قد يرتبط كل اسم مؤلف بعدّة تدوينات، وبالتالي فإنّ العلاقة ما بين التدوينات وأسماء المؤلفين هي علاقة متعدّد-إلى-متعدّد. أيُّ تطبيقٍ آخر من تطبيقات التواصل الاجتماعي هو مثالٌ آخر، إذ أن كل منشور قد يحتوي عدّة إشارات مرجعية، وكل إشارة مرجعية قد تتضمّن عدة منشورات. سنعمل في هذا المقال على تعديل تطبيق مبني باستخدام كل من فلاسك والإضافة SQLAlchemy فيه، وذلك بإضافة علاقة من نوع متعدّد-إلى-متعدّد إليه، إذ سننشئ علاقة ما بين التدوينات والوسوم tags، بحيث يمكن لكل تدوينة أن تتضمّن عدّة وسوم، ويمكن أن يُشار للوسم الواحد في عدّة تدوينات. يمكنك قراءة هذا المقال مُنفصلًا، إلّا أنّه تكملة لمقالنا السابق كيفية استخدام SQLAlchemy في فلاسك، وفيه قد بنينا قاعدة بيانات مُتعدّدة الجداول بعلاقة من نواع واحد إلى مُتعدّد one-to-many ما بين التدوينات والتعليقات عليها وذلك ضمن تطبيق مدونة. سيتضمّن تطبيقك -مع نهاية هذا المقال- ميزةً جديدة تتمثّل بإمكانية إضافة الوسوم إلى التدوينات، بحيث يُشار إلى التدوينة الواحدة في عدّة وسوم، لتعرض صفحة الوسم الواحد كافّة التدوينات المُشار إليها من خلاله. مستلزمات العمل قبل المتابعة في هذا المقال لا بُدّ من: توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو "flask_app". الفهم الجيد لأساسيات فلاسك، مثل مفهوم الوجهات ودوال العرض، وفي هذا الصدد يمكنك الاطلاع على المقالين كيفية بناء موقعك الإلكتروني الأول باستخدام إطار عمل فلاسك Flask من لغة بايثون وكيفية استخدام القوالب في تطبيقات فلاسك Flask لفهم مبادئ فلاسك. فهم أساسيات لغة HTML. (مُتطلّب اختياري) في الخطوة الأولى ستنسخ تطبيق المدونة الذي ستعمل عليه في هذا المقال، ولكن يمكنك بناءه من الصفر بدلًا من نسخه باتباعك للخطوات الواردة في المقال السابق، كما يمكنك الوصول إلى الشيفرة الكاملة للتطبيق من flask-slqa-bloggy. الخطوة 1 - إعداد تطبيق الويب سنعمل في هذه الخطوة على إعداد تطبيق المدونة ليكون جاهزًا للتعديل عليه، كما سنستعرض نماذج قاعدة بيانات Flask-SQLAlchemy ووجهات فلاسك لضمان فهمك الجيد لبنية التطبيق. يمكنك تجاوز هذه الخطوة في حال اتباعك للمقال السابق المذكور آنفًا في فقرة مستلزمات العمل واحتفاظك بالشيفرة والبيئة الافتراضية على حاسوبك. سنستخدم شيفرة التطبيق المبني في المقال السابق (راجع فقرة مستلزمات العمل) لشرح آلية إضافة علاقة من النوع متعدّد-إلى-متعدّد لتطبيق ويب مبني باستخدام فلاسك مع الإضافة Flask-SQLAlchemy؛ وهذا التطبيق هو مدونة يتيح إمكانية إضافة وعرض التدوينات والتعليق عليها، إضافةً إلى إمكانية قراءة التعليقات الموجودة وحذفها. سننسخ الآن ملف الشيفرة من المستودع ونعيد تسميته من "flask-slqa-bloggy" ليصبح "flask_app" وذلك باستخدام الأمر التالي: user@localhost:$ git clone https://github.com/do-community/flask-slqa-bloggy flask_app ننتقل الآن إلى الملف "flask_app" على النحو التالي: user@localhost:$ cd flask_app ثمّ سننشئ بيئة افتراضية جديدة: user@localhost:$ python -m venv env ونفعّل هذه البيئة على النحو التالي: user@localhost:$ source env/bin/activate ونثبّت الآن كلًا من فلاسك والإضافة Flask-SQLAlchemy باستخدام الأمر: (env)user@localhost:$ pip install Flask Flask-SQLAlchemy ثمّ نضبط متغيرات البيئة التالية: (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development يشير متغيّر البيئة FLASK_APP إلى التطبيق الذي نعمل على تطويره حاليًا، وهو "app.py" في حالتنا، أمّا المتغير FLASK_ENV فيحدّد وضع التشغيل، وقد اخترناه ليكون وضع التطوير development الذي يوفّر إمكانية تنقيح الأخطاء في التطبيق، مع الانتباه إلى عدم استخدام وضع التشغيل هذا في بيئة الإنتاج. والآن، سنفتح صَدَفَة فلاسك لإنشاء جداول قاعدة البيانات: (env)user@localhost:$ flask shell ومن ثمّ سنستورد الكائن db الخاص بقاعدة بيانات Flask-SQLAlchemy، بالإضافة إلى النموذج post الخاص بالتدوينات، والنموذج Comment الخاص بالتعليقات، لننشئ بعدها جداول قاعدة البيانات الموافقة للنماذج السابقة باستخدام الدالة ()db.create_all، كما يلي: >>> from app import db, Post, Comment >>> db.create_all() >>> exit() ومن ثمّ سنملأ قاعدة البيانات باستخدام البرنامج من الملف "init_db.py": (env)user@localhost:$ python init_db.py والذي سيضيف ثلاث تدوينات وأربعة تعليقات إلى قاعدة البيانات. نشغّل الآن خادم التطوير: (env)user@localhost:$ flask run الآن وبالذهاب إلى المتصفّح، يصبح من الممكن الوصول إلى التطبيق عبر الرابط: http://127.0.0.1:5000/ فتظهر صفحة شبيهة بما يلي: وفي حال ظهور خطأ، تأكّد من اتباعك الخطوات السابقة على النحو الصحيح. يمكنك إيقاف تشغيل خادم التطوير بالضغط على مفتاحي "CTRL+C" في لوحة المفاتيح. فيما يلي سنمر على نماذج قاعدة البيانات Flask-SQLAlchemy بغية فهم العلاقات الحالية ما بين الجداول فيها، فإذا كانت محتويات الملف "app.py" مألوفةً يالنسبة لك من المقال السابق، فيمكنك تجاوز هذه الخطوة. نفتح الآن الملف "app.py": (env)user@localhost:$ nano app.py فتظهر محتوياته على النحو التالي: import os from flask import Flask, render_template, request, redirect, url_for from flask_sqlalchemy import SQLAlchemy basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] =\ 'sqlite:///' + os.path.join(basedir, 'database.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100)) content = db.Column(db.Text) comments = db.relationship('Comment', backref='post') def __repr__(self): return f'<Post "{self.title}">' class Comment(db.Model): id = db.Column(db.Integer, primary_key=True) content = db.Column(db.Text) post_id = db.Column(db.Integer, db.ForeignKey('post.id')) def __repr__(self): return f'<Comment "{self.content[:20]}...">' @app.route('/') def index(): posts = Post.query.all() return render_template('index.html', posts=posts) @app.route('/<int:post_id>/', methods=('GET', 'POST')) def post(post_id): post = Post.query.get_or_404(post_id) if request.method == 'POST': comment = Comment(content=request.form['content'], post=post) db.session.add(comment) db.session.commit() return redirect(url_for('post', post_id=post.id)) return render_template('post.html', post=post) @app.route('/comments/') def comments(): comments = Comment.query.order_by(Comment.id.desc()).all() return render_template('comments.html', comments=comments) @app.post('/comments/<int:comment_id>/delete') def delete_comment(comment_id): comment = Comment.query.get_or_404(comment_id) post_id = comment.post.id db.session.delete(comment) db.session.commit() return redirect(url_for('post', post_id=post_id)) وفيه لدينا نموذجي قاعدة بيانات يمثلان جدولين، هما: الجدول Post: والذي يتضمّن عمودًا لكل من معرّف التدوينة وعنوانها ومضمونها، إضافةً إلى علاقة من النوع واحد-إلى-مُتعدّد مع جدول التعليقات. الجدول Comment: والذي يتضمّن عمودًا لكل من معرّف التعليق ومحتواه، إضافةً إلى عمود لمعرّف التدوينة post_id للإشارة إلى التدوينة التي يتبع لها التعليق. كما يتضمّن الملف بعد النموذجين كل من الوجهات التالية: /: الصفحة الرئيسية للتطبيق، والتي تعرض كافّة التدوينات الموجودة في قاعدة البيانات. /<int:post_id>/: الصفحة الخاصّة بكل تدوينة، فمثلًا يعرض الرابط "/http://127.0.0.1:5000/2" تفاصيل التدوينة الثانية من قاعدة البيانات مع التعليقات عليها. /comments/: صفحة لعرض كافّة التعليقات من قاعدة البيانات مع روابط التدوينات التي يتبع لها كل تعليق. comments/<int:comment_id>/delete/: وجهة مُخصّصة لحذف التعليقات باستخدام زر أوامر باسم حذف تعليق Delete Comment. نغلق الآن الملف app.py. سنستخدم في الخطوة التالية علاقة من نوع مُتعدّد-إلى-مُتعدّد للربط بين الجدولين المذكورين سابقًا. الخطوة 2 - إعداد نماذج قاعدة بيانات لإنشاء علاقة من نوع متعدد-إلى-متعدد سنضيف في هذه الخطوة نموذج قاعدة البيانات المُمثّل لجدول الوسوم، والذي سنربطه مع جدول التدوينات الحالي باستخدام "جدول ارتباط association table"، والذي يعرّف بأنّه جدول وسيط يربط بين جدولين بعلاقة مُتعدّد-إلى-مُتعدّد. تربط العلاقة مُتعدّد-إلى-مُتعدّد بين جدولين، بحيث يتعلّق كل عنصر من أحدهما بعدّة عناصر من الآخر، وتشير كل تدوينة في جدول الارتباط إلى وسومها، كما يشير كل وسم إلى كافّة التدوينات المُتضمّنة له. سنضيف أيضًا في هذه الخطوة بعضًا من التدوينات والوسوم إلى قاعدة البيانات، لنتمكن لاحقًا من عرض التدوينات مع وسوم كل منها أو الوسوم مع التدوينات المُتضمّنة لكل منها. ليكن لدينا جدول تدوينات بسيط على النحو التالي: Posts +----+-----------------------------------+ | id | content | +----+-----------------------------------+ | 1 | A post on life and death | | 2 | A post on joy | +----+-----------------------------------+ وجدولًا للوسوم كما يلي: Tags +----+-------+ | id | name | +----+-------+ | 1 | life | | 2 | death | | 3 | joy | +----+-------+ يمكننا مثلًا إضافة كل من الوسمين "life" و "death" إلى التدوينة "A post on life and death" عن طريق إضافة عمود جديد إلى جدول التدوينات، ليصبح بالشّكل: Posts +----+-----------------------------------+------+ | id | content | tags | +----+-----------------------------------+------+ | 1 | A post on life and death | 1, 2 | | 2 | A post on joy | | +----+------------------------------------------+ لن تعمل هذه المنهجية لأنّ كل عمود يجب أن يتضمّن قيمة واحدة، وفي حال وجود عدّة قيم للعمود الواحد من السجل، ستصبح العمليات الأساسية مثل إضافة البيانات وتحديثها مرهقة وبطيئة، وسنستخدم بدلًا من ذلك جدولًا إضافيًا ثالثًا ليشير إلى المفاتيح الأساسية ويربط بينها وبين الجداول المرتبطة، وهو ما ندعوه بجدول الارتباط أو جدول الصلة join table، والذي سيخزّن معرّفات كل عنصر من كل جدول. وفيما يلي مثال لجدول ارتباط يربط بين جدولي التدوينات والوسوم: post_tag +----+---------+-------------+ | id | post_id | tag_id | +----+---------+-------------+ | 1 | 1 | 1 | | 2 | 1 | 2 | +----+---------+-------------+ ترتبط التدوينة ذات المعرّف رقم "1" (وهي "A post on life and death") مع الوسم ذي المعرّف رقم "1" (وهو "life") في السجل الأوّل من الجدول السابق؛ أمّا في السجل الثاني، فترتبط التدوينة الأولى نفسها أيضًا مع الوسم ذو المعرّف رقم "2" (وهو "death")، ما يعني أنّ التدوينة الأولى تتضمّن كلا الوسمين "life" و "death"، ويمكن ربط كل تدوينة بوسومات متعدّدة بنفس الآلية. سنعدّل الآن الملف app.py لإضافة نموذج قاعدة بيانات جديد ليمثّل الجدول الذي سنستخدمه لتخزين الوسوم، كما سنضيف جدول ارتباط باسم "post_tag" لربط التدوينات بالوسوم. لذا، سنفتح بدايةً الملف app.py بغية إنشاء العلاقة ما بين التدوينات والوسوم: (env)user@localhost:$ nano app.py سنضيف الآن جدول post_tag ونموذج باسم Tag بعد كائن قاعدة البيانات db وقبل النموذج Post، ومن ثمّ سنضيف للنموذج Post عمودًا علاقيًا زائفًا للوسوم باسم tags لنمكّن لاحقًا من الوصول إلى وسوم التدوينة باستخدام التعليمة post.tags أو الوصول إلى التدوينات المُتضمّنة وسمًا ما باستخدام التعليمة tag.posts. # ... db = SQLAlchemy(app) post_tag = db.Table('post_tag', db.Column('post_id', db.Integer, db.ForeignKey('post.id')), db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')) ) class Tag(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50)) def __repr__(self): return f'<Tag "{self.name}">' class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100)) content = db.Column(db.Text) comments = db.relationship('Comment', backref='post') tags = db.relationship('Tag', secondary=post_tag, backref='posts') def __repr__(self): return f'<Post "{self.title}">' احفظ الملف واغلقه. استخدمنا في الشيفرة السابقة الدالة ()db.Table لإنشاء جدول بعمودين، إذ أنّ الإجراء الأفضل بالنسبة لجدول الارتباط هو استخدام جدول بدلًا من نموذج قاعدة بيانات، وهنا يمتلك الجدول post_tag عمودين يمثلان مفتاحين خارجيين foreign keys أي المفتاحين المستخدمين للإشارة إلى أعمدة المفاتيح الأساسية في الجداول الأصل (المفتاح الأساسي لجدول ما هو مفتاح أجنبي عند تواجده في جدول آخر): post_id: وهو مفتاح خارجي من نوع عدد صحيح يمثّل معرّف التدوينة ويشير إلى عمود المُعرّف ID في جدول التدوينات post. tag_id: وهو مفتاح خارجي من نوع عدد صحيح يمثّل معرّف الوسم ويشير إلى عمود المُعرّف ID في جدول الوسوم tag. ستُنشئ هذه المفاتيح العلاقات ما بين الجداول. وبعد الانتهاء من الجدول post_tag، أنشأنا النموذج Tag الممثّل للجدول الذي سنخزّن ضمنه الوسوم، ويحتوي هذا الجدول على عمودين: id: معرّف الوسم. name: اسم الوسم. استخدمنا اسم الوسم ضمن التابع الخاص ()__repr__ بغية إعطاء كل كائن وسم تمثيلًا محرفيًا واضحًا لأسباب تنقيحية، كما أضفنا متغير صنف للوسوم باسم tags ضمن النموذج Post، وذلك باستخدام التابع ()db.relationship ممررين إليه اسم نموذج الوسوم، وهو Tag في حالتنا. مرّرنا جدول الارتباط post_tag إلى المعامل الثاني secondary من التابع بغية إنشاء علاقة من نوع مُتعدّد-إلى-مُتعدّد ما بين التدوينات والوسوم. استخدمنا المعامل backref لإضافة مرجعٍ عكسي back reference يلعب ما يشبه دور العمود في النموذج Tag، وبذلك يصبح من الممكن الوصول تدوينات الوسم المُحدّد عبر التعليمة tag.posts ووسوم تدوينة ما باستخدام التعليمة post.tags، وسنعرض مثالًا يوضّح هذه الفكرة لاحقًا. أمّا الآن فسنحرّر برنامج بايثون "init_db.py" لتعديل قاعدة البيانات بإضافة كل من جدول الارتباط post_tag وجدول الوسوم المبني استنادًا إلى نموذج الوسوم Tag، على النحو التالي: (env)user@localhost:$ nano init_db.py ونعدّل الملف ليصبح كما يلي: from app import db, Post, Comment, Tag db.drop_all() db.create_all() post1 = Post(title='Post The First', content='Content for the first post') post2 = Post(title='Post The Second', content='Content for the Second post') post3 = Post(title='Post The Third', content='Content for the third post') comment1 = Comment(content='Comment for the first post', post=post1) comment2 = Comment(content='Comment for the second post', post=post2) comment3 = Comment(content='Another comment for the second post', post_id=2) comment4 = Comment(content='Another comment for the first post', post_id=1) tag1 = Tag(name='animals') tag2 = Tag(name='tech') tag3 = Tag(name='cooking') tag4 = Tag(name='writing') post1.tags.append(tag1) # Tag the first post with 'animals' post1.tags.append(tag4) # Tag the first post with 'writing' post3.tags.append(tag3) # Tag the third post with 'cooking' post3.tags.append(tag2) # Tag the third post with 'tech' post3.tags.append(tag4) # Tag the third post with 'writing' db.session.add_all([post1, post2, post3]) db.session.add_all([comment1, comment2, comment3, comment4]) db.session.add_all([tag1, tag2, tag3, tag4]) db.session.commit() احفظ الملف واغلقه. استوردنا في الشيفرة السابقة النموذج Tag، كما حذفنا كل محتويات قاعدة البيانات باستخدام الدالة ()db.drop_all بغية إضافة كل من جدول الوسوم والجدول post_tag بأمان، متجنبين بذلك وقوع أي من المشاكل الشائعة التي قد تحدث لدى إضافة جداول جديدة إلى قاعدة البيانات. أنشأنا بعد ذلك جميع الجداول مُجدّدًا باستخدام الدالة ()db.create_all. يمكننا -بعد تصريح الشيفرة من المقال السابق عن التدوينات والتعليقات- استخدام نموذج الوسوم Tag لإنشاء أربعة وسوم، ومن ثم إضافة الوسوم إلى التدوينات باستخدام السمة tags، التي أُضيفت عبر السطر البرمجي: tags = db.relationship('Tag', secondary=post_tag, backref='posts') من الملف app.py. كما خصّصنا الوسوم للتدوينات باستخدام تابع الإسناد ()append الشبيه بقوائم بايثون. أُضيفت بعد ذلك الوسوم المُنشأة إلى جلسة قاعدة البيانات باستخدام الدالة ()db.session.add_all. ملاحظة: لا تعيد الدالة ()db.create_all إنشاء جدول موجود أصلًا ولا تحدثّه، فعلى سبيل المثال، لو عدّلنا أحد النماذج بإضافة عمود جديد إليه، ثمّ استخدمنا الدالة ()db.create_all فلن يُطبَّق هذا التغيير على الجدول في حال كان هذا الجدول موجود أصلًا. ويكون الحل بحذف كافّة الجداول الموجودة في قاعدة البيانات باستخدام الدالة ()db.drop_all ومن ثمّ إعادة إنشائها باستخدام الدلة ()db.create_all كما هو موضّح في الملف "init_db.py". تضمن هذه الآلية تطبيق التغييرات المُنفّذة على النماذج إلّا أنّها ستتسبب بحذف كافّة البيانات الموجودة في الجداول. من الممكن تهجير ملف تخطيط قاعدة البيانات Schema migration بغية تحديث قاعدة البيانات مع الحفاظ على البيانات الموجودة ضمنها، وهذا التهجير سيسمح بتعديل الجداول مع الحفاظ على البيانات فيها، إذ يمكننا استخدام الإضافة Flask-Migrate لإجراء ترحيل لملف تخطيط قاعدة بيانات SQLAlchemy عن طريق واجهة سطر أوامر فلاسك. سنشغّل الآن البرنامج init_db.py لتطبيق التغييرات على قاعدة البيانات: (env)user@localhost:$ python init_db.py يُفترض في هذه المرحلة أن يُنفّذ البرنامج بنجاح دون ظهور أي خرج، أمّا في حال ظهور رسالة خطأ، فتأكّد من كونك قد أجريت التغييرات الصحيحة على الملف init_db.py. ولإلقاء نظرة على التدوينات والوسوم الموجودة حاليًا في قاعدة البيانات، سنفتح صَدَفَة فلاسك: (env)user@localhost:$ flask shell ثمّ سننفّذ الشيفرة التالية التي ستمر على كافّة التدوينات والوسوم: from app import Post posts = Post.query.all() for post in posts: print(post.title) print(post.tags) print('---') استوردنا في الشيفرة السابقة نموذج التدوينات Post من الملف app.py، واستعلمنا عن جدول التدوينات جالبين منه كافّة التدوينات الموجودة في قاعدة البيانات، ومررنا بعدها على كل تدوينة لنعرض عنوانها وقائمة الوسوم المرتبطة بها، وسيظهر الخرج كما يلي: Post The First [<Tag "animals">, <Tag "writing">] --- Post The Third [<Tag "cooking">, <Tag "tech">, <Tag "writing">] --- Post The Second [] --- يمكنك الوصول إلى أسماء الوسوم باستخدام الأمر tag.name كما هو مبين في المثال التالي، والذي يمكنك تشغيله باستخدام صَدَفَة فلاسك: from app import Post posts = Post.query.all() for post in posts: print('TITLE: ', post.title) print('-') print('TAGS:') for tag in post.tags: print('> ', tag.name) print('-'*30) وفي هذه الحالة وبالإضافة إلى طباعة عنوان كل تدوينة، نمرّ على وسوم كل منها ونطبع أسماءها، لنحصل على خرجٍ على النحو التالي: TITLE: Post The First - TAGS: > animals > writing ------------------------------ TITLE: Post The Third - TAGS: > cooking > tech > writing ------------------------------ TITLE: Post The Second - TAGS: ------------------------------ نلاحظ مما سبق أنّ الوسوم المُضافة إلى التدوينات في البرنامج init_db.py متموضعة بمكانها الصحيح بالنسبة للتدوينات. وللاطلاع على مثال لتوضيح كيفية الوصول إلى التدوينات التابعة لكل وسم باستخدام التعليمة tag.posts، سنشغّل الشيفرة التالية في صّدّفّة فلاسك: from app import Tag writing_tag = Tag.query.filter_by(name='writing').first() for post in writing_tag.posts: print(post.title) print('-'*6) print(post.content) print('-') print([tag.name for tag in post.tags]) print('-'*20) استوردنا في الشيفرة السابقة النموذج Tag، ثمّ استخدمنا تابع الترشيح ()filter_by على سمة الاستعلام query ممررين إليه معامل الاسم name ليحصل على الوسم writing، ونحصل على أوّل نتيجة باستخدام التابع ()first، ثم خزّنا كائن الوسم الناتج ضمن متغير باسم writing_tag. للمزيد حول تابع الترشيح filter_by ننصحك بقراءة الخطوة 4 من المقال استخدام الإضافة Flask-SQLAlchemy للتخاطب مع قواعد البيانات في تطبيقات فلاسك. مررنا بعد ذلك على جميع التدوينات المُتضمّنة للوسم writing، والتي وصلنا إليها باستخدام التعليمة writing_tag.posts، لنطبع عنوان كل تدوينة ومحتواها وقائمة بأسماء الوسوم الخاصّة بها والتي أنشأناها باستخدام طريقة استيعاب القائمة list comprehension المعتمدة على وسوم التدوينة، التي يمكننا الوصول إليها باستخدام الأمر post.tags. ويكون الخرج على النحو التالي: Post The Third ------ Content for the third post - ['cooking', 'tech', 'writing'] -------------------- Post The First ------ Content for the first post - ['animals', 'writing'] -------------------- نرى في الخرج السابق التدوينتان المُتضمنتان للوسم writing مع قائمة بايثون بأسماء كافّة وسوم كل من التدوينتين. ومع نهاية هذه الخطوة أصبح من الممكن الوصول إلى التدوينات ووسوم كل منها أو إلى التدوينات المُتضمّنة لوسم معيّن، إذ أضفنا نموذج قاعدة بيانات ليمثّل جدول الوسوم، وربطنا ما بين جدولي التدوينات والوسوم باستخدام جدول ارتباط، كما أدخلنا عددًا من الوسوم إلى قاعدة البيانات واستخدمناها للإشارة إلى التدوينات، وأصبح من الممكن الوصول إلى التدوينات ووسوم كل منها أو إلى التدوينات المُتضمّنة لوسم ما. سنستخدم في الخطوة التالية صَدَفَة فلاسك لإضافة تدوينات ووسوم جديدة والربط فيما بينها، كما سنتعرّف على كيفية حذف وسوم من تدوينة ما. الخطوة 3 - إدارة البيانات في العلاقة متعدد-إلى-متعدد سنستخدم في هذه الخطوة صَدَفَة فلاسك لإضافة تدوينات ووسوم جديدة إلى قاعدة البيانات والربط فيما بينها، إذ سنتمكّن من الوصول إلى كل تدوينة مع وسومها، وسنتعرّف على كيفية فك ارتباط عنصرين في العلاقة مُتعدّد-إلى-مُتعدّد. بدايةً وفي حال كون صَدَفَة فلاسك غير مفتوحة أصلًا، نفتحها مع التأكّد من كون البيئة البرمجية مُفعّلة، كما يلي: (env)user@localhost:$ flask shell سنضيف بعد ذلك بضعة تدويناتٍ ووسوم: from app import db, Post, Tag life_death_post = Post(title='A post on life and death', content='life and death') joy_post = Post(title='A post on joy', content='joy') life_tag = Tag(name='life') death_tag = Tag(name='death') joy_tag = Tag(name='joy') life_death_post.tags.append(life_tag) life_death_post.tags.append(death_tag) joy_post.tags.append(joy_tag) db.session.add_all([life_death_post, joy_post, life_tag, death_tag, joy_tag]) db.session.commit() وهذا ما سيُنشئ بالنتيجة تدوينتين وثلاثة وسوم. إذ أشرنا إلى التدوينات باستخدام الوسوم الموافقة، كما استخدمنا التابع ()add_all لإضافة العناصر المُنشأة حديثًا إلى جلسة قاعدة البيانات. ونهايةً أكدنا التغييرات وطبّقناها على قاعدة البيانات باستخدام التابع ()commit. فيما يلي سنستخدم صَدَفة فلاسك للحصول على كافّة التدوينات ووسومها كما فعلنا في الخطوة السابقة، على النحو التالي: posts = Post.query.all() for post in posts: print(post.title) print(post.tags) print('---') فنحصل على خرجٍ شبيه بما يلي: Post The First [<Tag "animals">, <Tag "writing">] --- Post The Third [<Tag "cooking">, <Tag "tech">, <Tag "writing">] --- Post The Second [] --- A post on life and death [<Tag "life">, <Tag "death">] --- A post on joy [<Tag "joy">] --- وبذلك نجد أنّ التدوينات قد أُضيفت مع وسوم كل منها. الآن وبغية توضيح كيفية فك ارتباط عنصرين ضمن علاقة قاعدة البيانات من النوع مُتعدّد-إلى-مُتعدّد، سنفرض على سبيل المثال أنّ التدوينة الثالثة Post The Third لم تعد معنية بالطبخ، وبالتالي يجب أن نحذف الوسم cooking منها. لتحقيق ذلك، سنجلب بدايةً التدوينة مع الوسم المراد حذفه، كما يلي: >>> from app import db, Post, Tag >>> post = Post.query.filter_by(title='Post The Third').first() >>> tag = Tag.query.filter_by(name='cooking').first() >>> print(post.title) >>> print(post.tags) >>> print(tag.posts) جلبنا في الشيفرة السابقة التدوينة ذات العنوان Post The Third باستخدام التابع ()filter_by، كما جلبنا الوسم cooking، لنطبع كل من عنوان التدوينة ووسومها وكافّة التدوينات المُتضمّنة للوسم cooking. يعيد التابع ()filter_by كائن استعلام، وباستخدام التابع ()all نحصل على نتائج هذا الاستعلام ضمن قائمة، ولكن وبما أنّنا نتوقّع الحصول على نتيجة واحدة من الاستعلام في هذه الحالة، فاستخدمنا التابع ()first للحصول فقط على النتيجة الأولى، وللمزيد حول كل من التابعين ()first و ()all، ننصحك بقراءة الخطوة 4 من المقال استخدام الإضافة Flask-SQLAlchemy للتخاطب مع قواعد البيانات في تطبيقات فلاسك. ويكون الخرج في هذه الحالة على النحو التالي: Post The Third [<Tag "cooking">, <Tag "tech">, <Tag "writing">] [<Post "Post The Third">] وفيه نرى عنوان التدوينة ووسومها، مع قائمة بكافّة التدوينات المُتضمّنة للوسم cooking. الآن وبغية حذف الوسم cooking من التدوينة، سنستخدم التابع ()remove كما يلي: >>> post.tags.remove(tag) >>> db.session.commit() >>> print(post.tags) >>> print(tag.posts) استخدمنا في الشيفرة السابقة التابع ()remove لفصل الوسم cooking عن التدوينة، ثمّ استخدمنا التابع ()db.session.commit لتطبيق التغييرات على قاعدة البيانات، فنحصل على خرجٍ يؤكّد حذف الوسم من التدوينة، على النحو التالي: [<Tag "tech">, <Tag "writing">] [] وبذلك نجد أنّ الوسم cooking قد حُذف فعلًا من القائمة post.tags، كما أنّ التدوينة لم تعد موجودةً ضمن قائمة التدوينات tag.posts المُتضمّنة لهذا الوسم. نُغلق الآن صَدَفَة فلاسك: >>> exit() ومع نهاية هذه الخطوة نكون قد أضفنا تدويناتٍ ووسوم جديدة، كما أضفنا الوسوم إلى التدوينات وحذفناها منها. سنعمل في الخطوة التالية على عرض الوسوم الخاصّة بكل تدوينة في صفحة تطبيق المدونة الرئيسية. الخطوة 4 - عرض الوسوم أسفل كل تدوينة سنحرّر في هذه الخطوة قالب الصفحة الرئيسية لعرض الوسوم الخاصّة بكل تدوينة أسفلها. لذا، اطلع بدايةً على البنية الحالية للصفحة الرئيسية للتطبيق. أعلم فلاسك بالتطبيق المطلوب تشغيله وهو في حالتنا الملف app.py باستخدام متغير البيئة FLASK_APP وذلك بعد التأكد من أن البيئة البرمجية مُفعّلة، واضبط متغير البيئة FLASK_ENV على وضع التطوير development، على النحو التالي: (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development شغّل البرنامج الآن: (env)user@localhost:$ flask run وأثناء كون خادم التطوير قيد التشغيل، انتقل إلى الرابط التالي باستخدام المُتصفح: http://127.0.0.1:5000/ فستظهر لك صفحة شبيهة بالشّكل التالي: نترك خادم التطوير قيد التشغيل ونفتح نافذة سطر أوامر جديدة. نريد في هذه المرحلة عرض كافّة وسوم كل تدوينة ضمن صفحتين من صفحات التطبيق: الأولى في الصفحة الرئيسية أسفل كل تدوينة، والثانية في الصفحة المُخصّصة لكل تدوينة لتظهر الوسوم أسفل محتواها، إذ سنستخدم في الحالتين نفس الشيفرة لعرض الوسوم، لذا ولتجنّب التكرار سنستخدم ماكرو جينجا jinja macro، الذي يعمل مثل أي دالة في بايثون ويتضمّن شيفرة HTML ديناميكية قابلة للاستخدام في أي مكان يُستدعى فيه الماكرو؛ وفي حال التعديل عليه، سيُعدّل تلقائيًا في كافّة الأماكن المُستدعى فيها ما يجعل الشيفرة قابلة لإعادة الاستخدام. لذا، سننشئ ملفًا جديدًا باسم "macros.html" ضمن مجلد القوالب templates، على النحو التالي: (env)user@localhost:$ nano templates/macros.html ونكتب ضمنه الشيفرة التالية: {% macro display_tags(post) %} <div class="tags"> <p> <h4>Tags:</h4> {% for tag in post.tags %} <a href="#" style="text-decoration: none; color: #dd5b5b"> {{ tag.name }} </a> | {% endfor %} </p> </div> {% endmacro %} نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة الكلمة المفتاحية macro للتصريح عن ماكرو باسم ()display_tags ذو معامل باسم post، إذ استخدمنا الوسم <div> ليعرض عنوانًا من المستوى الرابع <h4>، كما استخدمنا حلقة for تكرارية بغية المرور على كافّة وسوم كائن التدوينة المُمرّرة مثل وسيط إلى الماكرو المُستدعى، كما هو الحال لدى تمرير أي وسيط لدالة بايثون اعتيادية مُراد استدعاؤها. حصلنا على وسوم كل تدوينة باستخدام التعليمة post.tags، لنعرض اسم الوسم ضمن وسم رابط <a>، إذ سنعدّل لاحقًا قيمة السمة href لوسم الرابط لنجعله يشير إلى صفحة مُخصّصة للوسم سننشئها لاحقًا لتعرض كافّة التدوينات المُتضمّنة للوسم المُحدّد. نهايةً، استخدمنا الكلمة المفتاحية endmacro للدلالة على انتهاء الماكرو. الآن، وبغية عرض كافّة الوسوم التابعة للتدوينة أسفلها في الصفحة الرئيسية للتطبيق، سنفتح ملف القالب index.html: (env)user@localhost:$ nano templates/index.html وسنستورد بدايةً الماكرو ()display_tags من الملف "macros.html"، لذا سنضيف الاستيرادات التالية إلى بداية الملف قبل السطر {% extends 'base.html' %}، كما يلي: {% from 'macros.html' import display_tags %} {% extends 'base.html' %} سنعدّل بعد ذلك الحلقة التكرارية for post in posts لتتضمّن استدعاءً للماكرو ()display_tags على النحو التالي: {% for post in posts %} <div class="post"> <p><b>#{{ post.id }}</b></p> <b> <p class="title"> <a href="{{ url_for('post', post_id=post.id)}}"> {{ post.title }} </a> </p> </b> <div class="content"> <p>{{ post.content }}</p> </div> {{ display_tags(post) }} <hr> </div> {% endfor %} نحفظ الملف ونغلقه. استدعينا في الشيفرة السابقة الشيفرة الجامعة ()display_tags ممررين إليها الكائن post، وهذا ما سيعرض بالنتيجة أسماء الوسوم أسفل كل تدوينة. الآن، بتحديث الصفحة الرئيسية للتطبيق، نرى الوسوم أسفل كل تدوينة كما في الشّكل: أمّا الآن فسنعمل على إضافة الوسوم أسفل محتوى التدوينة في الصفحة المُخصّصة لها. لذا، نفتح ملف القالب post.html: (env)user@localhost:$ nano templates/post.html ونستورد في بدايته الماكرو display_tags بالشّكل: {% from 'macros.html' import display_tags %} {% extends 'base.html' %} ونستدعي بعد ذلك الماكرو ()display_tags ممررين إليه الكائن post أسفل محتوى التدوينة وقبل وسم الفاصل الأفقي <hr> على النحو التالي: <div class="post"> <p><b>#{{ post.id }}</b></p> <b> <p class="title">{{ post.title }}</p> </b> <div class="content"> <p>{{ post.content }}</p> </div> {{ display_tags(post) }} <hr> <h3>Comments</h3> نحفظ الملف ونغلقه. الآن وبالانتقال إلى صفحة التدوينة (الثانية مثلًا) باستخدام الرابط: http://127.0.0.1:5000/2 نرى الوسوم معروضة أسفل محتوى التدوينة بنفس طريقة عرضها في الصفحة الرئيسية للتطبيق. سنعمل في الخطوة التالية على إضافة وجهة جديدة إلى تطبيق فلاسك هذا لتعرض كافّة التدوينات المُتضمّنة لوسم معيّن، كما سنجعل رابط الوسم الذي أضفناه في هذه الخطوة فعّالًا. الخطوة 5 - عرض الوسوم وتدويناتها سنضيف في هذه الخطوة وجهةً وقالبًا إلى التطبيق لعرض الوسوم الموجودة في قاعدة البيانات مع التدوينات المُتضمّنة لكل منها. لذا، سنضيف بدايةً وجهةً لعرض التدوينات المُتضمّنة لكل وسم، فعلى سبيل المثال، ستعرض الوجهة /tags/tag_name/ صفحةً تحتوي كافّة التدوينات المُتضمّنة للوسم المُحدّد في الجزء المُخصّص لاسم الوسم المطلوب tag_name من الوجهة. نقتح الآن الملف app.py لتحريره: (env)user@localhost:$ nano app.py ونضيف الوجهة التالية إلى نهايته: # ... @app.route('/tags/<tag_name>/') def tag(tag_name): tag = Tag.query.filter_by(name=tag_name).first_or_404() return render_template('tag.html', tag=tag) نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة متغير الرابط tag_name المسؤول عن تحديد كل من الوسم والتدوينات المُتضمّنة لهذا الوسم والتي ستُعرض ضمن صفحة الوسم، إذ يُمرّر اسم الوسم إلى الدالة ()tag العاملة في فلاسك باستخدام المعامل tag_name المُستخدم أيضًا ضمن تابع الترشيح ()filter_by للاستعلام عن الوسم المطلوب. استخدمنا التابع ()first_or_404 للحصول على كائن الوسم، فإمّا أن يُخزّن في حال وجوده ضمن متغير باسم tag، وإلّا سنحصل على رسالة الخطأ "404 Not Found" في حال عدم وجود وسم في قاعدة البيانات باسم مطابق للاسم المطلوب. ثمّ صيّرنا ملف قالب باسم "tag.html" مُمررين إليه الكائن tag. سنفتح الآن القالب الجديد "templates/tag.html" بغية تحريره: (env)user@localhost:$ nano templates/tag.html ونكتب ضمنه الشيفرة التالية: {% from 'macros.html' import display_tags %} {% extends 'base.html' %} {% block content %} <span class="title"> <h1>{% block title %} Posts Tagged with "{{ tag.name }}" {% endblock %}</h1> </span> <div class="content"> {% for post in tag.posts %} <div class="post"> <p><b>#{{ post.id }}</b></p> <b> <p class="title"> <a href="{{ url_for('post', post_id=post.id)}}"> {{ post.title }} </a> </p> </b> <div class="content"> <p>{{ post.content }}</p> </div> {{ display_tags(post) }} <hr> </div> {% endfor %} </div> {% endblock %} نحفظ الملف ونغلقه. استوردنا في الشيفرة السابقة الماكرو ()display_tags من الملف "macros.html"، واستخدمنا القالب الرئيسي بالاعتماد على تعليمة extend. كما عيّنا ترويسةً ضمن كتلة المحتوى لتعمل مثل عنوان مُتضمنةً اسم الوسم، مرّرنا بعد ذلك على كافّة التدوينات المُتضمّنة للوسم المطلوب والتي قد وصلنا إليها باستخدام التعليمة tag.posts، لنعرض معرّف التدوينة وعنوانها ومحتواها، ومن ثمّ استدعينا الماكرو ()display_tags لعرض كافّة الوسوم التابعة للتدوينة. الآن ومع كون خادم التطوير قيد التشغيل، ننتقل إلى الرابط التالي باستخدام المُتصفّح: http://127.0.0.1:5000/tags/writing/ وهي الصفحة المُخصّصة للوسم writing، وكما هو موضّح في الصورة أدناه فإنّ كافّة الوسوم المُتضمّنة للوسم writing معروضةٌ في هذه الصفحة. أمّا الآن فسنعدّل الماكرو ()display_tags لجعل روابط الوسوم فعّالة، لذا سنفتح الملف "macros.html": (env)user@localhost:$ nano templates/macros.html ونعدّل قيمة السمة href كما يلي: {% macro display_tags(post) %} <div class="tags"> <p> <h4>Tags:</h4> {% for tag in post.tags %} <a href="{{ url_for('tag', tag_name=tag.name) }}" style="text-decoration: none; color: #dd5b5b"> {{ tag.name }} </a> | {% endfor %} </p> </div> {% endmacro %} نحفظ الملف ونغلقه. الآن وبتحديث الصفحات التي استخدمنا فيها الماكرو ()display_tags، نلاحظ أنّ روابط الوسوم أصبحت فعّالة: http://127.0.0.1:5000/ http://127.0.0.1:5000/2/ http://127.0.0.1:5000/tags/writing/ وبذلك نجد أنّ استخدام الماكرو المُقدّمة من محرّك القوالب جينجا سمح بإعادة استخدام الشيفرة وبالتالي تقليل التكرار، ناهيك عن حقيقة كون تعديل ملف الماكرو الرئيسي كفيل بتطبيق التغييرات على كافّة القوالب المُستخدِمة له. ومع نهاية هذه الخطوة أصبح لدينا صفحة خاصّة بكل وسم يتمكّن من خلالها المُستخدمون من رؤية كافّة التدوينات المُتضمّنة لوسم معيّن، إذ يمكن الوصول إلى صفحة الوسم هذه من خلال النقر على الوسوم أسفل كل تدوينة كونها تتضمّن روابط إلى الصفحة الخاصّة بالوسم. الخاتمة وضحنا في هذا المقال كيفية إدارة العلاقات من نوع مُتعدّد-إلى-مُتعدّد باستخدام الإضافة Flask-SQLAlchemy وذلك بإضافة ميزة الوسوم إلى تطبيق المدونة السابق، إذ تعرفنا على كيفية ربط جدولين باستخدام جدول الارتباط (والذي يُدعى أيضًا جدول الصلة)، وكيفية ربط المدخلات ببعضها وإضافتها إلى قاعدة البيانات والوصول إلى البيانات وفك ارتباطها عن أحد المُدخلات. ترجمة -وبتصرف- للمقال How To Use Many-to-Many Database Relationships with Flask-SQLAlchemy لصاحبه Abdelhadi Dyouri. اقرأ أيضًا العلاقات بين الجداول في SQL استخدام علاقة many-to-many مع فلاسك Flask ومحرك قواعد البيانات SQLite الرّبط بين جدولي المقالات والمُستخدمين بعلاقة واحد للعديد One-to-Many Relationship
-
تعرفنا في المقال السابق قواعد تنسيق الشيفرات ودور المنسق Black في بايثون من هذه السلسلة على مفهوم تنسيق الشيفرة مبينين مجموعة من القواعد الواجب تطبيقها على الشيفرة المصدرية لمنحها مظهرًا معينًا. كما تعرفنا على منسِّق Black وهو أداة لتنسيق الشيفرات، إذ تنسق الشيفرة المصدرية تلقائيًا إلى شكلٍ مقروء ومتناغم، دون التأثير على سلوك البرنامج وأداءه؛ وسنتعرف في هذا المقال على آلية تثبيت واستخدام وتخصيص هذه الأداة. ينسّق Black الشيفرات تلقائيًا في ملفات بايثون ذات اللاحقة .py، ورغم ضرورة فهمك لقواعد التنسيق الواردة في المقال السابق، إلا أن Black وحده قادر على إجراء كافة عمليات التنسيق الفعلية، ففي حال كنت تعمل على مشروعٍ برمجي ضمن فريق، فسيوفّر عليكم استخدام Black الكثير من الجدالات حول كيفية تنسيق الشيفرات. وفي الواقع من غير الممكن تغيير أغلبية التنسيقات التي يجريها Black، ومن هنا جاءت تسميته بالصارم. أما عن اسمه (Black الأسود)، فهو مستوحى من اقتباس مالك مصنع السيارات هينري فورد حول خيارات ألوان السيارات التي يقدمها لعملائه، إذ يقول "يمكنك اختيار أي لون تريده، طالما أنك اخترت اللون الأسود". تثبيت الأداة Black نثبّت Black باستخدام أداة تثبيت الحزم pip الافتراضية في بايثون. ولإجراء ذلك، في حال استخدامك لنظام التشغيل ويندوز، افتح نافذة سطر أوامر، واكتب ضمنها ما يلي: C:\Users\Al\>python -m pip install --user black أما إذا كنت تستخدم أحد نظاميّ ماك أو لينكس، فافتح نافذة موجه الأوامر مُستخدمًا الأمر python3 بدلًا من python كما هو الحال في ويندوز (الأمر الواجب مراعاته في كافّة تعليمات بايثون الواردة في هذا المقال): $ python3 -m pip install --user black ويعد الخيار m- مسؤولًا عن إعلام بايثون لتشغيل الوحدة pip كتطبيق، مع ملاحظة أن بعض الوحدات في بايثون مضبوطة لتعمل كتطبيق افتراضيًا. ثمّ نتحقق من كون التثبيت قد تم بنجاح عبر تشغيل الأمر python -m black، وعندها يجب أن تظهر رسالة مفادها عدم وجود مسارات معطاة No paths given أو ما من شيء لفعله Nothing to do، بدلًا من ظهور رسالة تفيد بعدم وجود وحدة باسم black أو No module named black. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تشغيل Black من سطر الأوامر من الممكن تشغيل المنسّق Black لأي ملف بايثون باستخدام سطر الأوامر، كما أنّ محررات النصوص وبيئات التطوير المتكاملة قادرة على تشغيل Black في الخلفية أثناء عملك ضمنها. وللحصول على التعليمات المتعلقة بكيفية تشغيل Black مع محررات نصوص مثل Jupyter و Visual Studio Code و PyCharm وغيرها، من خلال زيارة الصفحة الرئيسية للمنسّق Black على psf/black. فعلى سبيل المثال، لنفرض أننا نريد تنسيق ملف باسم yourScript.py تلقائيًا، لذا نفتح نافذة سطر الأوامر ونكتب فيها الأمر التالي ونشغله (مع مراعاة تبديل الأمر python إلى python3 في حال استخدام أحد نظامي ماك أو لينكس): C:\Users\Al>python -m black yourScript.py وبمجرد تشغيل الأمر السابق، سينُسَق محتوى الملف yourScript.py وفقًا لقواعد تنسيق Black. وفي حال كون متغير البيئة PATH مضبوطًا ليشغّل Black مباشرةً، عندها يكفي كتابة الشيفرة التالية لتنسيق الملف yourScript.py تلقائيًا: C:\Users\Al>black yourScript.py كما من الممكن جعل Black يعمل على كل الملفات ذات اللاحقة py. ضمن مجلد معين، وذلك بتحديد هذا المجلد ضمن الأمر بدلًا من تحديد اسم ملف معيّن، فمثلًا الأمر التالي في ويندوز سينسّق كافة الملفات ذات اللاحقة py. الموجودة ضمن المجلد C:\yourPythonFiles بما يتضمّن الملفات الموجودة في المجلدات الفرعية منه: C:\Users\Al>python -m black C:\yourPythonFiles فتحديد مجلد كامل لتنسيق ملفاته يعد خيارًا مفيدًا جدًا في حال كون مشروعك يتضمّن عدة ملفات بايثون، كبديل عن كتابة أمر خاص لتنسيق كل ملف وحده. ورغم كون Black صارمًا في طريقة تنسيقه للشيفرات، إلا أن الفقرات الثلاث التالية تتضمّن بعض الخيارات الممكن تغيرها فيه، وللاطلاع على كامل الخيارات التي يوفرها Black، شغّل الأمر python -m black --help في نافذة سطر الأوامر. تعديل إعدادات طول السطر في Black إن الطول المعياري للسطر الواحد في بايثون هو 80 محرفًا، ويعود تاريخ الأسطر ذات الثمانين محرفًا إلى حقبة الحوسبة باستخدام البطاقات المُثقبّة punch cards في عشرينيات القرن الماضي، حين طرحت شركة IBM بطاقات مثقبة ذات 80 عمود و 12 سطر، ليستمر استخدام معيار الثمانين محرفًا في الطابعات والشاشات ونوافذ سطر الأوامر لعدّة عقود لاحقة. ولكن مع ظهور الشاشات عالية الدقة في القرن الواحد والعشرين، غدت هذه الشاشات قادرة على عرض نصوص يزيد عرضها عن ثمانين محرف. ففي حين أن الأسطر الأطول قد تقلل من عبء التمرير الرأسي لاستعراض الملف، وبالمقابل فالأسطر الأقصر تقلل من اكتظاظ الشيفرات في السطر الواحد، ما قد يسهل من عملية مقارنة شيفرتين جنبًا إلى جنب (بتقسيم الشاشة رأسيًا) دون الحاجة إلى التمرير أفقيًا أثناء المقارنة. يستخدم Black افتراضيًا أسطرًا ذات 88 محرفًا، مضيفًا 10% زيادة عن القيمة المعيارية (80 محرف)، أما عن تفضيلي الشخصي فهو 120 محرف للسطر، ولجعل Black ينسق الشيفرة جاعلًا الحد الأعظمي لطول السطر 120 محرفًا على سبيل المثال، نستخدم خيار سطر الأوامر l 120-، إذ يبدو الأمر في نظام ويندوز كما يلي: C:\Users\Al>python -m black -l 120 yourScript.py وبغض النظر عما تختاره بخصوص الحد الأقصى لعدد المحارف بالسطر، الأهم أن يكون هذا الخيار موحدًا لكافّة ملفات بايثون في المشروع الواحد. تعطيل إعداد حصر السلاسل النصية بعلامات اقتباس مزدوجة يغير Black صياغة أي سلسلة نصية في الشيفرة تلقائيًا لتصبح محصورة بين علامتي اقتباس مزدوجتين، وذلك في حال استخدام علامتي اقتباس مفردتين لحصرها، إلا في حال كون السلسلة النصية تتضمّن أصلًا علامات اقتباس مزدوجة، فعندها يستخدم علامات مفردة، لنأخذ مثلًا ملفًا باسم yourScript.py يتضمن الشيفرات التالية: a = 'Hello' b = "Hello" c = 'Al\'s cat, Zophie.' d = 'Zophie said, "Meow"' e = "Zophie said, \"Meow\"" f = '''Hello''' وبتشغيل Black على الملف السابق، سينسقه ليصبح بالشكل: 1 a = "Hello" b = "Hello" c = "Al's cat, Zophie." 2 d = 'Zophie said, "Meow"' e = 'Zophie said, "Meow"' 3 f = """Hello""" إن تفضيل Black لاستخدام علامات الاقتباس المزدوجة لإحاطة السلاسل النصية يجعل من الشيفرة المكتوبة بلغة بايثون شبيهة بتلك المكتوبة بلغات برمجة أخرى، إذ تستخدم معظم اللغات الأخرى علامات الاقتباس المزدوجة لصياغة السلاسل النصية. ومن الجدير بالملاحظة أنه وباستخدام Black على الشيفرة السابقة فأن السلاسل النصية في المتغيرات a و b و c قد حُصرت ضمن علامات اقتباس مزدوجة، في حين بقيت علامة الاقتباس المفردة للسلسلة في المتغير d لتجنب الخلط مع العلامة المزدوجة الموجودة أصلًا ضمن السلسلة في السطر 2، كما نلاحظ أن Black قد استبدل علامات الاقتباس المفردة الثلاثية في السطر 3 الدالة على كون هذه السلسة متعددة الأسطر multiline، بعلامة اقتباس مزدوجة ثلاثية. وفي حال رغبتك بترك علامات الاقتباس على حالها دون تغييرها من قبل Black، فاستخدم ضمن استدعاءه الخيار S- في نافذة سطر الأوامر (مع ملاحظة أنّ حرف S كبير هنا)، فعلى سبيل المثال، في حال تشغيل Black على الملف yourScript.py الأصلي في نظام ويندوز، سيظهر الخرج التالي: C:\Users\Al>python –m black -S yourScript.py All done! 1 file left unchanged. كما من الممكن استخدام كلا خياري الحد الأقصى لمحارف السطر l- وخيار عدم تغيير علامات الاقتباس S- في سطرٍ واحد، بالشكل: C:\Users\Al>python –m black –l 120 -S yourScript.py استعراض التغييرات التي أجراها Black رغم كون تعديلات Black تنسيقية بحتة لا تشتمل مثلًا على تغيير أسماء المتغيرات أو أي تغيير في سلوك الشيفرة البرمجي، إلا أنك قد لا تفضل التغييرات التنسيقية التي أجراها، وفي حال رغبتك بالرجوع إلى تنسيقات شيفرتك الأصلية، فبإمكانك إما استخدام نظام إدارة الإصدارات أو الرجوع إلى نسخك الاحتياطية الخاصة. ولعل الخيار البديل الأفضل هو معاينة التغييرات التي سيجريها Black قبل تطبيقها الفعلي من خلال تشغيل Black مع استخدام خيار سطر الأوامر diff--، والذي يبدو في نظام ويندوز بالشكل: C:\Users\Al>python -m black --diff yourScript.py سيعطي هذا الأمر خرجًا يمثّل التغييرات التي سيجريها Black في صياغة لبيان الاختلافات شبيهة بتلك شائعة الاستخدام في برمجيات إدارة الشيفرات، إلا أنها مقروءة وواضحة للبشر. فمثلًا لو تضمّن الملف yourScript.py السطر البرمجي[weights=[42.0,3.1415,2.718، وبتشغيل الخيار diff-- مع Black، سيكون الخرج بالشكل: C:\Users\Al\>python -m black --diff yourScript.py --- yourScript.py 2020-12-07 02:04:23.141417 +0000 +++ yourScript.py 2020-12-07 02:08:13.893578 +0000 @@ -1 +1,2 @@ -weights=[42.0,3.1415,2.718] +weights = [42.0, 3.1415, 2.718] تشير علامة الناقص إلى أن Black سيحذف السطر التالي: weights=[42.0,3.1415,2.718] ليستبدله إلى لسطر المشار إليه بعلامة الزائد، أي: weights = [42.0, 3.1415, 2.718] وتذكّر دائمًا أنه ما من طريقة للرجوع عن التغييرات التي يجريها Black بعد تنفيذها، وما من سبيل سوى أن تنشئ نسخًا احتياطية من شيفرتك أو أن تستخدم إحدى برمجيات إدارة الشيفرات مثل Git وذلك قبل تطبيق تغييرات Black عليها. تعطيل عمل Black على أجزاء محددة من الشيفرة مع كامل روعة عمل Black، إلا أنك قد لا ترغب بإجرائه لتغييرات على أجزاء معينة من شيفرتك، فمثلًا أنا شخصيًا أفضّل اتباع طريقتي الخاصة لإنشاء التباعد في تعليمات الإسناد المتعددة المتعلقة ببعضها بغية ترتيبها، كما في المثال التالي: # Set up constants for different time amounts: SECONDS_PER_MINUTE = 60 SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY ومع استخدام Black على هذا الجزء، فإنه سيحذف الفراغات الإضافية قبل معامل الإسناد =، جاعلًا مقروئية التعليمات أقل من وجهة نظري، بالشكل: # Set up constants for different time amounts: SECONDS_PER_MINUTE = 60 SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY ولإعلام Black برغبتنا بعدم إجراء أي تغييرات على مقطع برمجي معيّن، يكفي حصره ما بين التعليقين fmt: off # و fmt: on #، ليتابع بعده إجراء التنسيقات بشكل طبيعي: # Set up constants for different time amounts: # fmt: off SECONDS_PER_MINUTE = 60 SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY # fmt: on وبذلك وبتشغيل Black على هذا الملف، فإنه لن يغير من التباعدات الخاصة أو أي تنسيقات أخرى في الجزء من الشيفرة المحصور بين التعليقين. الخاتمة مما لا خلاف فيه بأن نمط تنسيق الشيفرات إجمالًا هو تفضيل شخصي، الأمر الذي لا يتنافى مع أصول مجال تطوير البرمجيات، مع وجود خطوط عريضة متفق عليها تحدد التنسيق الجيد من السيء، تاركةً فيما بينها هامشًا من الحرية للتفضيلات الشخصية. وبما أن معظم أعمال التطوير البرمجية جماعية وليست فردية، فسواءً كنت تعمل مع مبرمجين آخرين على مشروعٍ ما، أو كنت ببساطة ستطرح سؤالًا متعلقًا بشيفرتك على أحد المبرمجين الخبراء طالبًا مساعدته، في كلتا الحالتين يعد تنسيق الشيفرة وفق النمط المقبول المعتمد أمرًا بالغ الأهمية، وهنا يبرز دور الأداة Black، ففي حين أن تنسيق الشيفرات يدويًا ضمن المحررات قد تكون مهمَّة مملة، فمن الممكن أتمتتها باستخدام أداة مثل Black، إذ استعرضنا في هذا المقال كيفية تثبيت الأداة Black وضبطها. مما لا شك فيه أن تنسيق الشيفرات لا يقتصر على التباعد أو استخدام علامات الاقتباس المفردة أو المزدوجة، فمثلًا اختيار أسماء للمتغيرات تصف وظيفتها هو أمر مهم للمقروئية، فمع كون المنسقات الآلية قادرة على اتخاذ القرارات حيال التنسيقات النحوية الشكلية، إلا أنها غير قادرة على اتخاذ تلك المتعلقة بالدلالات اللفظية من قبيل اختيار أسماء مناسبة للمتغيرات، إذ تقع هذه المسؤوليات على عاتقك كمبرمج، الأمر الذي سنناقشه تفصيليًا في المقال القادم. ترجمة -وبتصرف- للجزء الثاني من الفصل الثالث Code Formatting With Black من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigarti. اقرأ أيضًا المقال السابق: قواعد تنسيق الشيفرات ودور المنسق Black في بايثون بيئات التطوير IDE المستخدمة في تطوير تطبيقات بايثون كيفية استخدام سطر أوامر بايثون التفاعلي كيفية تنسيق النصوص في بايثون
-
يعرَّف تنسيق الشيفرة بأنه تطبيق مجموعة من القواعد على الشيفرة المصدرية لمنحها مظهرًا معينًا. ورغم عدم أهمية تنسيق الشيفرة بالنسبة للحاسوب المُحلِّل للبرامج، إلا أنه أمر بالغ الأهمية من ناحية سهولة قراءة الشيفرة، وهذا ما يسهل مراجعتها. فإذا كانت الشيفرة صعبة الفهم على البشر (عليك أو على أحد زملائك) فما بالك بإصلاح الأخطاء فيها أو إضافة ميزات جديدة إليها، سيكون الأمر أصعب بكل تأكيد، فتنسيق الشيفرة ليس بالمسألة الجمالية فحسب، ولعل أحد أهم أسباب شعبية لغة بايثون هي سهولة قراءتها. سنتعرف في هذا المقال على منسِّق الشيفرات Black في بايثون وهو أداة لتنسيق الشيفرات، إذ تنسق الشيفرة المصدرية تلقائيًا إلى شكلٍ مقروء ومتناغم، دون التأثير على سلوك البرنامج وأداءه، وتكمن أهمية Black في كونها تجنب المستخدم التنسيق المُمل للشيفرات في محرر النصوص أو بيئة التطوير المتكاملة IDE، إذ سنتعرف على آلية اختيار Black لنمط الشيفرة المحدد. فوضى شيفراتك ستفقدك أصدقائك وتنفر زملائك في العمل كل الدروب تؤدي إلى روما من وجهة نظر الحاسوب، إذ من الممكن كتابة الشيفرة الواحدة بطرق عديدة وستكون نتيجة التنفيذ واحدة. فعلى سبيل المثال، من الممكن كتابة عناصر قائمة بحيث يفصل بين كل عنصر وآخر مسافة واحدة بعد كل فاصلة مع حصر كل عنصر بين علامتي اقتباس مفردتين، بالشكل: spam = ['dog', 'cat', 'moose'] وستبقى هذه الشيفرة صالحة برمجيًا ومقبولة من ناحية الصياغة حتى في حال كونها أكثر فوضوية، مع استخدام أعداد مختلفة من المسافات بين عنصر وآخر مثلًا، أو استخدام أنماط متعددة من علامات الاقتباس (المفردة والمزدوجة)، كما يلي: spam= [ 'dog' ,'cat',"moose"] وقد يميل المبرمجون ممن يتبعون النهج السابق في كتابة الشيفرات لتفضيل طريقة الفصل البصري المنتظم باستخدام مسافات فاصلة متساوية وعلامات اقتباس متماثلة، كما هو الحال في الطريقة الأولى، في حين قد يفضّل البعض الطريقة الثانية، إذ أنّ جلّ اهتمامهم ينصب على كون البرامج تعمل كما يجب، مبتعدين عن التفكير بالتفاصيل التي لا تؤثر على صحة عمل البرامج. يهمل المبرمجون المبتدئون عادةً تنسيق الشيفرات، نظرًا لأن جلّ تركيزهم ينصب على المفاهيم البرمجية وصياغة الجمل البرمجية وبنائها، إلّا أنّه من المفيد لهم أن يعتادوا على كتابة الشيفرات بطريقة منسقة من البدايات، فالبرمجة لا تخلو من الصعوبة بطبيعة الحال، وكتابة الشيفرات بطريقة منظمة ومفهومة للآخرين (ولك أنت عندما تعود لشيفرتك مستقبلًا) من شأنه تذليل بعض من صعوبتها. فالبرمجة بطبيعتها في الغالب نشاط جماعي مشترك، حتى لو كانت انطلاقتك فيها فردية. فلو فرضنا وجود عدد من المبرمجين يعملون معًا على شيفرة مصدرية واحدة، فكيف ستكون النتيجة النهائية في حال كتب كل منهم شيفراته بطريقته الخاصّة؟ فوضى وعدم تناسق بالطبع، حتى ولو عملت الشيفرة بالنتيجة دون أخطاء برمجية. والأسوأ مثلًا أن يعمل كل منهم على تعديل تنسيق شيفرة الآخر إلى الشكل المفهوم بالنسبة له، الأمر الذي سيضيع الوقت مؤديًا إلى جدالات عديدة. فعلى سبيل المثال، القرار حول وضع مسافة بعد الفاصلة من عدمه هو تفضيل شخصي خاص بكل مبرمج، طالما أنّه يتبع نفس المنوال دائمًا، بما يشبه القرار حول اتخاذ الجانب الأيمن أو الأيسر من الطريق أثناء القيادة، فليس المهم الجهة التي اختارها كل سائق طالما أنّه لا يغيّر مساره. الدليل الإرشادي PEP 8 لكيفية تنسيق الشيفرات لعلّ اتباع دليل إرشادي يمثّل طريقة سهلة لكتابة شيفرات بمقروئية عالية، والدليل الإرشادي بالتعريف هو مستند يتضمن مجموعة من قواعد التنسيق التي يجب اتباعها أثناء إنشاء أي مشروع برمجي، وما PEP8 الذي هو الإصدار الثامن من مستند تحسين مقروئية بايثون، سوى دليل إرشادي مكتوب من قبل فريق تطوير بايثون، إلّا أنّ بعض الشركات البرمجية أنشأت دليلها الإرشادي الخاص. يمكنك الاطلاع على الدليل PEP 8 بزيارة python.org. ويرى العديد من مبرمجي بايثون الدليل PEP 8 كمجموعة من القواعد المبالغ بصرامتها، في حين يرى منشؤوه العكس. إذ يذكّر القسم "رفض التغيير هو سمة العقول غير الناضجة" من الدليل القراء بأن الدافع الرئيسي لاتباع دليل إرشادي موحّد هو زيادة مقروئية الشيفرات في المشاريع من خلال الحفاظ على تنسيق معيّن بدلًا من التنسيقات الفردية المختلفة. ومن الممكن عدّ PEP 8 متساهلًا لدرجة أنه يقدم نصيحة مفادها "كن أنت الربان، قرر متى من المجدي التخلّي عن قواعدك الخاصة، إذ يصعب في بعض الأحيان تطبيق إرشادات التنسيق على شيفرتك، امتثل لتقييمك الخاص عند ترددك حيال الأفضل"، وسواءً اتبعت كامل إرشادات PEP 8 أو بعضًا منها أو حتى إن لم تتبعها أصلًا، فهو يستحق القراءة في كافّة الأحوال. ونظرًا لاستخدامنا منسّق Black، فإن شيفراتنا ستتبع في تنسيقها دليل Black الإرشادي المتوافق تمامًا مع قواعد PEP 8. ولابد من تعلمك لهذه الأصول وكيفية تطبيقها يدويًا، فقد لا يكون منسّق Black متوفر دائمًا بسهولة، ناهيك عن كون القواعد التنسيقية التي ستتعلمها في هذا المقال عامّة وتتطبق على لغات البرمجة الأخرى، والتي قد لا تتضمّن منسقًا تلقائيًا. فأنا على الصعيد الشخصي لا أحب كامل تنسيقات Black، ولكن أليست أفضل من لا شيء؟ لذا أراها كحل وسطي جيد، إذ يستخدم Black طرقًا في التنسيق يمكن لأي مبرمج التكيّف معها، مقللًا بذلك من الوقت المخصص للجدالات، ومتيحًا وقتًا أكبر للتركيز في صلب العمل. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن التباعد الأفقي لعل تلك المسافة الفارغة البسيطة لا تقل بأهميتها للمقروئية عن أهمية الشيفرة بحد ذاتها. إذا تساعد هذه المسافات على فصل الأجزاء المختلفة من الشيفرة عن بعضها البعض، جاعلةً التمييز فيما بينها أسهل. سنتعرّف في هذا القسم على مفهوم التباعد الأفقي، الذي يعرّف بأنه تضمين مسافة فارغة في السطر الواحد من الشيفرة، ويتضمّن هذا المفهوم المسافة البادئة أيضًا. استعمال مفتاح المسافة الفارغة Space لإنشاء مسافة بادئة تعرّف المسافة البادئة بأنها ذلك الفراغ المتموضع في بداية أحد أسطر الشيفرة، ولإضافتها من الممكن استخدام أحد مفتاحي إضافة المحارف الفارغة في لوحة المفاتيح، وهما مفتاح المسافة الفارغة Space ومفتاح الجدولة Tab. ورغم كون أي منهما يؤدّي هذا الغرض، إلا أن استخدام مفتاح المسافة الفارغة يعد الإجراء الأفضل. والسبب في ذلك يعود لكون كل من المفتاحين يعمل بطريقة مختلفة، ففي حين يُصيَّر مفتاح Space على الشاشة بهيئة قيمة محرفية تساوي فراغًا واحدًا بالشكل ‘ ‘، إلا أن مفتاح Tab يُصيّر على هيئة قيمة محرفية تقابل محرف الهروب t\ ما يجعل حجم الفراغات غامضًا، فعلى الغالب يعرض هذا المفتاح حجمًا متغيرًا للفراغات مُحددًا علامة جدولة ليبدأ النص التالي بعدها، إذا تتموضع علامة الجدولة عند كل ثمان فراغات وفقًا لعرض المستند، والمثال التالي يبيّن الفرق من خلال شيفرة مكتوبة في الصَدَفَة التفاعلية، إذ استخدمنا بدايةً مفتاح Space للفصل بين الكلمات ومن ثم مفتاح Tab: >>> print('Hello there, friend!\nHow are you?') Hello there, friend! How are you? >>> print('Hello\tthere,\tfriend!\nHow\tare\tyou?') Hello there, friend! How are you? وبذلك وبما أن علامات الجدولة تُحدّد مسافات بيضاء متفاوتة العرض، فيجب تجنب استخدامها في الشيفرة المصدرية. مع ملاحظة أن معظم محررات النصوص وبيئات التطوير المتكاملة تُدرج تلقائيًا أربع أو ثمان محارف فراغ لدى الضغط على مفتاح Tab، بدلًا من استخدام علامة جدولة (متغيرة مع عرض الملف). كما أنه من غير الممكن استخدام كلا المفتاحين في إنشاء المسافات البادئة في نفس الكتلة من الشيفرة، ولطالما كان استخدامهما معًا في إنشاء المسافات البادئة مصدرًا لأحد الأخطاء المزعجة في إصدارات بايثون القديمة، لدرجة أن الإصدار الثالث من بايثون لن يشغّل شيفرة بمسافات بادئة مبنية بهذه الطريقة، مُظهرًا رسالة الاستثناء الآتي: TabError: inconsistent use of tabs and spaces in indentation دلالةً على الاستخدام غير المتناسق لعلامات الجدولة والفراغات في إنشاء المسافات البادئة، في حين أنّ المنسّق Black يحوّل أي علامة جدولة مضافة كمسافة بادئة إلى أربعة فراغات تلقائيًا. وللحفاظ على طول ثابت لكل مستوى مسافة بادئة، فإن الإجراء الشائع في شيفرات بايثون هو استخدام أربع فراغات لكل مستوى، والمثال التالي يبين هذه الفكرة، وفيه استخدمنا النقطة بدلًا من الفراغ لجعلها مرئية: def getCatAmount(): ....numCats = input('How many cats do you have?') ....if int(numCats) < 6: ........print('You should get more cats.') ولاستخدام مسافة بادئة بأربعة محارف ميزاتٍ عديدة مقارنةً بأي خيار آخر، فمثلًا استخدام ثمان فراغات لكل مستوى من مستويات المسافة البادئة سيؤدي بالنتيجة إلى تجاوز الحد الأقصى لعدد المحارف في السطر الواحد، في حين أن استخدام فراغين فقط سيجعل ملاحظة وجود مسافة بادئة أصلًا أمرًا صعبًا نظرًا للفرق البسيط مع الأسطر العادية، أما باقي الاحتمالات من قبيل استخدام ثلاثة أو ستة فراغات فهو أمر غير شائع لدى المبرمجين، فهم كعالم الحوسبة إجمالًا، متحيزين لمضاعفات العدد 2 أي 2 و4 و6 و8 وهكذا. التباعد ضمن السطر الواحد يتضمّن التباعد الأفقي أكثر من مجرد مفهوم المسافة البادئة، فللفراغات دورها المهم في تسهيل التمييز البصري ما بين الأجزاء المختلفة من السطر البرمجي الواحد، فبدونها سيبدو السطر كثيفًا مكتظًا صعب التحليل والفهم، وتبيّن الأقسام الفرعية التالية بعض قواعد استخدام الفراغات التي يجب اتباعها. استخدم فراغ واحد ما بين العمليات والمعرفات إن لم تترك فراغًا ما بين العمليات والمعرفات (المتغيرات، الدوال، الكائنات …)، ستبدو الشيفرة وكأنها متداخلة. فمثلًا السطر التالي يتضمّن فراغات ما بين العمليات والمتغيرات: YES: blanks = blanks[:i] + secretWord[i] + blanks[i + 1 :] وفي حال حذف تلك الفراغات، فسيبدو بالشكل: NO: blanks=blanks[:i]+secretWord[i]+blanks[i+1:] تستخدم هذه الشيفرة في كلتا الحالتين معامل الجمع + لجمع ثلاثة قيم، ولكن في حالة عدم استخدام فراغات، قد يبدو معامل الجمع الموجود ضمن المتغير [:blanks[i+1 على أنّه عملية جمع رابعة، إلا أن استخدام الفراغات سيوضح أن معامل الجمع هذا جزء من قيمة الشريحة slice (بنية في بايثون تمكننا من الوصول إلى أجزاء من التسلسلات مثل السلاسل النصية والجداول والقوائم) الموجودة بين القوسين. لا تضع مسافات قبل الفواصل وضع مسافة واحدة بعدها نستخدم محرف الفاصلة للفصل ما بين عناصر القوائم والقواميس والمعاملات ضمن بناء تعريف الدوال، إذ يجب عدم ترك أي فراغ قبل الفاصلة، مع ترك فراغ وحيد بعدها، كما في المثال التالي: YES: def spam(eggs, bacon, ham): YES: weights = [42.0, 3.1415, 2.718] وإلا سينتهي الأمر بشيفرة مجمّعة ذات مقروئية منخفضة، كما يلي: NO: def spam(eggs,bacon,ham): NO: weights = [42.0,3.1415,2.718] كما من الضروري عدم وضع مسافة قبل الفاصلة، لأن ذلك سيجذب النظر نحوه: NO: def spam(eggs , bacon , ham): NO: weights = [42.0 , 3.1415 , 2.718] يضيف منسّق Black تلقائيًا مسافة بعد كل فاصلة، كما يحذف أي فراغات مُضافة قبلها. لا تستخدم أي فراغات قبل أو بعد النقاط رغم كون قواعد بايثون تسمح باستخدام مسافات قبل وبعد محرف النقطة المشير إلى بدء استخدام سمة من سمات بايثون، ولكن من المفضّل عدم استخدام هذه المسافات، فبدونها يصبح الارتباط بين الكائن وسمته أوضح، كما في المثال التالي: YES: 'Hello, world'.upper() أما في حال استخدام مسافة قبل النقطة أو بعدها، فقد يبدو الكائن وسمته غير مرتبطان، كما في الحالة: NO: 'Hello, world' . upper() يحذف منسق Black تلقائيًا أي مسافات قبل أو بعد النقاط. لا تضع مسافات بعد أسماء الدوال أو التوابع أو حاويات البيانات من السهل تمييز أسماء الدوال والتوابع كونها تُتبع دومًا بقوسين، لذا يجب عدم وضع مسافة بين اسم الدالة أو التابع وقوس البداية، فعادةً ما يتم استدعاء الدوال بالشكل: YES: print('Hello, world!') ومع استخدام مسافة قبل قوس البداية، قد يبدو استدعاء الدالة الواحدة على أنه أمران مستقلان، كما في المثال: NO: print ('Hello, world!') لذا يحذف منسّق Black أي مسافات بين اسم الدالة أو التابع وقوس بدايتها. وكذلك الأمر بالنسبة للأقواس المعقوفة الخاصة بمؤشرات المصفوفات أو الشرائح أو المفاتيح، فعادةً ما نصل إلى العناصر الموجودة في حاويات البيانات (كالقوائم والقواميس والصفوف) دون إضافة مسافات بين اسم المتغير وقوس البداية المعقوف، كما في المثال التالي: YES: spam[2] YES: spam[0:3] YES: pet['name'] ومع إضافة مسافات قد تبدو الشيفرة مجددًا وكأنها أمران منفصلان: NO: spam [2] NO: spam [0:3] NO: pet ['name'] لذا يحذف منسّق Black أي مسافات كائنة بين اسم المتغير وقوس البداية المعقوف. لا تضع مسافات بعد أقواس البداية أو قبل أقواس النهاية بأنواعها يجب عدم استخدام مسافات في فصل الأقواس بأنواعها الهلالية () والمعقوصة {} والمعقوفة [] عن محتوياتها، فيجب أن تبدأ المعاملات في عبارة تصريح عن دالة ما أو القيم ضمن قائمة بعد قوس بدايتها وتنتهي قبل قوس نهايتها مباشرةً. YES: def spam(eggs, bacon, ham): YES: weights = [42.0, 3.1415, 2.718] إذ يجب عدم إضافة أي مسافات بعد قوس البداية أو قبل قوس النهاية، سواء كانت هذه الأقواس هلالية أم معقوفة: NO: def spam( eggs, bacon, ham ): NO: weights = [ 42.0, 3.1415, 2.718 ] فإضافة هذه الأقواس لا يحسّن من مقروئية الشيفرة وبالتالي لا تقدّم أي قيمة مضافة، لذا يحذف منسّق Black هذه المسافات تلقائيًا في حال وجودها. ضع فراغين قبل تعليقات نهاية السطر إن أردت إضافة تعليق إلى نهاية أحد أسطر الشيفرة، فيجب إضافة مسافتين بعد نهاية السطر البرمجي وقبل المحرف # الدال على بداية التعليق، كما في المثال: YES: print('Hello, world!') # Display a greeting. فبوجود الفراغين يصبح من السهل تمييز الشيفرة عن التعليق، أما في حال وجود مسافة واحدة أو عدم وجود مسافة أبدًا كأسوأ سيناريو، سيكون من الصعب الفصل بصريًا بين الشيفرة والتعليق. NO: print('Hello, world!') # Display a greeting. NO: print('Hello, world!')# Display a greeting. لذا يضيف منسّق Black تلقائيًا مسافتين ما بين نهاية السطر البرمجي وبداية التعليق. وأنا شخصيًا لا أنصح عمومًا باستخدام التعليقات في نهاية السطر، إذ أنها قد تجعل السطر طويلًا جدًا فلا يظهر كاملًا على الشاشة. التباعد الرأسي يعرّف التباعد الرأسي بأنه إضافة أسطر فارغة بين أسطر الشيفرة، بما يشبه حالة بدء فقرة جديدة في الكتب، الأمر الذي يجنبنا رؤية كتلة كبيرة متماسكة من النص، فباستخدام التباعد الرأسي نستطيع تجميع أسطر معينة من الشيفرة مع بعضها البعض ضمن مجموعة، مميزين إياها عن المجموعات الأخرى. ويشتمل الدليل PEP 8 على العديد من الإرشادات حول إدراج الأسطر الفارغة ضمن الشيفرات، إذ ينص على وجوب فصل كل من الدوال والأصناف بسطرين فارغين، وفصل التوابع الموجودة ضمن الأصناف بسطر فارغ واحد، ويتبع منسّق Black هذه القواعد تلقائيًا، مضيفًا الأسطر الفارغة اللازمة وحاذفًا الزائد منها، ليحوّل مثلًا شيفرة مكتوبة بالشكل: NO: class ExampleClass: def exampleMethod1(): pass def exampleMethod2(): pass def exampleFunction(): pass لتصبح تلقائيًا بالشكل: YES: class ExampleClass: def exampleMethod1(): pass def exampleMethod2(): pass def exampleFunction(): pass مثال على التباعد الرأسي إنّ منسق Black غير قادر على اتخاذ قرار حيال أماكن وضع سطر فارغ ضمن الدوال أو التوابع أو النطاق العام، فكيفية تجميع هذه السطور البرمجية وفصلها عن غيرها هو تفضيل شخصي يعود للمبرمج. لنأخذ على سبيل المثال صنف التحقق من البريد الإلكتروني EmailValidator من الملف validators.py في إطار عمل تطبيق ويب محرك القوالب جانغو، مع ملاحظة أنه من غير الضروري الآن فهم آلية عمل هذه الشيفرة، بل سنركّز على كيفية الفصل ما بين توابع الاستدعاء ()__call__ باستخدام الأسطر الفارغة إلى أربعة مجموعات: --snip-- def __call__(self, value): 1 if not value or '@' not in value: raise ValidationError(self.message, code=self.code) 2 user_part, domain_part = value.rsplit('@', 1) 3 if not self.user_regex.match(user_part): raise ValidationError(self.message, code=self.code) 4 if (domain_part not in self.domain_whitelist and not self.validate_domain_part(domain_part)): # Try for possible IDN domain-part try: domain_part = punycode(domain_part) except UnicodeError: pass else: if self.validate_domain_part(domain_part): return raise ValidationError(self.message, code=self.code) --snip-- فرغم عدم وجود تعليقات لوصف عمل هذا الجزء من الشيفرة، إلا أن الأسطر الفارغة تشير لكون المجموعات مستقلة مفاهيميًا عن بعضها البعض، فالمجموعة الأولى تتحقق من وجود الرمز @ في المعامل value من معاملات التابع، الأمر المختلف تمامًا عن مهمة المجموعة الثانية المتمثلة في تقسيم السلسلة النصية المعبرة عن عنوان البريد الإلكتروني الممرر إلى المعامل value إلى قسمين، ليتم إسناد القسم الأول إلى المتغير user_part والثاني إلى المتغير domain_part، لتستخدم المجموعتين الثالثة والرابعة هذين المتغيرين للتحقق من قسمي اسم المستخدم والنطاق من البريد الإلكتروني على التوالي. ورغم كون المجموعة الرابعة من الشيفرة السابقة تتكوّن من 11 سطرًا، متجاوزةً بذلك باقي المجموعات، إلّا أن هذه الأسطر جميعها متعلقة بنفس المهمّة المتمثلة في التحقق من صحّة نطاق البريد الإلكتروني، وفي مثل هذه الحالات، إن شعرت بأنّ المهمّة الواحدة مُجزأة إلى مهام فرعية، فمن الممكن الفصل فيما بينها بأسطر فارغة. فالمبرمج في مثالنا السابق ارتأى أن الأسطر المتعلقة بالتحقق من صحة نطاق البريد الإلكتروني يجب أن تتبع جميعها لمجموعة واحدة، وقد لا يشاطره مبرمجون آخرون هذا الرأي، وبالتالي ونظرًا لكون هذا الأمر كما ذكرنا هو قرار شخصي، فإن منسّق Black لا يعدل على الأسطر الفارغة الموجودة ضمن الدالة الواحدة أو التابع الواحد. أفضل الممارسات حيال التباعد الرأسي لعل إحدى ميزات بايثون غير المعروفة على نطاق واسع هي إمكانية الفصل بين عدة عبارات في السطر واحد باستخدام الفاصلة المنقوطة، بمعنى أنّه مثلًا من الممكن كتابة السطرين: print('What is your name?') name = input() على سطر واحد بشرط الفصل بينهما بفاصلة منقوطة: print('What is your name?'); name = input() وكما هو الحال مع استخدام الفاصلة العادية، يجب عدم إضافة مسافة قبل الفاصلة المنقوطة، وإضافة مسافة واحدة بعدها. أمّا بالنسبة للعبارات البرمجية التي تنتهي بنقطتين رأسيتين، كعبارات if و for و while و def أو جمل بناء الأصناف، فمن الممكن كتابتها على سطر واحد، فمثلًا من الممكن كتابة استدعاء الدالة ()print في الشيفرة التالية: if name == 'Alice': print('Hello, Alice!') على نفس السطر مع العبارة الشرطية if، بالشكل: if name == 'Alice': print('Hello, Alice!') إلا أن حقيقة كون قواعد بايثون تسمح بتضمين عدة عبارات في السطر الواحد لا تعني أنّ ذلك يمثّل إجراءً جيد دومًا، فقد يؤدي بالنتيجة إلى أسطر برمجية طويلة مكتظة المحتويات، ما يجعل قابلية قراءتها أقل، لذا يقسّم منسّق Black العبارات في مثل هذه الحالات على أسطر منفصلة. وكذلك الأمر بالنسبة لاستيراد الوحدات، إذ من الممكن استيراد عدة وحدات باستخدام أمر واحد في سطر واحد، كما في المثال: import math, os, sys ومع ذلك، فإن الدليل PEP 8 يوصي بفصل هذه العبارات، لتتم عملية استيراد كل وحدة في سطر مستقل، بالشكل: import math import os import sys فمع كتابة الاستيرادات في أسطر منفصلة، سيكون من الأسهل والأسرع اكتشاف وجود أي إضافات أو نواقص في الوحدات المستوردة لدى مقارنة التغييرات باستخدام أداة اكتشاف الاختلافات diff من أدوات نظم إدارة الشيفرة مثل git. كما ينصح الدليل PEP 8 بتجميع عبارات الاستيراد ضمن ثلاث مجموعات، كما يلي: الوحدات من مكتبة بايثون المعيارية، مثل الوحدة math و os و sys. الوحدات الخارجية، مثل Selenium و Requests و Django. الوحدات الداخلية التي تمثّل جزءًا من البرنامج. وإجمالًا لا يغير منسق Black من تنسيق الاستيرادات في الشيفرة، نظرًا لكون الإرشادات المتعلقة بالاستيرادات اختيارية. الخاتمة رغم كون التنسيق الجيد أمرًا غير مرئي لدى تنفيذ البرامج، إلا أن التنسيق السيئ قد يجعل قراءة الشيفرات أمرًا صعبًا ومحبطًا، ومما لا خلاف فيه بأن نمط التنسيق إجمالًا هو تفضيل شخصي، الأمر الذي لا يتنافى وأصول مجال تطوير البرمجيات، مع وجود خطوط عريضة متفق عليها تحدد التنسيق الجيد من السيء، تاركةً فيما بينها هامش من الحرية للتفضيلات الشخصية. يمكن عد بناء جمل بايثون البرمجي أمرًا مرنًا من ناحية نمط التنسيق، وفي الواقع في حال لدى كتابتك لشيفرة لن يقرأها أحد سواك، فلك مطلق الحرية بتنسيقها كما تريد، إلا أن معظم أعمال التطوير البرمجية جماعية وليست فردية، فسواءً كنت تعمل مع مبرمجين آخرين على مشروعٍ ما، أو كنت ببساطة ستطرح سؤالًا متعلقًا بشيفرتك على أحد المبرمجين الخبراء طالبًا مساعدته، في كلتا الحالتين يعد تنسيق الشيفرة وفق النمط المقبول المعتمد أمرًا بالغ الأهمية. إن تنسيق الشيفرات يدويًا ضمن المحررات قد تكون مهمَّة مملة، إذ من الممكن أتمتتها باستخدام أداة مثل Black. استعرضنا في هذا المقال عددًا من الإرشادات التي يتبعها Black جاعلًا شيفرتك بمقروئية أعلى، بما يتضمّن التباعد الأفقي والرأسي اللذان يجنباننا اكتظاظ الشيفرات، ما يسهل قراءتها بالنتيجة، بالإضافة إلى تحديد العدد الأعظمي من المحارف المتاح للسطر الواحد، إذ يعمل Black على فرض هذه القواعد، مجنبًا الفريق البرمجي أي جدالات محتملة حول آلية التنسيق الأفضل. لقد استعرضنا في هذا المقال التنسيقات الأساسية التي يجريها Black، ويمكنك الاطلاع على دليل Black الإرشادي لكامل التنسيقات بزيارة black.readthedocs.io. ترجمة -وبتصرف- للجزء الأول من الفصل الثالث Code Formatting With Black من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigarti. اقرأ أيضًا المقال السابق: متغير البيئة PATH والعمل دون نافذة سطر الأوامر في بايثون أساسيات البرمجة بلغة بايثون كيفية تنسيق النصوص في بايثون 3 كيفية استخدام آلية تنسيق السلاسل النصية في بايثون 3
-
تعرفنا في مقال سابق من هذه السلسة على الأوامر الشائعة في نافذة سطر أوامر بايثون، وفيه عرّفنا مفهوم متغيرات البيئة وكيفية عرضها. ولعلّ أحد أهم هذه المتغيرات للفهم هو متغير المسار PATH، لدوره المحوري في كيفية تعامل نافذة سطر الأوامر مع الأوامر المكتوبة ضمنها. فعندما نكتب أمرًا أو اسم برنامج في نافذة سطر الأوامر فإنها تتفحص المجلدات الموجودة في متغير البيئة PATH بحثًا عن ذلك الاسم، ومن الضروري فهم هذه الآلية ما يساعد على تفسير سبب ظهور رسائل الخطأ command not found التي قد تواجهنا. ما هو متغير البيئة PATH؟ عند كتابة أمر ما مثل الأمر python في نظام التشغيل ويندوز أو الأمر python3 في نظامي التشغيل ماك أو إس ولينكس، تتحقق نافذة موجه الأوامر من وجود برنامج بذلك الاسم في المجلد الحالي. وإن لم تجده هناك، فستبحث في المجلدات المذكورة في متغير البيئة PATH. كمثال على ذلك، يوجد البرنامج python.exe على حاسوب يعمل بنظام تشغيل ويندوز في مجلد C:\Users\Al\AppData\Local\Programs\Python\Python38، ولتشغيله علينا إدخال المسار الآتي كاملًا: C:\Users\Al\AppData\Local\Programs\Python\Python38\python.exe أو الانتقال إلى ذلك المجلد ومن ثم إدخال الأمر python.exe. إلا أن المسار الطويل هذا يتطلب الكثير من الكتابة، لذا من الممكن إضافة هذا المسار إلى متغير البيئة PATH، وبذلك وبمجرد كتابة الأمر python.exe في أي موقع، سيبحث برنامج سطر الأوامر عن برنامج بنفس الاسم في المجلدات الموجودة في PATH، ما يوفر عليك عناء كتابة المسارات الكاملة للملفات كل مرة. وبما أن متغيرات البيئة لا يمكن أن تحتوي إلا على سلسلة مفردة، فإن إضافة أسماء عدة مجلدات إلى متغير البيئة PATH يتطلب استخدام صيغة خاصة، في نظام التشغيل ويندوز نستخدم الفاصلة المنقوطة للفصل بين أسماء المجلدات. من الممكن عرض قيمة المتغير PATH الحالية باستخدام الأمر path، بالشكل: C:\Users\Al>path C:\Path;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem; --snip-- C:\Users\Al\AppData\Local\Microsoft\WindowsApps أما في نظامي التشغيل ماك أو إس ولينكس، فيتم الفصل بين أسماء المجلدات باستخدام الفاصلة، بالشكل: al@ubuntu:~$ echo $PATH /home/al/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin إن ترتيب أسماء المجلدات ضروري، فإن كان لدينا ملفان يحملان الاسم someProgram.exe في المجلدين C:\WINDOWS\system32 و C:\WINDOWS فإن إدخال الأمر someProgram.exe سيشغل البرنامج الموجود في المجلد C:\WINDOWS\system32 لأن هذا المجلد يظهر أولًا ضمن متغير البيئة PATH. إن لم يكن البرنامج أو الأمر الذي أدخلناه موجودًا في دليل العمل الحالي أو أي من المجلدات المخزنة في المتغير PATH، فعندها ستعرض نافذة سطر الأوامر رسالة خطأ مثلاً command not found أو not recognized as an internal or external command بمعنى أن الأمر غير موجود أو أنه أمر خارجي أو غير معرف، وإن كنت متأكدًا من عدم وجود أي خطأ إملائي، فعليك التأكد من وجود المجلد المتضمن للبرنامج المطلوب ضمن متغير البيئة PATH. تغيير متغير البيئة PATH الخاص بسطر الأوامر من الممكن تغيير متغير البيئة PATH الخاص بنافذة موجه الأوامر ليتضمن المزيد من المجلدات، وتختلف طريقة إضافة مجلدات إلى المتغير PATH بعض الشيء بين أنظمة التشغيل المختلفة، ففي نظام التشغيل ويندوز يمكن تنفيذ الأمر path لإضافة مجلد جديد إلى قيمة المتغير PATH الحالية، بالشكل: 1 C:\Users\Al>path C:\newFolder;%PATH% 2 C:\Users\Al>path C:\newFolder;C:\Path;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem; --snip-- C:\Users\Al\AppData\Local\Microsoft\WindowsApps وبذلك يتوسع المسار %PATH% (المشار له في السطر رقم 1) إلى القيمة الجديدة لمتغير البيئة PATH، عبر إضافة اسم المجلد الجديد مع فاصلة منقوطة إلى بداية القيمة الحالية للمتغير PATH، وبتشغيل الأمر path مجددًا، ستلاحظ القيمة الجديدة للمتغير. أم في في نظامي التشغيل ماك أو إس ولينكس، فيمكن ضبط متغير البيئة PATH بصيغة مشابهة لعبارة الاسناد التقليدية في بايثون، بالشكل: 1 al@al-VirtualBox:~$ PATH=/newFolder:$PATH 2 al@al-VirtualBox:~$ echo $PATH /newFolder:/home/al/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin وبذلك يتوسع المسار %PATH% (المشار له في السطر رقم 1) إلى القيمة الجديدة لمتغير البيئة PATH، عبر إضافة اسم المجلد الجديد مع فاصلة منقوطة إلى بداية القيمة الحالية للمتغير PATH، وبتشغيل الأمر path مجددًا، ستلاحظ القيمة الجديدة للمتغير. لكن التغييرات في قيمة المتغير PATH الناتجة عن الطريقتين السابقتين تسري فقط على نافذة موجه الأوامر الحالية، وأي برنامج يتم تشغيله عبرها بعد إجراء التغيير، لكن لدى فتح نافذة موجه أوامر جديدة، فلن تكون هذه التغيرات مطبقة عليها، لذا لإضافة المجلدات بطريقة دائمة، يجب تغيير مجموعة متغيرات البيئة الخاصة بنظام التشغيل. إضافة المجلدات بطريقة دائمة إلى المتغير PATH في نظام التشغيل ويندوز يحتوي نظام التشغيل ويندوز على مجموعتين من متغيرات البيئة: متغيرات البيئة الخاصة بالنظام (المطبقة على جميع المستخدمين)، ومتغيرات البيئة الخاصة بالمستخدم (والتي تتجاوز متغيرات البيئة الخاصة بالنظام وتطبق فقط على المستخدم الحالي). ولتعديل هذه المتغيرات، نضغط على قائمة ابدأ ثم نكتب في مربع البحث Edit environment variables for your account، فتفتح نافذة متغيرات البيئة كما هو مبين في الشكل التالي أدناه. نختار المتغير Path من قائمة متغيرات المستخدم (وليس من قائمة متغيرات النظام) ثم نضغط على تحرير Edit ونضيف اسم مجلد جديد في حقل النص الذي يظهر (نضيف فاصلة منقوطة للفصل بين الأسماء) ثم نضغط موافق OK. ليس من السهل العمل مع هذه الواجهة، لذا إن لم تكن متمرسًا بتغيير متغيرات البيئة في ويندوز ننصحك بتحميل البرنامج المجاني Rapid Environment Editor من الرابط https://www.rapidee.com/. بعد تثبيت هذا البرنامج يجب تشغليه كمسؤول لتعديل متغيرات البيئة الخاصة بالنظام، لذا نضغط على زر ابدأ ونكتب في مربع البحث Rapid Environment Editor ونضغط بزر الفأرة الأيمن على أيقونة البرنامج ونختار التشغيل كمسؤول. يمكننا تعديل متغير PATH الخاص بالنظام بشكل دائم من موجه الأوامر باستخدام الأمر setx، بالشكل: C:\Users\Al>setx /M PATH "C:\newFolder;%PATH%" كما يجب تشغيل موجه الأوامر كمسؤول لتنفيذ الأمر setx. نافذة متغيرات البيئة في نظام التشغيل ويندوز. إضافة مجلدات بطريقة دائمة إلى متغير المسار PATH في ماك أو إس ولينكس لإضافة مجلدات إلى متغير البيئة PATH في جميع نوافذ موجهات الأوامر في نظامي التشغيل ماك أو إس ولينكس، لابد من تعديل الملف النصي .bashrc في المجلد الرئيسي بإضافة السطر التالي: export PATH=/newFolder:$PATH يعمل هذا السطر على تعديل المتغير PATH لجميع موجهات الأوامر التي ستشغل مستقبلًا. أما في إصدار ماك أو إس كاتالينا أو الإصدارات الأحدث، فقد تغير برنامج الصدفة الافتراضي من Bash إلى Z Shell، وبالتالي يجب تعديل الملف النصي .zshrc في المجلد الرئيسي بدلًا من الملف النصي .bashrc. تشغيل برامج بايثون دون استخدام نافذة سطر الأوامر جميعنا في الغالب يعلم كيفية تشغيل البرامج باستخدام المشغل المزود به نظام التشغيل، فمثلًا يحتوي نظام ويندوز على قائمة ابدأ، في حين يحتوي نظام ماك أو إس على Finder و Dock، أما نظام أوبنتو لينكس فيحتوي على Dash، إذ تضيف البرامج نفسها إلى هذه المشغلات فور تثبيتها، كما من الممكن تشغيل البرامج من مستكشف الملفات (مثل File Explorer في ويندوز و Finder في ماك أو إس و Files في أبونتو لينكس) وذلك بالنقر المزدوج على أيقونة البرنامج. إلا أن هذه الطرق لا تنطبق على برامج بايثون، فالنقر المزدوج على ملف ذو اللاحقة .py سيفتح برنامج بايثون ضمن محرر النصوص أو بيئة تطوير متكاملة IDE بدلاً من تشغيله، ولو أردت تشغيل بايثون مباشرة، ستفتح الصدفة التفاعلية فحسب. إن الطرق الأكثر شيوعًا لتشغيل برامج بايثون هي إما فتحها بواسطة بيئة تطوير متكاملة ما والضغط على خيار التشغيل run، أو تنفيذه في موجه الأوامر. كلا الطريقتين مملتين إن كان كل ما تريده هو مجرد تشغيل برنامج بايثون. فبدلًا من ذلك، من الممكن ضبط برامج بايثون لتعمل بسهولة باستخدام المشغل الخاص بنظام التشغيل، كأي برنامج آخر. الأقسام التالية تبين كيفية القيام بذلك في أنظمة التشغيل المختلفة. تشغيل برامج بايثون على نظام التشغيل ويندوز يمكن تشغيل برامج بايثون في نظام التشغيل ويندوز بعدة طرق، فبدلًا من فتح نافذة موجه الأوامر، من الممكن الضغط على زري WIN+R في لوحة المفاتيح لفتح نافذة المشغل Run، وفيها نكتب py c:\path\to\yourScript.py كما هو مبين في الشكل التالي، حيث أن البرنامج py.exe مثبت في المسار C:\Windows\py.exe الموجود ضمن متغير البيئة PATH، أما اللاحقة exe. فهي اختيارية عند تشغيل البرامج. نافذة Run في نظام التشغيل ويندوز إلا أن الطريقة السابقة تتطلب إدخال المسار الكامل للنص البرمجي، كما أن نافذة موجه الأوامر التي ستعرض خرج البرنامج، ستغلق تلقائيًا بمجرد انتهاء التنفيذ وبالتالي قد لا نلاحظ بعض نتائج الخرج. يمكن حل هذه المشاكل بإنشاء نص برمجي دفعي batch script، وهو ملف نصي صغير ذو لاحقة bat. يمكن له تشغيل عدة موجهات أوامر دفعة واحدة بما يشبه النص البرمجي للصدَفَة shell script في نظامي التشغيل ماك أو إس ولينكس، ومن الممكن استخدام أي محرر نصوص (كبرنامج المفكرة) لإنشاء هذه الملفات، وكمثال سننشئ ملفًا نصيًا جديدًا يحتوي على الأمرين التاليين: @py.exe C:\path\to\yourScript.py %* @pause مع مراعاة استبدال المسار في الشيفرة السابقة بالمسار المطلق للبرنامج لديك، واحفظ الملف باللاحقة bat. (مثلاً yourScript.bat)، إن رمز @ في بداية كل أمر تمنع عرضه في نافذة موجه الأوامر، ويعمل الرمز *% على توجيه أي وسطاء موجه أوامر مدخلة بعد اسم الملف الدفعي إلى شيفرة بايثون، التي تقرأ بدورها الوسطاء من القائمة sys.argv. وبذلك سيغنيك هذا الملف الدفعي عن كتابة المسار المطلق الكامل لبرنامج بايثون في كل مرة تريد تشغيله فيها، كما أن الأمر pause@ سيضيف عبارة press any key to continue… إلى نهاية الخرج وذلك لمنع النافذة من الإغلاق بسرعة. وفي هذا الصدد يُنصح بوضع جميع الملفات الدفعية وملفات py. في مجلد واحد موجود مسبقًا في متغير البيئة PATH كالمجلد الرئيسي C:\Users\ وبذلك وبعد إعداد الملف الدفعي يمكنك تشغيل شيفرات بايثون من خلال الضغط على WIN+R وإدخال اسم الملف الدفعي (إدخال لاحقة bat. هو أمر اختياري) والضغط على زر ENTER. تشغيل برامج بايثون في نظام التشغيل ماك أو إس يمكن إنشاء نص برمجي للصدَفَة في نظام التشغيل ماك أو إس لتشغيل شيفرات بايثون بإنشاء ملف نصي ذو لاحقة command. لذا سننشئ ملفًا باستخدام أحد محررات النصوص (مثل TextEdit) ونكتب ضمنه التالي: #!/usr/bin/env bash python3 /path/to/yourScript.py نحفظ الملف في المجلد الرئيسي، ومن نافذة موجه الأوامر نجعل شيفرة الصدفة السابقة قابلة للتنفيذ بتشغيل الأمر chmod u+x yourScript.command. وبذلك يصبح من الممكن الضغط على أيقونة Spotlight (أو على شريط Command-Space) وإدخال اسم الملف المتضمن شيفرة بايثون المراد تنفيذها، ليتولى النص البرمجي للصدَفَة هذا الأمر. تشغيل برامج بايثون في نظام أوبنتو لينكس لا يوجد طريقة سريعة لتشغيل نصوص بايثون البرمجية في نظام أبونتو لينكس كما هو الحال في ويندوز وماك أو إس، لكن من الممكن اختصار بعض الخطوات اللازمة، لذا تأكد بدايةً من وجود الملف ذو اللاحقة py. في المجلد الرئيسي، واكتب الأمر التالي في السطر الأول من الملف ذو اللاحقة py.، بالشكل: #!/usr/bin/env python3 يدعى هذا بسطر shebang دلالةً على الرمز #! الذي يبدأ به، مخبرًا نظام أبونتو بأنه عند تشغيل هذا الملف نريد استخدام python3 في ذلك، ومن ثم نضيف سماحية التنفيذ إلى الملف هذا باستخدام الأمر chmod في نافذة موجه الأوامر، بالشكل: al@al-VirtualBox:~$ chmod u+x yourScript.py والآن متى ما أردت تشغيل شيفرتك المكتوبة في ملف بايثون هذا، يمكنك الضغط على مفاتيح CTR+ALT+T لفتح نافذة موجه أوامر جديدة، حيث ستكون هذه النافذة مضبوطة افتراضيًا على المجلد الرئيسي، لذلك يمكنك إدخال الأمر yourScript.py/. بسهولة لتشغيل البرنامج. إن الرمز /. ضروري كونه يخبر نظام أبونتو بأن الملف yourScript.py موجود ضمن دليل العمل الحالي (وهو نفسه المجلد الرئيسي في هذه الحالة). الخلاصة يتضمن ضبط البيئة كافة الخطوات الضرورية لجعل الحاسوب قادرًا على تشغيل برامجنا بسهولة، وتتطلب هذه العملية منا معرفة بعض المفاهيم الأولية حول كيفية عمل الحاسوب، كفهم متغيرات البيئة. فعندما نكتب أمرًا أو اسم برنامج في نافذة سطر الأوامر فإنها تتفحص المجلدات الموجودة في متغير البيئة PATH بحثًا عن ذلك الاسم، ومن الضروري فهم هذه الآلية ما يساعد على تفسير سبب ظهور رسائل الخطأ command not found التي قد تواجهنا. وتختلف قليلًا خطوات إضافة مجلدات جديدة إلى متغير البيئة PATH باختلاف أنظمة التشغيل. قد يستغرق الأمر وقتًا لتعتاد التعامل مع موجه الأوامر، نظرًا لكثرة الأوامر ووسطاء كل منها، لذا لا تتردد في البحث مطولًا على الإنترنت عن المساعدة، فهو الأمر الطبيعي الذي يقوم به مطورو البرامج يوميًا. ولا تنسَ أنّه من الممكن ضبط برامج بايثون لتعمل مباشرةً باستخدام المشغل الخاص بنظام التشغيل، كأي برنامج آخر في حال كان المطلوب ببساطة مجرد تشغيل برنامج ما. ترجمة -وبتصرف- للفصل الثاني "إعداد البيئة وواجهة سطر الأوامر" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: الأوامر الشائعة في نافذة سطر أوامر بايثون ما هو سطر الأوامر ؟ كيفية استخدام سطر أوامر بايثون التفاعلي
-
يُعد فلاسك إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا العديد من الأدوات والميزات التي من شأنها إنشاء تطبيقات ويب في لغة بايثون. أمّا SQLAlchemy، فهي أداةٌ في محرك قواعد البيانات SQL تؤمن وصولًا فعالًا وعالي الأداء إلى قواعد البيانات العلاقية، كما توفّر طرقًا للتخاطب مع العديد من محركات قواعد البيانات مثل SQLite، و MySQL، و PostgreSQ، مانحةً إمكانية الوصول إلى آليات SQL الخاصة بقواعد البيانات، كما توفّر مُخطِّط الكائنات العلاقية Object Relational Mapper -أو اختصارًا ORM- ليتيح إمكانية إنشاء الاستعلامات والتعامل مع البيانات باستخدام توابع وكائنات بسيطة في بايثون. تُعدّ Flask-SQLAlchemy إضافة لفلاسك تسهّل من استخدام SQLAlchemy ضمنه وتؤمن الأدوات والوسائل المناسبة للتعامل مع قاعدة البيانات في تطبيقات فلاسك من خلال SQLAlchemy. تُعرَّف علاقة قاعدة البيانات من النوع واحد-إلى-متعدد one-to-many بكونها علاقةً بين جدولين من قاعدة البيانات، بحيث يمكن لسجلٍ موجودٍ في أحد الجدولين الإشارة إلى عدّة سجلات من الجدول الثاني، ويشكّل تطبيق المدونة خير مثال على العلاقات في قواعد البيانات من النوع واحد-إلى-متعدد، نظرًا لارتباط الجدول الخاص بتخزين التدوينات posts مع الجدول الخاص بتخزين التعليقات comments بعلاقة من نوع واحد-إلى-متعدد؛ إذ يمكن أن يكون لكل تدوينة عدّة تعليقاتٍ مرتبطةٍ بها، وبالمقابل يرتبط كل تعليق بتدوينة واحدةٍ فقط، وبذلك فإنّ للتدوينة الواحدة علاقةٌ بالعديد من التعليقات. يُعد الجدول الخاص بالتدوينات في هذه الحالة الجدول الأب parent، في حين يُعد الجدول الخاص بالتعليقات الجدول الابن child؛ إذ يمكن للسجل الموجود في الجدول الأب الإشارة إلى عدّة سجلات موجودة في الجدول الابن، وهو أمرٌ أساسي لإتاحة إمكانية الوصول إلى البيانات المطلوبة في كل جدول. سنعمل في هذا المقال على بناء مدونة ويب صغيرة لبيان كيفيّة إنشاء علاقة من النوع واحد-إلى-مُتعدّد باستخدام الإضافة Flask-SQLAlchemy، إذ سننشئ علاقةً بين التدوينات والتعليقات، فمن الممكن أن يكون لكل تدوينة عددًا من التعليقات. مستلزمات العمل قبل المتابعة في هذا المقال لا بدّ من: توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو flask_app. الفهم الجيد لأساسيات فلاسك، مثل مفهوم الوجهات ودوال العرض، وفي هذا الصدد يمكنك الاطلاع على المقالين كيفية بناء موقعك الإلكتروني الأول باستخدام إطار عمل فلاسك Flask من لغة بايثون وكيفية استخدام القوالب في تطبيقات فلاسك Flask لفهم مبادئ فلاسك. فهم أساسيات لغة HTML. الخطوة 1: تثبيت فلاسك والأداة Flask-SQLAlchemy سنعمل في هذه الخطوة على تثبيت كل من فلاسك وكافّة الحزم اللازمة لعمل التطبيق. لذا، وبعد التأكّد من تفعيل البيئة الافتراضية، سنستخدم أمر تثبيت الحزم pip لتثبيت كل من فلاسك وأداة Flask-SQLAlchemy على النحو التالي: (env)user@localhost:$ pip install Flask Flask-SQLAlchemy وبمجرّد انتهاء التثبيت بنجاح، سيظهر في السطر الأخير من الخرج ما يشبه التالي: Successfully installed Flask-2.1.1 Flask-SQLAlchemy-2.5.1 Jinja2-3.1.1 MarkupSafe-2.1.1 SQLAlchemy-1.4.35 Werkzeug-2.1.1 click-8.1.2 greenlet-1.1.2 itsdangerous-2.1.2 ومع نهاية هذه الخطوة نكون قد ثبتنا ما يلزم من حزم بايثون، وأصبح من الممكن الانتقال إلى الخطوة التالية المُتمثّلة بإعداد قاعدة البيانات. الخطوة 2: إعداد النماذج Models وقاعدة البيانات سنعمل في هذه الخطوة على إعداد الاتصال مع قاعدة البيانات وإنشاء نماذج قاعدة البيانات SQLAlchemy، وهي أصناف بايثون تمثّل جداول قاعدة البيانات، إذ سننشئ نموذجًا للتدوينات وآخرًا للتعليقات، وسنعمل على تهيئة قاعدة البيانات وإنشاء جدول للتدوينات وآخرٍ للتعليقات وفقًا للنماذج التي سنصرّح عنها، ونهايةً سنضيف بعض التدوينات والتعليقات إلى قاعدة البيانات. إعداد الاتصال بقاعدة البيانات سننشئ بدايةً ملفًا باسم app.py ضمن المجلد flask.app، والذي سيحتوي على الشيفرات الخاصة بإعداد قاعدة البيانات ووجهات فلاسك اللازمة لعمل التطبيق: (env)user@localhost:$ nano app.py سيتصل هذا الملف بقاعدة بيانات من نوع SQLite باسم database.db، وسيحتوي على صنفين، الأوّل يدعى Post ويمثّل جدول التدوينات في قاعدة البيانات, والثاني يُدعى Comment ويمثّل جدول التعليقات، كما سيتضمّن هذا الملف وجهات فلاسك. الآن، سنضيف الاستدعاءات التالية باستخدام التعليمة import إلى بداية الملف app.py: import os from flask import Flask, render_template, request, redirect, url_for from flask_sqlalchemy import SQLAlchemy استوردنا في الشيفرة السابقة الوحدة os التي تمكنّنا من الوصول إلى واجهات نظام التشغيل المختلفة، والتي سنستخدمها في بناء مسار ملف قاعدة البيانات database.db. كما استوردنا من حزمة فلاسك كافّة المُساعدات اللازمة للتطبيق، مثل الصنف Flask المُستخدم في إنشاء النسخة الفعلية من التطبيق، والدالة ()render_template لتصيير قوالب HTML، والكائن request المسؤول عن التعامل مع الطلبيات، والدالة ()url_for لبناء روابط الوجهات، والدالة ()redirect لإعادة توجيه المُستخدمين من صفحة لأُخرى. وبعد الانتهاء من الاستدعاءات، سنعيّن مسارًا لملف قاعدة البيانات، ونستنسخ تطبيق فلاسك، كما سنضبط ونؤسس اتصالًا للتطبيق مع الإضافة SQLAlchemy، ثمّ سنضيف الشيفرة التالية إلى الملف app.py: basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] =\ 'sqlite:///' + os.path.join(basedir, 'database.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) أنشأنا في الشيفرة السابقة مسارًا لملف قاعدة البيانات SQLite، إذ حددنا المجلد الحالي ليكون المجلد الرئيسي، كما استخدمنا الدالة ()os.path.abspath للحصول على المسار الكامل لمجلد الملف الحالي، إذ يتضمّن المتغير الخاص __file__ اسم مسار الملف الحالي app.py، لنخزّن المسار الكلي للمجلد الأساسي ضمن المتغير basedir، ثمّ أنشأنا نسخةً فعلية من تطبيق فلاسك باسم app، والتي سنستخدمها لضبط مفتاحي إعدادات خاصّين بالإضافة Flask-SQLAlchemy وهما: SQLALCHEMY_DATABASE_URI: وهو معرّف الموارد الموحّد الخاص بقاعدة البيانات database URI المسؤول عن تحديد قاعدة البيانات المراد إنشاء اتصال معها، وفي هذه الحالة تكون صيغة هذا المعرّف بالشكل الآتي: sqlite:///path/to/database.db إذ نستخدم الدالة ()op.path.join لتحقيق الربط المدروس ما بين المجلد الأساسي الذي أنشاناه وخزنّا مساره في المتغير basedir، وبين اسم الملف database.db، الأمر الذي يسمح بالاتصال مع ملف قاعدة البيانات database.db الموجود في المجلد flask_app، إذ سيُنشأ هذا الملف فور تهيئة قاعدة البيانات. SQLALCHEMY_TRACK_MODIFICATIONS: وهو إعداد لتمكين أو إلغاء تمكين ميزة تتبع تغيرات الكائنات، وقد عيّناه إلى القيمة false لإلغاء التتبع وبالتالي تحقيق استخدام أقل لموارد الذاكرة. ملاحظة: من الممكن استخدام أي محرّك قواعد بيانات آخر مثل، PostgreSQL، أو MySQL، ويجب في هذه الحالة استخدام معرّف الموارد الموحد URI المناسب. ففي حال استخدام PostgreSQL سيتبع الصيغة: postgresql://username:password@host:port/database_name أمّا في حال استخدام MySQL: mysql://username:password@host:port/database_name وبعد الانتهاء من ضبط إعدادات الأداة SQLAIchemy من خلال تعيين معرّف الموارد الموحّد لقاعدة البيانات وإلغاء تمكين ميزة تتبّع التغييرات، سننشئ كائن قاعدة بيانات باستخدام الصنف SQLAIchemy، ممررين إليه نسخة التطبيق بغية ربط تطبيق فلاسك مع الأداة SQLAIchemy، ونخزّن كائن قاعدة البيانات هذا ضمن متغير باسم db لنستخدمه في التخاطب مع قاعدة البيانات. التصريح عن الجداول الآن وبعد تأسيس الاتصال مع قاعدة البيانات وإنشاء كائن قاعدة البيانات، سنستخدم هذا الكائن لإنشاء جدول للتدوينات وآخر للتعليقات، حيث تُمثّل الجداول بنماذج models، والنموذج هو صنف بايثون يرث من صنف رئيسي توفرّه الأداة Flask-SQLAlchemy من خلال نسخة قاعدة البيانات db التي أنشأناها مُسبقًا. لذا، وبغية تعريف جدولي التدوينات والتعليقات مثل نماذج، سنضيف الصنفين التاليين إلى الملف app.py على النحو التالي: class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100)) content = db.Column(db.Text) comments = db.relationship('Comment', backref='post') def __repr__(self): return f'<Post "{self.title}">' class Comment(db.Model): id = db.Column(db.Integer, primary_key=True) content = db.Column(db.Text) post_id = db.Column(db.Integer, db.ForeignKey('post.id')) def __repr__(self): return f'<Comment "{self.content[:20]}...">' أنشأنا في الشيفرة السابقة كل من النموذج Post والنموذج Comment مُستخدمين الصنف db.Column لتعريف أعمدة الجداول، إذ يمثّل الوسيط الأوّل نوع العمود، أمّا باقي الوسطاء فتمثّل إعدادات العمود. وقد عرفنا الأعمدة التالية للنموذج Post: id: وهو معرّف التدوينة، ويحتوي على بياناتٍ من نوع رقم صحيح وذلك باستخدام db.Integer و primary_key=True التي تعرّفه مفتاحًا أساسيًا، وتُخصّص قيمةٌ فريدة في قاعدة البيانات من أجل كل سجل (والسجل هو التدوينة في حالتنا). title: عنوان التدوينة، وهو سلسلة نصية بعدد محارف أعظمي يساوي 100. content: محتوى التدوينة، وتشير db.Text إلى أنّ هذا العمود يضم نصوصًا طويلة. وتُعرِّف سمة الصنف المسماة comments علاقةً من النوع واحد-إلى-متعدد ما بين النموذج Post والنموذج Comment، إذ استخدمنا التابع ()db.relationship ممررين إليه اسم نموذج التعليقات (المُسمّى Comment في حالتنا)، كما استخدمنا المعامل backref لإضافة مرجعٍ للعودة back reference، والذي يتمثّل بأحد أعمدة النموذج Comment، بحيث يمكننا الوصول إلى المنشور الذي يتبع له تعليق معيّن باستخدام السمة post. على سبيل المثال، إذا كان لدينا كائن تعليق مُخزّن ضمن متغير باسم comment، فمن الممكن الوصول إلى المنشور الذي وُضع هذا التعليق عليه باستخدام التعليمة comment.post، وسنبين فيما بعد مثال يوضح هذه الفكرة. تمكننا الدالة الخاصة __repr__ من تمثيل كل كائن بصيغة سلسلة نصية، الأمر الذي يساعد في التعرّف على هذا الكائن لأغراض التنقيح، إذ استخدمنا في حالتنا عنوان التدوينة لهذا الغرض. أمّا النموذج Comment فيمثّل جدول التعليقات، وقد عرفنا الأعمدة التالية له: id: وهو معرّف التعليق، ويحتوي على بياناتٍ من نوع رقم صحيح وذلك باستخدام db.Integer و primary_key=True التي تعرّفه مفتاحًا أساسيًا، وتُخصّص قيمةٌ فريدة في قاعدة البيانات من أجل كل سجل (والسجل هو التعليق في هذه الحالة). content: محتوى التعليق، وتشير db.Text إلى أنّ هذا العمود يضم نصوصًا طويلة. post_id: وهو عدد صحيح يمثّل مفتاحًا خارجيًا foreign مُنشأ باستخدام الصنف ()db.ForeignKey؛ وهو مفتاح يربط جدولًا بآخر من خلال مفتاحه الأساسي، وفي حالتنا سيربط التعليق بالتدوينة التي يتبع لها باستخدام المفتاح الأساسي للتدوينة المتمثّل بمعرّفها، وبذلك يكون جدول التدوينات في هذه الحالة هو الجدول الأب، ما يعني أنّ كل تدوينة سترتبط بعدّة تعليقات، إذ يمثّل جدول التعليقات الجدول الابن، ويرتبط كل تعليق بتدوينة من الجدول الأب باستخدام معرّفها، وبالتالي سيكون لكل تعليق قيمة في العمود post_id والتي سنستخدمها في الوصول إلى التدوينة التي أُضيف التعليق عليها. تُظهر الدالة الخاصة __repr__ في نموذج التعليقات أوّل 20 محرفًا من محتوى التعليق ما يمنح كائن التعليق تمثيلًا محرفيًا قصيرًا. وبذلك سيبدو الملف app.py حاليًا بالشّكل التالي: import os from flask import Flask, render_template, request, redirect, url_for from flask_sqlalchemy import SQLAlchemy basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] =\ 'sqlite:///' + os.path.join(basedir, 'database.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100)) content = db.Column(db.Text) comments = db.relationship('Comment', backref='post') def __repr__(self): return f'<Post "{self.title}">' class Comment(db.Model): id = db.Column(db.Integer, primary_key=True) content = db.Column(db.Text) post_id = db.Column(db.Integer, db.ForeignKey('post.id')) def __repr__(self): return f'<Comment "{self.content[:20]}...">' نحفظ الملف ونغلقه. إنشاء قاعدة بيانات الآن، وبعد الانتهاء من إعداد كل من الاتصال مع قاعدة البيانات ونماذج التدوينة والتعليق، سنستخدم صدفة فلاسك لإنشاء قاعدة البيانات وجداول التدوينات والتعليقات اعتمادًا على النماذج المُعرّفة مُسبقًا. بعد التأكّد من كون البيئة الافتراضية مُفعّلة، نستخدم متغير البيئة FLASK_APP لتحديد الملف app.py على أنه تطبيق فلاسك: (env)user@localhost:$ export FLASK_APP=app ومن ثمّ نفتح صَدفة فلاسك باستخدام الأمر التالي وذلك أثناء وجودنا ضمن المجلد flask_app: (env)user@localhost:$ flask shell فتُفتح صَدفة بايثون تفاعلية خاصّة، تعمل على تشغيل الأوامر ضمن تطبيق فلاسك، وبذلك نضمن اتصال دوال Flask-SQLAlchemy التي سنستدعيها مع تطبيق فلاسك. سنستورد الآن كائن قاعدة البيانات ونماذج التدوينات والتعليقات، ثمّ سنشغّل الدالة ()db.create_all بغية إنشاء الجداول الموافقة لتلك النماذج، كما يلي: >>> from app import db, Post, Comment >>> db.create_all() نبقي الصدفة في حالة تشغيل، ونفتح نافذة طرفية جديدة وننتقل إلى المجلد flask_app، فنجد ملفًا جديدًا ضمنه باسم database.db. ملاحظة:لا تنشئ الدالة ()db.create_all أو تحدّث جدولًا موجودًا أصلًا، فمثلًا لو عدّلنا أحد النماذج التي أنشأناها بإضافة عمود جديد إليه، ثمّ شغلنا الدالة ()db.create_all فلن يُطبّق التغيير الذي أجريته على النموذج على الجدول إذا كان الجدول موجود أصلًا في قاعدة البيانات، ويكون الحل لهذه المشكلة بحذف كافّة الجداول الموجودة في قاعدة البيانات باستخدام الدالة ()db.drop_all، ثمّ إعادة إنشائها باستخدام الدالة ()db.create_all، على النحو التالي: >>> db.drop_all() >>> db.create_all() ستعمل هذه الشيفرة على تطبيق التعديلات على النموذج، كما ستحذف كافّة البيانات الموجودة في قاعدة البيانات، فلو أردت تحديث قاعدة البيانات مع الحفاظ على البيانات الموجودة فيها، لا بدّ من استخدام أداة ترحيل ملف تخطيط قاعدة البيانات schema migration، لنتمكن من تعديل الجداول مع الحفاظ على البيانات، ولاستخدام هذه الأداة من الممكن الاستعانة بالإضافة Flask-Migrate لتنفيذ ترحيل ملف تخطيط قاعدة البيانات باستخدام الأداة SQLAIchemy من خلال واجهة أسطر الأوامر في فلاسك. وفي حال ظهور رسالة خطأ، يجب التأكّد من كون التصريح صحيح عن كل من النماذج ومعرّف الموارد الموحّد لقاعدة البيانات. ملء الجدول الآن وبعد إنشاء قاعدة البيانات وجداول التدوينات والتعليقات، سننشئ ملفًا ضمن المجلد flask_app لإضافة بعض التدوينات والتعليقات إلى قاعدة البيانات. لذا، سننشئ ملفًا جديدًا باسم init_db.py: (env)user@localhost:$ nano init_db.py ونكتب ضمنه الشيفرات التالية، إذ سينشئ هذا الملف ثلاثة كائنات للتدوينات وأربعة كائنات للمنشورات، مُضيفًا كل منها إلى قاعدة البيانات: from app import db, Post, Comment post1 = Post(title='Post The First', content='Content for the first post') post2 = Post(title='Post The Second', content='Content for the Second post') post3 = Post(title='Post The Third', content='Content for the third post') comment1 = Comment(content='Comment for the first post', post=post1) comment2 = Comment(content='Comment for the second post', post=post2) comment3 = Comment(content='Another comment for the second post', post_id=2) comment4 = Comment(content='Another comment for the first post', post_id=1) db.session.add_all([post1, post2, post3]) db.session.add_all([comment1, comment2, comment3, comment4]) db.session.commit() نحفظ الملف ونغلقه. استوردنا في الشيفرة السابقة كلًا من كائن قاعدة البيانات والنموذج Post والنموذج Comment من الملف app.py، كما أنشأنا بضعة كائنات تدوينات باستخدام النموذج Post، ممررين لذلك عنوان التدوينة إلى المعامل title ومحتواها إلى المعامل content، ثمّ أنشأنا بعد ذلك بضع كائنات تعليقات، ممررين محتوى التعليق؛ وهنا من الممكن استخدام أحدى طريقتين لربط التعليق بالتدوينة التي يتبع لها، الأولى بتمرير كائن التدوينة إلى المعامل post كما هو مبيّن في كلا الكائنين comment1 و comment2، أو من الممكن تمرير معرّف التدوينة إلى المعامل post_id كما هو مبيّن في الكائنين comment3 و comment4، وبالتالي من الممكن تمرير العدد الصحيح المُمثّل لمعرّف التدوينة وحده في حال عدم وجود كائن التدوينة في الشيفرة. بعد الانتهاء من تعريف كل من كائني التدوينة والتعليق، استخدمنا التابع ()db.session.add_all لإضافة كافة كائنات التدوينات والتعليقات إلى جلسة قاعدة البيانات التي تدير العمليات، ثمّ استخدمنا التابع ()db.session.commit لتأكيد وتطبيق التغييرات على قاعدة البيانات. للمزيد حول جلسات قواعد البيانات في SQLAIchemy، ننصحك بقراءة الخطوة الثانية من المقال كيفية استخدام الإضافة Flask-SQLAlchemy للتخاطب مع قواعد البيانات في تطبيقات فلاسك. الآن، سنشغل الملف init_db.py لتنفيذ الشيفرة وإضافة البيانات إلى قاعدة البيانات: (env)user@localhost:$ python init_db.py وللاطلاع على البيانات المُضافة إلى قاعدة البيانات، نفتح صَدفة فلاسك بغية الاستعلام عن كافّة التدوينات وعرض عناوينها ومحتويات التعليقات على كل منها، على النحو التالي: (env)user@localhost:$ flask shell ثمّ نشغّل الشيفرة التالية المسؤولة عن الاستعلام عن كافّة التدوينات وعرض عنوان كل تدوينة والتعليقات عليها أسفلها: from app import Post posts = Post.query.all() for post in posts: print(f'## {post.title}') for comment in post.comments: print(f'> {comment.content}') print('----') استوردنا في الشيفرة السابقة النموذج Post من الملف app.py، ثمّ استعلمنا عن كافّة التدوينات الموجودة في قاعدة البيانات باستخدام السمة query للتابع ()all، وخزّنا نتائج الاستعلام ضمن متغير باسم posts، ثمّ استخدمنا حلقة for تكرارية بغية المرور على كل عنصر من المتغير posts، لتُعرض العناوين، ثمّ استخدمنا بعد ذلك حلقة for تكرارية ثانية للمرور على كافّة تعليقات كل تدوينة، إذ استخدمنا التعليمة post.comments للوصول إلى التعليقات، ليُعرض بعدها محتوى التعليقات ويُطبَع فاصل بالشكل '----' بغية الفصل بين التدوينات في الخرج. فنحصل على الخرج التالي: ## Post The First > Comment for the first post > Another comment for the first post ---- ## Post The Second > Comment for the second post > Another comment for the second post ---- ## Post The Third ---- فكما هو واضح، من الممكن الوصول إلى بيانات كل تدوينة وتعليقاتها باستخدام شيفرة قصيرة جدًا. والآن نخرج من الصدفة: >>> exit() ومع نهاية هذه الخطوة، أصبح لدينا عددًا من المنشورات والتعليقات في قاعدة البيانات، أمّا في الخطوة التالية سنعمل على إنشاء وجهة فلاسك للصفحة الرئيسية للتطبيق بغية عرض جميع التدوينات الموجودة ضمن قاعدة البيانات في هذه الصفحة. الخطوة 3: عرض جميع السجلات سنعمل في هذه الخطوة على إنشاء وجهة وقالب لعرض كافّة التدوينات الموجودة في قاعدة البيانات ضمن الصفحة الرئيسية للتطبيق. نفتح الملف app.py لنضيف إليه وجهةً للصفحة الرئيسية: (env)user@localhost:$ nano app.py ونضيف الوجهة التالية إلى نهاية الملف: # ... @app.route('/') def index(): posts = Post.query.all() return render_template('index.html', posts=posts) نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة المُزخرف ()app.route لإنشاء دالة عرض باسم ()index، التي نستعلم فيها ضمن قاعدة البيانات لجلب كافّة التدوينات كما فعلنا في الخطوة السابقة، لنخزّن نتائج هذا الاستعلام ضمن متغير باسم posts ممررين إياه إلى ملف قالب باسم index.html، الذي ستصيّره باستخدام الدالة المساعدة ()render_template. الآن وقبل إنشاء ملف القالب index.html المسؤول عن عرض التدوينات الموجودة ضمن قاعدة البيانات في الصفحة الرئيسية للتطبيق، سننشئ بدايةً ملف قالب رئيسي ليتضمّن كافة شيفرات HTML الأساسية اللازمة لترثها لاحقًا القوالب الأُخرى، وهذا ما يجنبنا تكرار الشيفرات؛ ثمّ سننشئ ملف قالب الصفحة الرئيسية index.html المُصيّر أصلًا باستخدام الدالة ()index. لذلك، سننشئ مجلدًا للقوالب باسم templates، وننشئ ضمنه ملف قالب باسم base.html، والذي سيمثّل القالب الأساسي لبقية القوالب، كما يلي: (env)user@localhost:$ mkdir templates (env)user@localhost:$ nano templates/base.html وسنكتب فيه الشيفرة التالية: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %} {% endblock %} - FlaskApp</title> <style> .title { margin: 5px; } .content { margin: 5px; width: 100%; display: flex; flex-direction: row; flex-wrap: wrap; } .comment { padding: 10px; margin: 10px; background-color: #fff; } .post { flex: 20%; padding: 10px; margin: 5px; background-color: #f3f3f3; inline-size: 100%; } .title a { color: #00a36f; text-decoration: none; } nav a { color: #d64161; font-size: 3em; margin-left: 50px; text-decoration: none; } </style> </head> <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="#">Comments</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% block content %} {% endblock %} </div> </body> </html> نحفظ الملف ونغلقه. يتضمّن القالب الأساسي كافّة الشيفرات المتداولة التي سنحتاجها في القوالب الأُخرى. ستُستبدل لاحقًا كتلة العنوان title بعنوان كل صفحة وكتلة المحتوى content بمحتواها، أمّا عن شريط التصفح فسيتضمّن ثلاثة روابط؛ إذ ينقل الأوّل المُستخدم إلى الصفحة الرئيسية للتطبيق باستخدام الدالة المساعدة ()url_for؛ والثاني إلى صفحة التعليقات Comments، أمّا الأخير لصفحة المعلومات حول التطبيق About في حال قررت تضمينها في تطبيقك، مع ملاحظة أنّنا سنعدّل هذا الملف لاحقًا بعد إضافة صفحة لعرض التعليقات الأحدث بغية جعل الرابط إلى هذه الصفحة فعّالًا. الآن، سننشئ ملف قالب باسم index.html وهو الاسم الذي حددناه في الملف app.py: (env)user@localhost:$ nano templates/index.html ونضيف ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <span class="title"><h1>{% block title %} Posts {% endblock %}</h1></span> <div class="content"> {% for post in posts %} <div class="post"> <p><b>#{{ post.id }}</b></p> <b> <p class="title"> <a href="#"> {{ post.title }} </a> </p> </b> <div class="content"> <p>{{ post.content }}</p> </div> <hr> </div> {% endfor %} </div> {% endblock %} نحفظ الملف ونغلقه. اعتمدنا في الشيفرة السابقة على الوراثة من ملف القالب base.html باستخدام تعليمة extends، واستبدلنا محتوى كتلة المحتوى content مُستخدمين تنسيق العنوان من المستوى الأوّل <h1> الذي يفي أيضًا بالغرض لعنوان الصفحة. استخدمنا في السطر البرمجي {% for post in posts %} حلقة for من تعليمات محرّك القوالب جينجا jinja، والهدف من استخدام هذه الحلقة هو المرور على كل تدوينة ضمن المتغير posts المُمرّر من الدالة ()index إلى هذا القالب، حيث سنعرض معرّف التدوينة وعنوانها ومحتواها، إذ سيعمل لاحقًا عنوان التدوينة مثل رابط إلى صفحة مُستقلّة تعرض هذه التدوينة مع تعليقاتها. الآن ومع وجودنا ضمن المجلد flask_app ومع كون البيئة الافتراضية مُفعّلة، سنُعلم فلاسك بالتطبيق المراد تشغيله (وهو في حالتنا الملف app.py) باستخدام متغير البيئة FLASK_APP، في حين يحدّد متغير البيئة FLASK_ENV وضع التشغيل وهنا قد اخترنا الوضع development ما يعني أنّ التطبيق سيعمل في وضع التطوير مع تشغيل مُنقّح الأخطاء. لمزيدٍ من المعلومات حول مُنقّح الأخطاء في فلاسك ننصحك بقراءة المقال كيفية التعامل مع الأخطاء في تطبيقات فلاسك، ولتنفيذ ما سبق سنشغّل الأوامر التالية: (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development والآن سنشغّل التطبيق باستخدام الأمر flask run: (env)user@localhost:$ flask run وبعد التأكد من تشغيل خادم التطوير، نذهب إلى الرابط التالي باستخدام المتصفح: http://127.0.0.1:5000/ فتظهر لنا التدوينات المُضافة إلى قاعدة البيانات في صفحة مشابهة للصورة التالية: عرضنا في هذه الخطوة كافّة التدوينات الموجودة ضمن قاعدة البيانات في الصفحة الرئيسية للتطبيق، أمّا في الخطوة التالية فسنعمل على إنشاء وجهة لصفحة خاصّة بكل تدوينة، وفيها سنعرض تفاصيل كل تدوينة مع كافّة التعليقات عليها أدناها. الخطوة 4: عرض تدوينة مفردة مع التعليقات عليها سننشئ في هذه الخطوة وجهة وقالب لعرض التفاصيل الخاصة بكل تدوينة في صفحة مُخصّصة لذلك، مع عرض كافّة تعليقات كل منها أدناها. في نهاية هذه الخطوة سيعرض الرابط http://127.0.0.1:5000/1 صفحةً تعرض التدوينة الأولى مع تعليقاتها (لأن الرابط يحوي على المعرّف رقم 1)، بينما يعرض الرابط http://127.0.0.1:5000/ID المنشور ذو المعرّف ID الموافق له إن وُجد. نبقي خادم التطوير قيد التشغيل، ونفتح نافذة طرفية جديدة. نفتح الملف app.py لتعديله كما يلي: (env)user@localhost:$ nano app.py ونضيف الوجهة التالية إلى نهاية الملف: # ... @app.route('/<int:post_id>/') def post(post_id): post = Post.query.get_or_404(post_id) return render_template('post.html', post=post) نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة الوجهة /<int:post_id>/ إذ يمثّل المعامل :int محوّلًا لتحويل السلسلة النصية الافتراضية في الرابط إلى نوع عدد صحيح، ويمثل post_id متغير الرابط الذي سيُحدّد التدوينة المطلوب عرضها في الصفحة. مررنا المعرّف من الرابط إلى دالة العرض ()post من خلال المعامل post_id، والتي نستعلم فيها عن جدول التدوينات لنجلب منه التدوينة من خلال معرّفها باستخدام التابع ()get_or_404، الأمر الذي يؤدي إلى حفظ بيانات التدوينة (في حال وجودها) ضمن المتغير post، وإلّا ستُعرض استجابة خطأ HTTP من النوع 404 Not Found في حال عدم وجود تدوينة موافقة للمعرّف المطلوب في قاعدة البيانات. ومن ثمّ صيّرنا قالبًا باسم post.html ممرّرين إليه التدوينة المُستخرجة. لذا، سنفتح ملف قالب جديد باسم post.html: (env)user@localhost:$ nano templates/post.html وسنكتب فيه الشيفرة التالية، والأمر مشابه للقالب index.html باستثناء أنّ هذا الملف سيعرض تدوينةً واحدةً كل مرة: {% extends 'base.html' %} {% block content %} <span class="title"><h1>{% block title %} {{ post.title }} {% endblock %}</h1></span> <div class="content"> <div class="post"> <p><b>#{{ post.id }}</b></p> <b> <p class="title">{{ post.title }}</p> </b> <div class="content"> <p>{{ post.content }}</p> </div> <hr> <h3>Comments</h3> {% for comment in post.comments %} <div class="comment"> <p>#{{ comment.id }}</p> <p>{{ comment.content }}</p> </div> {% endfor %} </div> </div> {% endblock %} نحفظ الملف ونغلقه. اعتمدنا في هذه الشيفرة على الوراثة من القالب base.html باستخدام تعليمة extends، وجعلنا عنوان التدوينة عنوانًا للصفحة، كما عرضنا كلًا من معرّف التدوينة ومحتواها، ومن ثمّ مررنا على كافّة التعليقات المتوفرّة على هذه التدوينة باستخدام سمة الكائن post.comments، لنعرض معرّف التعليق ومحتواه. ننتقل باستخدام متصفح الويب إلى رابط التدوينة الثانية كما يلي: http://127.0.0.1:5000/2/ فتظهر صفحة مشابهة لما يلي: ثمّ سنحرّر ملف القالب index.html لنربط عنوان كل تدوينة مع الصفحة الخاصة بها: (env)user@localhost:$ nano templates/index.html نعدّل قيمة سمة الرابط href لعنوان التدوينة ضمن حلقة for التكرارية: ... {% for post in posts %} <div class="post"> <p><b>#{{ post.id }}</b></p> <b> <p class="title"> <a href="{{ url_for('post', post_id=post.id)}}"> {{ post.title }} </a> </p> </b> <div class="content"> <p>{{ post.content }}</p> </div> <hr> </div> {% endfor %} نحفظ الملف ونغلقه. ننتقل إلى الصفحة الرئيسية للتطبيق ونحدثّها: http://127.0.0.1:5000/ وبالنقر على عنوان كل تدوينة في الصفحة الرئيسية للتطبيق نجد أنّه مرتبط بالصفحة الموافقة. الآن وبعد الانتهاء من إنشاء صفحة خاصّة بكل تدوينة، سنعمل في الخطوة التالية على إضافة نموذج ويب إلى صفحة التدوينة بغية السماح للمستخدمين بإضافة تعليقات جديدة. الخطوة 5: إضافة تعليقات جديدة سنعمل في هذه الخطوة على تحرير الوجهة /<int:post_id>/ ودالة العرض ()post الخاصة بها والمسؤولة عن عرض التدوينات المنفردة، إذ سنضيف نموذج ويب أسفل كل تدوينة من شأنه السماح للمستخدمين بإضافة تعليقات جديدة على التدوينة، ومن ثمّ سنجهّز الآليات اللازمة للتعامل مع إرسال التعليق وإضافته إلى قاعدة البيانات. لذا، سنفتح بدايةً ملف القالب post.html لإضافة نموذج ويب مكوّن من حقل نصي لمحتوى التعليق بالإضافة إلى زر أوامر Add Comment لتأكيد وإرسال التعليق: (env)user@localhost:$ nano templates/post.html نعدّل الملف بإضافة نموذج بعد العنوان من المستوى الثالث Comments وقبل حلقة for التكرارية، كما يلي: <hr> <h3>Comments</h3> <form method="post"> <p> <textarea name="content" placeholder="Comment" cols="60" rows="5"></textarea> </p> <p> <button type="submit">Add comment</button> </p> </form> {% for comment in post.comments %} نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة الوسم <form> وضبطنا فيه السمة method التي تُحدّد نوع طلبية HTTP لتكون من النوع POST، ما يعني أنّ نموذج الويب هذا سيرسل طلبًا من النوع POST. وبذلك أصبح لدينا حقلًا نصيًا لمحتوى التعليق بالإضافة إلى زر تأكيد وإرسال له. الآن -وأثناء عمل خادم التطوير- نستخدم المتصفح للانتقال إلى تدوينة ما (ولتكن التدوينة الثانية مثلًا): http://127.0.0.1:5000/2/ فستظهر صفحة مشابهة للصورة التالية: يرسل نموذج الإدخال هذا طلبًا من النوع POST إلى دالة العرض ()post، ولكن حتى هذه اللحظة لا توجد شيفرة مسؤولة عن معالجة هذا الطلب، وبالتالي لن يحدث شيء في حال ملء النموذج الآن وإرساله بمعنى أنّ النموذج لا يعمل حتى اللحظة. الآن، سنضيف الشيفرة اللازمة لدالة العرض ()post لمعالجة النماذج المُرسلة وإضافة التعليق الجديد إلى قاعدة البيانات. لذا، سنفتح الملف app.py بهدف تعديله ليتعامل مع الطلبات من النوع post المُرسلة من قبل المستخدم: (env)user@localhost:$ nano app.py ونعدّل الوجهة /<int:post_id>/ ودالة العرض ()post الخاصة بها لتصبح كما يلي: @app.route('/<int:post_id>/', methods=('GET', 'POST')) def post(post_id): post = Post.query.get_or_404(post_id) if request.method == 'POST': comment = Comment(content=request.form['content'], post=post) db.session.add(comment) db.session.commit() return redirect(url_for('post', post_id=post.id)) return render_template('post.html', post=post) نحفظ الملف ونغلقه. سمحنا في الشيفرة السابقة بكلا نوعي طلبيات HTTP وهما GET و POST من خلال المعامل method؛ إذ تتخصّص الطلبيات من النوع GET بجلب البيانات من الخادم، أمّا الطلبيات من النوع POST فهي مُتخصّصة بإرسال البيانات إلى وجهة مُحدّدة، مع ملاحظة أنّ الطلبيات من النوع GET هي الوحيدة المسموحة افتراضيًا. تُعالج الطلبية من النوع POST المُرسلة من قبل المُستخدم عبر النموذج ضمن العبارة الشرطية 'if request.method == 'POST، إذ ننشئ كائن تعليقات باستخدام النموذج Comment، ممررين إليه محتوى التعليق المُرسل والمُستخرج من الكائن request.form، ثمّ نحدّد التدوينة التي يتبع لها التعليق باستخدام المعامل post ممررين إليه الكائن post المُستخرج اعتمادًا على مُعرّف التدوينة الموافقة باستخدام التابع ()get_or_404. نهايةً، نضيف كائن التعليقات الذي أنشأناه للتو إلى جلسة قاعدة البيانات، ونحفظ التغييرات، ونعيد توجيه المستخدم إلى صفحة التدوينة. الآن، نحدّث صفحة التدوينة ونضيف تعليقًا ونرسله، فسيظهر هذا التعليق الجديد أسفل التدوينة. أصبح لدينا مع نهاية هذه الخطوة نموذج ويب يمكّن المُستخدمين من إضافة تعليقات على التدوينة، وللمزيد حول نماذج الويب ننصحك بقراءة المقال كيفية استخدام نماذج الويب في تطبيقات فلاسك، وللمزيد من المعلومات المتقدمة ولتتعرّف على كيفية إدارة نماذج الويب بأمان ننصحك بقراءة المقال استخدام والتحقق من نماذج الويب ذات واجهة المستخدم التفاعلية في فلاسك باستخدام الإضافة Flask-WTF. سنعمل في الخطوة التالية على إضافة صفحة لعرض كافّة التعليقات الموجودة ضمن قاعدة البيانات مع التدوينة التي يتبع لها كل تعليق. الخطوة 6: عرض جميع التعليقات سنعمل في هذه الخطوة على إضافة صفحة للتعليقات Comments لعرض جميع التعليقات الموجودة ضمن قاعدة البيانات وترتيبها بحيث تظهر التعليقات الأحدث أولًا، حيث سيظهر مع كل تعليق عنوان ورابط التدوينة المرتبط بها. نفتح الملف app.py: (env)user@localhost:$ nano app.py ثم نضيف الوجهة التالية إلى نهاية هذا الملف، والتي تجلب جميع التعليقات من قاعدة البيانات، وترتّبها بحيث يكون الأحدث أولًا، لتمرّرها إلى ملف قالب باسم comments.html، الذي سننشئه لاحقًا: # ... @app.route('/comments/') def comments(): comments = Comment.query.order_by(Comment.id.desc()).all() return render_template('comments.html', comments=comments) نحفظ الملف ونغلقه. طبقنا في الشيفرة السابقة التابع ()order_by على السمة query بغية جلب كافّة التعليقات ولكن وفق ترتيب معيّن، وقد استخدمنا في حالتنا التابع ()desc على العمود Comment.id بهدف جلب التعليمات وفق ترتيب تنازلي، بحيث يكون التعليق الأحدث هو الأوّل، ومن ثمّ استخدمنا التابع ()all للحصول على النتيجة وتخزينها ضمن متغير باسم comments. ثمّ صيّرنا ملف قالب باسم comments.html، ممررين إليه الكائن comments المُتضمّن كافّة التعليقات مُرتبّة بحيث يكون التعليق الأحدث أولًا. لذا، سنفتح الآن ملف القالب comments.html: (env)user@localhost:$ nano templates/comments.html ونكتب ضمنه الشيفرة التالية، والتي ستعمل على عرض التعليقات وربط كل منها بالتدوينة التي يتبع لها: {% extends 'base.html' %} {% block content %} <span class="title"><h1>{% block title %} Latest Comments {% endblock %}</h1></span> <div class="content"> {% for comment in comments %} <div class="comment"> <i> (#{{ comment.id }}) <p>{{ comment.content }}</p> </i> <p class="title"> On <a href="{{ url_for('post', post_id=comment.post.id) }}"> {{ comment.post.title }} </a> </p> </div> {% endfor %} </div> </div> {% endblock %} نحفظ الملف ونغلقه. اعتمدنا في الشيفرة السابقة على الوراثة من ملف القالب الأساسي base.html باستخدام تعليمة extends، وعيّنا عنوانًا ومررنا على كافّة التعليقات باستخدام حلقة for تكرارية، لنعرض بعدها معرّف التعليق ومحتواه ورابط التدوينة التي يتبع لها، إذ تمكنا من الوصول إلى بيانات التدوينة باستخدام سمة الكائن comment.post. الآن وباستخدام المتصفّح ننتقل إلى صفحة التعليقات باستخدام الرابط التالي: http://127.0.0.1:5000/comments/ فتظهر صفحة مشابهة للصورة التالية: والآن سنعمل على تمكين رابط صفحة التعليقات Comments في شريط التصفّح، لذا نفتح ملف القالب base.html لتحريره: (env)user@localhost:$ nano templates/base.html ونعدل الشيفرة الخاصّة بشريط التنقل لتبدو كما يلي: <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="{{ url_for('comments') }}">Comments</a> <a href="#">About</a> </nav> نحفظ الملف ونغلقه. الآن وبتحديث صفحة التعليقات نجد أنّ الرابط Comments في شريط التصفّح بات يعمل. ومع نهاية هذه الخطوة أصبح لدينا صفحة لعرض كافّة التعليقات الموجودة ضمن قاعدة البيانات، أمّا في الخطوة التالية فسنعمل على إضافة زر أسفل كل تعليق في صفحة التدوينة من شأنه السماح للمُستخدمين بحذف التعليقات. الخطوة 7: حذف تعليقات سنعمل في هذه الخطوة على إضافة زر حذف التعليق Delete Comment أسفل كل تعليق في صفحة المنشور لنمكّن المستخدمين من حذف التعليقات غير المرغوبة. بدايةً، سنضيف وجهةً جديدةً للحذف، وهي /comments/ID/delete تتعامل مع الطلبات من النوع POST، إذ ستستقبل دالة الحذف رقم معرّف التعليق ID المراد حذفه، لتجلبه من قاعدة البيانات وتحذفه، ليُعاد بعدها التوجيه إلى صفحة التدوينة التي حّذف منها التعليق. الآن، افتح الملف app.py: (env)user@localhost:$ nano app.py أضِف الوجهة التالية إلى نهاية الملف: # ... @app.post('/comments/<int:comment_id>/delete') def delete_comment(comment_id): comment = Comment.query.get_or_404(comment_id) post_id = comment.post.id db.session.delete(comment) db.session.commit() return redirect(url_for('post', post_id=post_id)) نحفظ الملف ونغلقه. استخدمنا المزخرف app.post المُقدّم في الإصدار 2.0.0 من فلاسك بدلًا من استخدام المزخرف app.route المعتاد، والذي أضاف اختصارات للعديد من توابع HTTP الشائعة، فعلى سبيل المثال الأمر: @app.post("/login") هو اختصارٌ للأمر: @app.route("/login", methods=["POST"]) ما يعني أن الدالة العاملة في فلاسك هذه تتعامل فقط مع طلبيات من النوع POST، وبزيارة الوجهة comments/ID/delete/ في المتصفّح سيظهر الخطأ 405 Method Not Allowed لأن المتصفحات تستخدم طريقة GET افتراضيًا للطلبات، لذا بغية حذف تعليق ما، سنضيف زر أوامر عندما يضغط المستخدم عليه، تُرسَل إلى الوجهة طلبية من النوع POST. تستقبل دالة العرض ()delete_comment في الشيفرة السابقة معرّف التعليق المراد حذفه من خلال متغير الرابط comment_id، إذ استخدمنا التابع ()get_or_404 لجلب التعليق وحفظه في متغير باسم comment، أو الاستجابة بخطأ من النوع 404 Not Found في حال عدم وجود تعليق موافق، كما خزّنا معرّف التدوينة التي يتبع لها التعليق المحذوف في المتغير post_id والذي سنستخدمه في إعادة التوجيه إلى صفحة هذه التدوينة بعد حذف التعليق. استخدمنا التابع ()delete في الجلسة المفتوحة مع قاعدة البيانات في السطر البرمجي (db.session.delete(comment ممررين إليه كائن التدوينة، وهذا ما يجعل الجلسة بالنتيجة تحذف التعليق الموافق بمجرّد تأكيد الإجراءات، إذ ما من حاجة لإجراء أي تعديلات إضافية كون الإجراءات تُأكَّد مباشرةً باستخدام الأمر ()db.session.commit، ونهايةً أعدنا توجيه المستخدم إلى صفحة التدوينة التي يتبع لها التعليق المحذوف. سنعدّل القالب "post.html" بإضافة زر لحذف التعليق Delete Comment أسفل كل تعليق: (env)user@localhost:$ nano templates/post.html سنعدّل حلقة for التكرارية بإضافة وسم نموذج <form> مباشرةً أسفل محتوى التعليق: {% for comment in post.comments %} <div class="comment"> <p>#{{ comment.id }}</p> <p>{{ comment.content }}</p> <form method="POST" action="{{ url_for('delete_comment', comment_id=comment.id) }}"> <input type="submit" value="Delete Comment" onclick="return confirm('Are you sure you want to delete this entry?')"> </form> </div> {% endfor %} نحفظ الملف ونغلقه. أنشأنا في الملف السابق نموذج ويب يُرسل طلبًا من النوع POST إلى دالة العرض ()delete_comment ممررين القيمة comment.id وسيطًا للمعامل comment_id لتحديد التعليق المراد حذفه، كما استخدمنا التابع confirm() المتوفّر في متصفحات الويب لعرض رسالة تأكيد قبل إرسال الطلب. الآن وبالانتقال إلى صفحة إحدى التدوينات (التدوينة الثانية مثلًا): http://127.0.0.1:5000/2/ سيظهر زر أوامر لحذف التعليق Delete Comment أسفل كل تعليق، وبالنقر عليه ستظهر رسالة لتأكيد عملية الحذف، ومن ثمّ نجد أنّ التعليق قد حُذِف فعلًا. وبذلك أصبحت الآلية اللازمة لحذف التعليقات من قاعدة البيانات متوفّرة في التطبيق. الخاتمة عملنا في هذا المقال على بناء تطبيق مدونة بغية بيان كيفيّة إدارة علاقات قواعد البيانات من نوع واحد-إلى-مُتعدّد باستخدام الإضافة Flask-SQLAlchemy، إذ تعرفنا على كيفيّة ربط الجدول الأب بالجدول الابن، وكيفية إنشاء اقتران ما بين كائن ابن مع أبيه وإضافته إلى قاعدة البيانات، وكيفية الوصول إلى بيانات الابن انطلاقًا من سجلات الأب وبالعكس. ترجمة -وبتصرف- للمقال How to Use One-to-Many Database Relationships with Flask-SQLAlchemy لصاحبه Abdelhadi Dyouri. اقرأ أيضًا دليلك الشامل إلى قواعد البيانات استخدام علاقة one-to-many مع إطار العمل فلاسك Flask ومحرك قواعد البيانات SQLite تعديل العناصر في علاقات one-to-many باستخدام فلاسك Flask ومحرك قاعدة البيانات SQLite الرّبط بين جدولي المقالات والمُستخدمين بعلاقة واحد للعديد One-to-Many Relationship
-
يعتقد الكثير من المبرمجين المبتدئين أن استخدام واجهة سطر الأوامر أمر معقد، ما يجعله حكرًا على المحترفين، وهو اعتقاد مغلوط، إذ لا بدّ من التعامل مع واجهة سطر الأوامر بدءًا من مرحلة إعداد بيئة بايثون، وصولًا إلى تشغيل مرحلة البرامج. يتضمن هذا المقال قائمة مختصرة بالأوامر الشائعة والتي لابد من استخدامها في نافذة سطر الأوامر، من المؤكد وجود كم من الأوامر والوسطاء أكبر يكثير من هذا الموجود هنا، ولكن تعامل مع هذه القائمة على أنها الحد الأدنى المطلوب للمضي في استخدام نافذة سطر الأوامر. كما يقدّم هذا المقال تعريفًا بمفهوم متغيرات البيئة وكيفية عرضها والتعامل معها. سنعبر عن وسطاء سطر الأوامر في هذا القسم بتضمين كل منها بين قوسين متوسطين، فعلى سبيل المثال العبارة cd متبوعًأ باسم المجلد المستهدف تعني أنه يجب إدخال الأمر cd متبوعًا بوسيط هو اسم مجلد جديد لتغيير الدليل إليه. مطابقة أسماء المجلدات والملفات مع محارف البدل تتعامل الكثير من الأوامر مع أسماء المجلدات والملفات كوسطاء، كما تتعامل مع الأسماء المتضمنة محارف بدل مثل * و ?، ما يسمح لنا بتحديد عدة ملفات مطابقة، حيث يطابق محرف البدل * كافة الملفات الموافقة للمحارف التي تليه، في حين يطابق المحرف ? أي محرف مفرد مكانه، وندعو العبارات التي تستخدم محارف البدل * و? بالأنماط العامة. تسمح الأنماط العامة بتحديد أنماط للملفات، فعلى سبيل المثال يمكن تنفيذ الأمر dir أو ls لعرض جميع الملفات والمجلدات في دليل العمل الحالي، لكن إن أردنا عرض ملفات بايثون فقط، فنستخدم الأمرين بالشكل dir *.py أو ls *.py بغية عرض الملفات التي تنتهي باللاحقة py. فقط. فمثلًا النمط العام py.* يعني "أي مجموعة من المحارف المتبوعة بـ py.": C:\Users\Al>dir *.py Volume in drive C is Windows Volume Serial Number is DFF3-8658 Directory of C:\Users\Al 03/24/2019 10:45 PM 8,399 conwaygameoflife.py 03/24/2019 11:00 PM 7,896 test1.py 10/29/2019 08:18 PM 21,254 wizcoin.py 3 File(s) 37,549 bytes 0 Dir(s) 506,300,776,448 bytes free إن النمط العام records201?.txt يعني أي ملف يبدأ بالسلسلة records201 متبوعة بأي محرف متبوعًا بالسلسلة txt.، وهذا ما يوافق بالنتيجة كافة الملفات ابتداءً من records2010.txt وصولًا إلى records2019.txt (مع أي ملفات بأسماء من قبيل records201X.txt)، في حين أن النمط العام record20??.txt يوافق أي ملف بأي محرفين مكان ?? مثل records2021.txt أو records20AB.txt. تغيير المجلدات باستخدام الأمر cd إن تنفيذ الأمر cd متبوعًا باسم المجلد المستهدف يغير دليل العمل الحالي في الصَدَفَة إلى المجلد المطلوب: C:\Users\Al>cd Desktop C:\Users\Al\Desktop> تعرض الصدفة دليل العمل الحالي كجزء من موجه الأوامر، وأي مجلدات أو ملفات مستخدمة لاحقًا في الأوامر سيتم التعامل معها نسبةً إلى هذا الدليل. وفي حال كون اسم المجلد يتضمن مسافات، فلابد من تضمينه بين علامتي اقتباس مزدوجة. ولتغيير دليل العمل الحالي ليكون المجلد الرئيسي للمستخدم، نكتب الأمر cd ~ وذلك في نظامي التشغيل ماك أو إس ولينكس، أما في نظام ويندوز فنكتب الأمر cd %%USERPROFILE. وإن رغبت بتغيير محرك الأقراص الحالي أيضًا في ويندوز، فلابد بدايةً من إدخال اسم القرص المطلوب كأمرٍ منفصل: C:\Users\Al>d: D:\>cd BackupFiles D:\BackupFiles> أما لتغيير دليل العمل ليصبح هو المجلد الأب لدليل العمل الحالي، يكفي استخدام .. كاسم للمجلد، بالشكل: C:\Users\Al>cd .. C:\Users> عرض قائمة بمحتويات مجلد باستخدام الأمرين dir و ls يعرض الأمر dir الملفات والمجلدات ضمن دليل العمل الحالي وذلك في نظام التشغيل ويندوز، ويؤدي الأمر ls نفس المهمة في نظامي التشغيل ماك أو إس ولينكس، كما يمكن عرض محتويات مجلد آخر باستخدام الأمر dir أو ls متبوعًا باسم المجلد الآخر. يعد كل من رمزي الانتقال l- و a- وسيطًا مساعدًا مفيدًا للأمر ls، الذي يعرض افتراضيًا أسماء الملفات والمجلدات الموجودة فقط، أما لعرض قائمة أكثر تفصيلًا تحتوي على أحجام الملفات وسماحياتها وتاريخ آخر تعديل عليها وغيرها من المعلومات، فنستخدم الوسيط l-. يعامِل كل من نظامي التشغيل ماك أو إس ولينكس الملفات التي تبدأ بنقطة على أنها ملفات إعدادات، فيبقيانها مخفية عن الأوامر العادية، وهنا من الممكن استخدام الوسيط -a ليعرض الأمر ls جميع الملفات بما فيها المخفية، ولعرض قائمة المعلومات الموسعة والملفات المخفية معًا ندمج وسيطي التحويل كما يلي ls -al، وفيما يلي مثال من نافذة موجه الأوامر في نظام ماك او إس أو لينكس: al@ubuntu:~$ ls Desktop Downloads mu_code Pictures snap Videos Documents examples.desktop Music Public Templates al@ubuntu:~$ ls -al total 112 drwxr-xr-x 18 al al 4096 Aug 4 18:47 . drwxr-xr-x 3 root root 4096 Jun 17 18:11 .. -rw------- 1 al al 5157 Aug 2 20:43 .bash_history -rw-r--r-- 1 al al 220 Jun 17 18:11 .bash_logout -rw-r--r-- 1 al al 3771 Jun 17 18:11 .bashrc drwx------ 17 al al 4096 Jul 30 10:16 .cache drwx------ 14 al al 4096 Jun 19 15:04 .config drwxr-xr-x 2 al al 4096 Aug 4 17:33 Desktop --snip-- يعد الأمر dir في نظام ويندوز نظير الأمر ls -al، وفيما يلي مثال من نافذة موجه الأوامر في نظام ويندوز: C:\Users\Al>dir Volume in drive C is Windows Volume Serial Number is DFF3-8658 Directory of C:\Users\Al 06/12/2019 05:18 PM <DIR> . 06/12/2019 05:18 PM <DIR> .. 12/04/2018 07:16 PM <DIR> .android --snip-- 08/31/2018 12:47 AM 14,618 projectz.ipynb 10/29/2014 04:34 PM 121,474 foo.jpg عرض محتويات المجلدات الفرعية باستخدام الأمرين dir /s و find بتنفيذ الأمر dir /s في نظام التشغيل ويندوز تعرض المجلدات مع ما تحويه من مجلدات فرعية، فعلى سبيل المثال يعرض الأمر التالي جميع المجلدات التي تنتهي باللاحقة py. في المجلد C:\github\ezgmail مع كافة المجلدات الفرعية ضمنه: C:\github\ezgmail>dir /s *.py Volume in drive C is Windows Volume Serial Number is DEE0-8982 Directory of C:\github\ezgmail 06/17/2019 06:58 AM 1,396 setup.py 1 File(s) 1,396 bytes Directory of C:\github\ezgmail\docs 12/07/2018 09:43 PM 5,504 conf.py 1 File(s) 5,504 bytes Directory of C:\github\ezgmail\src\ezgmail 06/23/2019 07:45 PM 23,565 __init__.py 12/07/2018 09:43 PM 56 __main__.py 2 File(s) 23,621 bytes Total Files Listed: 4 File(s) 30,521 bytes 0 Dir(s) 505,407,283,200 bytes free ويؤدي الأمر find . –name في نظامي ماك أو إس ولينكس الوظيفة ذاتها، كالتالي: al@ubuntu:~/Desktop$ find . -name "*.py" ./someSubFolder/eggs.py ./someSubFolder/bacon.py ./spam.py وجود النقطة (.) يجعل الأمر find يبحث في دليل العمل الحالي، والخيار name- يجعله يبحث عن جميع أسماء المجلدات والملفات، أما الوسيط py.* ذو محرف البدل فيجعل الأمر find يعرض كافة المجلدات والملفات ذات الأسماء التي تتطابق النمط العام py.*، ومن الجدير بالملاحظة أن الأمر find يتطلب كون أي سلسلة تلي الوسيطname- محصورة بين علامتي اقتباس مزدوجتين. نسخ الملفات والمجلدات باستخدام الأمرين copy و cp لإنشاء نسخة من ملف أو مجلد في مجلد آخر، نكتب الأمر copy [source file or folder] [destination folder] أو cp [source file or folder] [destination folder] ، وفيما يلي مثال من نافذة موجه الأوامر في نظام لينكس: al@ubuntu:~/someFolder$ ls hello.py someSubFolder al@ubuntu:~/someFolder$ cp hello.py someSubFolder al@ubuntu:~/someFolder$ cd someSubFolder al@ubuntu:~/someFolder/someSubFolder$ ls hello.py الأسماء المختصرة للأوامر: عندما بدأت بتعلم نظام التشغيل لينكس، تفاجأت عندما اكتشفت أن الأمر copy الخاص بويندوز يدعى cp في نظام لينكس، إذ أن الاسم "copy" كان أكثر وضوحًا من "cp"، وفكرت حينها، هل يستحق اختصار محرفين جعل الاسم غامضًا؟ ومع الوقت ومع اكتسابي للمزيد من الخبرة في نوافذ سطر الأوامر، وجدتُ أن الجواب هو نعم بكل تأكيد. ففي حين أننا نقرأ الشيفرة المصدرية أكثر مما نكتبها، فإن استخدام أسماء كاملة وطويلة للمتغيرات والدوال مفيد بالفعل، أما في نافذة سطر الأوامر فإننا نكتب الأوامر أكثر بكثير من قراءتها، وبالتالي في هذه الحالة العكس هو الصحيح، بمعنى أن استخدام أسماء مختصرة أو قصيرة للأوامر يجعل من عبء كتابتها أيسر. نقل الملفات والمجلدات باستخدام الأمرين move و mv في نظام التشغيل ويندوز، يمكننا نقل ملف أو مجلد من مصدره إلى مجلد وجهة جديد من خلال تنفيذ الأمر move [source file or folder] [destination folder]، وبنفس الطريقة يؤدي الأمر mv [source file or folder] [destination folder] نفس الوظيفة في كل من نظامي التشغيل ماك أو إس ولينكس، فيما يلي مثال من نافذة موجه الأوامر في ويندوز: al@ubuntu:~/someFolder$ ls hello.py someSubFolder al@ubuntu:~/someFolder$ mv hello.py someSubFolder al@ubuntu:~/someFolder$ ls someSubFolder al@ubuntu:~/someFolder$ cd someSubFolder/ al@ubuntu:~/someFolder/someSubFolder$ ls hello.py إذ تم نقل الملف hello.py من مصدره ~/someFolder إلى الوجهة الجديدة ~/someFolder/someFolder، ولم يعد ظاهرًا في موقعه الأصلي. إعادة تسمية الملفات والمجلدات باستخدام الأمرين ren و mv بتنفيذ الأمرren [file or folder] [new name] نعيد تسمية الملف أو المجلد في نظام ويندوز، وبطريقة مشابهة يتم ذلك في نظامي ماك أو إس ولينكس باستخدام الأمر mv [file or folder] [new name]، ومن الجدير بالملاحظة أنه من الممكن استخدام الامر mv في نظامي ماك أو إس ولينكس لإعادة تسمية الملفات ونقلها أيضًا، ففي حال تم وضع اسم ملف موجود مسبقًا في موقع الوسيط الثاني المخصص للاسم الجديد للملف في حالة الرغبة بإعادة تسميته، فعندها ينقل الأمر mv الملف أو المجلد من مصدره إلى ذلك المجلد، أما في حال وضع اسمًا جديدًا لا يطابق أي من أسماء المجلدات الموجودة أصلًا، عندها يعيد الأمر mv تسمية الملف أو المجلد، وفيما يلي مثال من نافذة موجه الأوامر في نظام التشغيل لينكس: al@ubuntu:~/someFolder$ ls hello.py someSubFolder al@ubuntu:~/someFolder$ mv hello.py goodbye.py al@ubuntu:~/someFolder$ ls goodbye.py someSubFolder وبذلك تغير اسم الملف hello.py ليصبح goodbye.py. حذف الملفات والمجلدات باستخدام الأمرين del و rm لحذف ملف أو مجلد في نظام التشغيل ويندوز ننفذ الأمرdel [file or folder]، أما في نظامي التشغيل ماك أو إس ولينكس فننفذ الأمرrm [file] (إذ أن rm هو اختصار لكلمة remove). ولأمري الحذف السابقين اختلافات بسيطة، إذ أن تنفيذ الأمر del في نظام ويندوز على مجلد ما يؤدي إلى حذف كافة ملفاته دون مجلداته الفرعية، كما أن هذا الأمر لن يحذف المجلد المصدري، ولحذفه بكامل محتوياته بالكامل، استخدام أحد الأمرين rd أو rmdir اللذان سنشرحهما في فقرة "حذف الملفات باستخدام الأمرين rd وrmdir" لاحقًا، وبالتالي فإن تنفيذ الأمر del [folder] لن يحذف أي ملفات داخل المجلدات الفرعية لهذا المجلد المصدري، ولحذفها ننفذ الأمر بالشكل del /s /q [folder]، حيث يضمن الوسيط /s تطبيق الأمر del على المجلدات الفرعية، في حين يمثل الوسيط q/ تأكيدًا للعملية، ويوضح الشكل التالي هذا الفرق. القسم الأيسر يبين كيفية حذف الملفات في حال تنفيذ الأمرdel delicious، أما القسم الأيمن لحالة تنفيذ الأمر بالشكل del /s /q delicious من غير الممكن استخدام الأمر rm في نظامي التشغيل ماك أو إس ولينكس لحذف المجلدات وحده، لكن من الممكن استخدامه بالشكل rm -r [folder] لحذف المجلد بجميع محتوياته، وينفذ الأمر rd /s /q [folder] في نظام التشغيل ويندوز المهمة ذاتها، ويوضح الشكل التالي هذه الفكرة. يتم حذف المجلد بكامل محتوياته عند تنفيذ الأمر rd /s /q delicious أو rm -r delicious. إنشاء المجلدات باستخدام الأمرين md و mkdir من الممكن إنشاء مجلد جديد فارغ في نظام التشغيل ويندوز بتنفيذ الأمر md [new folder]، أما في نظامي ماك أو إس ولينكس فيتم ذلك باستخدام الأمر mkdir [folder]، كما يعمل الأمر mkdir أيضًا في نظام ويندوز، إلا ان الأمر md أسهل في الكتابة، وفيما يلي مثال من نافذة موجه الأوامر في لينكس: al@ubuntu:~/Desktop$ mkdir yourScripts al@ubuntu:~/Desktop$ cd yourScripts 1 al@ubuntu:~/Desktop/yourScripts$ ls al@ubuntu:~/Desktop/yourScripts$ نلاحظ أن الملف الجديد الذي تم إنشاؤه فارغ وبالتالي لا يظهر أي شيء لدى تنفيذ أمر عرض المحتويات ls (السطر المشار له بالرقم 1). حذف المجلدات باستخدام الأمرين rd و rmdir في نظام ويندوز، وبتنفيذ الأمر rd [source folder] يتم حذف المجلد المصدري، ولإنجاز نفس المهمة في نظامي التشغيل ماك أو إس ولينكس، نستخدم الأمر rmdir [source folder]. مع ملاحظة أن الأمر rmdir يؤدي أيضًا نفس الوظيفة في نظام ويندوز، إلا أن rd أسهل للكتابة، ولابد من كون المجلد فارغًا لنتمكن من حذفه، وفيما يلي مثال من نافذة موجه الأوامر في نظام التشغيل ويندوز: al@ubuntu:~/Desktop$ mkdir yourScripts al@ubuntu:~/Desktop$ ls yourScripts al@ubuntu:~/Desktop$ rmdir yourScripts al@ubuntu:~/Desktop$ ls al@ubuntu:~/Desktop$ أنشأنا في المثال السابق مجلدًا فارغًا باسم yourScript، ومن ثم حذفناه. أما لحذف المجلدات غير الفارغة (مع كل ما تتضمنه من ملفات ومجلدات)، فننفذ الأمر rd /s /q [source folder] في نظام التشغيل ويندوز، أو الأمر rm -rf [source folder] في نظامي التشغيل ماك أو إس ولينكس. إيجاد البرامج باستخدام الأمرين where و which بتنفيذ الأمر where [program] في نظام التشغيل ويندوز أو الأمر which [program] في نظامي ماك أو إس ولينكس نتبين الموقع المحدد للبرنامج، فعندما ندخل أمرًا في نافذة سطر الأوامر، يبدأ الحاسوب بالبحث عن البرنامج في المجلدات الموجودة في متغير البيئة PATH (رغم أن نظام ويندوز يبحث أولًا في دليل العمل الحالي). إذ تبين لنا هذه الأوامر أي من برامج بايثون التنفيذية سيتم تشغيلها لدى استخدام الأمر pythin في الصَدَفَة، ففي حال وجود عدة إصدارات من بايثون مثبتة على الجهاز، فسيتضمن بالنتيجة عدة برامج تنفيذية وبنفس الاسم، وستكون أفضلية التشغيل حسب ترتيبها في متغير البيئة PATH، وهنا يأتي دور الأمرين where و when لعرضه، كما في المثال: C:\Users\Al>where python C:\Users\Al\AppData\Local\Programs\Python\Python38\python.exe يشير اسم المجلد في المثال السابق إلى أن نسخة برنامج بايثون التي تعمل من الصدفة موجودة في المسار الآتي: C:\Users\Al\AppData\Local\Programs\Python\Python38 تنظيف نافذة موجه الأوامر باستخدام الأمرين cls و clear لحذف كافة النصوص من نافذة موجه الأوامر، ننفذ الأمر cls في نظام التشغيل ويندوز أو الأمر clear في نظامي التشغيل ماك أو إس أو لينكس، الأمر المفيد لدى رغبتك بالبدء بنافذة سطر أوامر جديدة فارغة. متغيرات البيئة ومتغير المسار PATH تحتوي جميع العمليات قيد التنفيذ وبغض النظر عن اللغة البرمجية التي كتبت بها، على مجموعة من المتغيرات المسماة متغيرات البيئة، وهي قادرة على تخزين السلاسل النصية. وعادةً ما تتضمن متغيرات البيئة إعداداتٍ على مستوى النظام، والتي يحتاجها أي برنامج، فعلى سبيل المثال يحتوي متغير البيئة TEMP على مسار الملف الذي يمكن لأي برنامج أن يخزن فيه الملفات المؤقتة. عندما يُشغّل نظام التشغيل برنامجًا (مثل نافذة سطر الأوامر)، فتتلقى العملية المنشأة حديثًا هذه نسختها من متغيرات ومعطيات البيئة الخاصة بنظام التشغيل. ومن الممكن تعديل متغيرات البيئة الخاصة بالعملية فرديًا، دون تعديل مجموعة متغيرات البيئة الخاصة بنظام التشغيل، ما يضمن تطبيق التغيرات فقط على هذه العملية وليس على نظام التشغيل أو على غيرها من العمليات. والهدف الأساس من التطرق إلى متغيرات البيئة في هذا المقال، هو أن المتغير PATH وهو أحدها، قادر على المساعدة في تشغيل البرامج باستخدام نافذة سطر الأوامر. عرض متغيرات البيئة من الممكن عرض قائمة بمتغيرات البيئة الخاصة بنافذة موجه الأوامر باستخدام الأمر set (في نظام التشغيل ويندوز) أو الأمر env (في نظامي التشغيل ماك أو إي ولينكس)، كما في المثال: C:\Users\Al>set ALLUSERSPROFILE=C:\ProgramData APPDATA=C:\Users\Al\AppData\Roaming CommonProgramFiles=C:\Program Files\Common Files --snip-- USERPROFILE=C:\Users\Al VBOX_MSI_INSTALL_PATH=C:\Program Files\Oracle\VirtualBox\ windir=C:\WINDOWS إن النص على يسار إشارة المساواة (=) هو اسم متغير البيئة والنص على يمينها هو قيمة السلسلة التي يتضمنها، ولكل عملية مجموعتها الخاصة من متغيرات البيئة لذلك يمكن لسطر الأوامر المختلفة أن تمتلك قيمًا مختلفة لمتغيرات البيئة نفسها. كما من الممكن عرض قيمة متغير بيئة بحد ذاته باستخدام الأمر echo، فمثلًا بتنفيذ الأمر echo %HOMEPATH% في نظام التشغيل ويندوز أو الأمر echo $HOME في نظامي التشغيل ماك أو إس ولينكس، تُعرض قيمة متغيرات البيئة HOMEPATH أو HOME على التوالي، والتي تحتوي على المجلد الرئيسي الحالي للمستخدم، ويبدو الأمر كالتالي في نظام التشغيل ويندوز: C:\Users\Al>echo %HOMEPATH% \Users\Al أما في نظامي التشغيل ماك أو إس ولينكس، فيبدو بالشكل: al@al-VirtualBox:~$ echo $HOME /home/al إن قامت تلك العملية بإنشاء عملية أخرى (كأن يقوم برنامج سطر الأوامر بتشغيل مفسر بايثون) فإن العملية الابن تحصل على نسختها الخاصة من متغيرات البيئة الخاصة بالعملية الأب، ومن الممكن التعديل على متغيرات البيئة للعملية الابن دون التأثير على متغيرات البيئة الخاصة بالعملية الأب وبالعكس. وبالتالي من الممكن عد متغيرات البيئة الخاصة بنظام التشغيل على أنها "مجموعة رئيسية"، تنسخ منها العمليات متغيرات البيئة اللازمة لعملها، وإجمالًا فإن متغيرات البيئة الخاصة بنظام التشغيل قد تُعدل بطريقة أقل مما هو عليه لمتغيرات البيئة الخاصة ببرنامج بايثون، وفي الواقع فمن النادر أن يعدل المستخدمون متغيرات البيئة مباشرةً. الخلاصة يتضمن ضبط البيئة كافة الخطوات الضرورية لجعل الحاسوب قادرًا على تشغيل برامجنا بسهولة، وتتطلب هذه العملية منا معرفة بعض مفاهيم الأولية حول كيفية عمل الحاسوب، كفهم نافذة سطر الأوامر وأهم الأوامر ووسطاء كل منها. ورغم اختلاف موجه الأوامر وأسماء الأوامر الشائعة باختلاف أنظمة التشغيل، إلا أنها تؤدي بالنتيجة المهام ذاتها. قد يستغرق الأمر وقتًا لتعتاد التعامل مع موجه الأوامر، نظرًا لكثرة الأوامر ووسطاء كل منها، لذا لا تتردد في البحث مطولًا على الإنترنت عن المساعدة، فهو الأمر الطبيعي الذي يقوم به مطورو البرامج يوميًا. ترجمة -وبتصرف- للفصل الثاني "إعداد البيئة وواجهة سطر الأوامر" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: إعداد البيئة وواجهة سطر الأوامر في بايثون دليل استخدام سطر الأوامر في عملية تطوير الويب من طرف العميل المرجع الشامل إلى تعلم لغة بايثون تعرف على المتغيرات وكيفية التعامل معها في بايثون
-
نحتاج عادةً إلى قاعدة بيانات في تطبيقات الويب، وهي مجموعةٌ مُنطمّةٌ من البيانات نستخدمها لتخزين وتنظيم البيانات الدائمة، موفّرةً لنا إمكانية استرجاعها ومعالجتها بفعالية، إذ سنحتاج مثلًا في تطبيق ما للتواصل الاجتماعي إلى قاعدة بيانات لتخزين بيانات المستخدم (من معلومات شخصية ومنشورات وتعليقات ومتابعين) لنتمكّن من معالجتها بفعالية، إذ توفّر قاعدة البيانات إمكانية إضافة بيانات جديدة إليها، أو استرجاع بيانات مُخزّنة أصلًا، أو التعديل عليها، أو حذفها بكل مرونة وذلك اعتمادًا على المتطلبات والشروط والظروف المُختلفة، ففي تطبيق ويب ما قد تكون هذه المتطلبات مثلًا هي حالة إضافة المُستخدم لمنشور جديد، أو حذف منشور سابق، أو حذف حسابه مع كافّة منشوراته أو مع الإبقاء عليها، بمعنى أنّ طريقة معالجة البيانات تعتمد أولًا وأخيرًا على الميزات التي يتيحها التطبيق، فقد لا ترغب مثلًا بالسماح لمُستخدمي تطبيقك بإضافة منشورات غير معنونة. يُعد فلاسك إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا عدة أدوات وميزات من شأنها إنشاء تطبيقات ويب في لغة بايثون. أمّا SQLAlchemy فهي أداة في محرك قواعد البيانات SQL تؤمن وصولًا فعالًا وعالي الأداء إلى قواعد البيانات العلاقية، كما توفّر طرقًا للتخاطب مع العديد من محركات قواعد البيانات، مثل SQLite و MySQL و PostgreSQ، مانحةً إمكانية الوصول إلى آليات SQL الخاصة بقواعد البيانات، كما توفّر مُخطِّط الكائنات العلاقية Object Relational Mapper -أو اختصارًا ORM- الذي يتيح إمكانية إنشاء الاستعلامات والتعامل مع البيانات باستخدام توابع وكائنات بسيطة في بايثون. تُعدّ Flask-SQLAlchemy بمثابة إضافة لفلاسك، لتسهيل استخدام SQLAlchemy ضمن فلاسك، وتؤمن الأدوات والوسائل المناسبة للتعامل مع قاعدة البيانات في تطبيقات فلاسك من خلال SQLAlchemy. سنعمل في هذا المقال على إنشاء نظام إدارة صغير للطلاب يبين لنا كيفية استخدام إضافة Flask-SQLAlchemy، إذ سنستخدمها مع فلاسك لأداء مهام أساسية، مثل الاتصال بخادم قاعدة البيانات، أو إنشاء جدول وإضافة بيانات إليه واسترجاع هذه البيانات وتحديث وحذف العناصر من قاعدة البيانات. سنستخدم الإضافة SQLAlchemy مع محرّك قواعد البيانات SQLite رغم أنه من الممكن استخدامها مع محركات قواعد بيانات أخرى، مثل PostgreSQL و MySQL، إلّا أنّ SQLite تعمل بكفاءة مع بايثون، لأن المكتبة القياسية لبايثون توفر الوحدة sqlite3 المُستخدمة من قبل SQLAlchemy في الخلفية للتعامل مع قاعدة بيانات SQLite دون الحاجة إلى تثبيت أي شيء إضافي، إذ تُثبّت SQlite افتراضيًا على أنظمة لينكس Linux، كما تُثبّت على أنها جزءٌ من حزمة بايثون في أنظمة تشغيل ويندوز Windows. مستلزمات العمل قبل المتابعة في هذا المقال لا بُدّ من: توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو flask_app. الفهم الجيد لأساسيات فلاسك، مثل مفهوم الوجهات ودوال العرض، ويمكنك في هذا الصدد الاطلاع على المقالين كيفية بناء موقعك الإلكتروني الأول باستخدام إطار عمل فلاسك Flask من لغة بايثون وكيفية استخدام القوالب في تطبيقات فلاسك Flask لفهم مبادئ فلاسك. فهم أساسيات لغة HTML. الخطوة 1 – تثبيت فلاسك والإضافة Flask-SQLAlchemy سنعمل في هذه الخطوة على تثبيت كل من فلاسك وكافّة الحزم اللازمة لعمل التطبيق. لذلك، وبعد التأكّد من كون البيئة الافتراضية مُفعّلة، سنستخدم أمر تثبيت الحزم pip لتثبيت كل من فلاسك والإضافة Flask-SQLAlchemy على النحو التالي: (env)user@localhost:$ pip install Flask Flask-SQLAlchemy وبمجرّد انتهاء التثبيت بنجاح، سيظهر في السطر الأخير من الخرج ما يشبه التالي: Successfully installed Flask-2.0.3 Flask-SQLAlchemy-2.5.1 Jinja2-3.0.3 MarkupSafe-2.1.0 SQLAlchemy-1.4.31 Werkzeug-2.0.3 click-8.0.4 greenlet-1.1.2 itsdangerous-2.1.0 ومع نهاية هذه الخطوة نكون قد ثبتنا ما يلزم من حزم بايثون، وأصبح من الممكن الانتقال إلى الخطوة التالية المُتمثّلة بإعداد قاعدة البيانات. الخطوة 2 – إعداد النموذج وقاعدة البيانات سنعمل في هذه الخطوة على إعداد الاتصال مع قاعدة البيانات وإنشاء نموذج قاعدة البيانات SQLAlchemy، وهو صنف بايثون يمثّل الجدول الذي سيخزّن البيانات، إذ سنعمل على تهيئة قاعدة البيانات وإنشاء جدول للطلاب وفقًا للنموذج الذي سنصرّح عنه، ونهايةً سنضيف بعض الطلاب إلى جدول الطلاب هذا. إعداد الاتصال بقاعدة البيانات سننشئ بدايةً ملف باسم app.py ضمن المجلد flask.app والذي سيحتوي على الشيفرات الخاصة بإعداد قاعدة البيانات ووجهات فلاسك اللازمة لعمل التطبيق: (env)user@localhost:$ nano app.py سيتصل هذا الملف بقاعدة بيانات من نوع SQLite باسم database.db، كما يحتوي على صنف يدعى Student يمثّل جدول الطلاب في قاعدة البيانات المُستخدم لتخزين معلوماتهم، إضافةً إلى وجهات فلاسك. سنضيف الاستدعاءات التالية باستخدام التعليمة import إلى بداية الملف app.py: import os from flask import Flask, render_template, request, url_for, redirect from flask_sqlalchemy import SQLAlchemy from sqlalchemy.sql import func استوردنا في الشيفرة السابقة الوحدة os التي تمكنّنا من الوصول إلى واجهات نظام التشغيل المختلفة، والتي سنستخدمها في بناء مسار ملف قاعدة البيانات database.db، كما استوردنا من حزمة فلاسك كافّة المُساعدات اللازمة للتطبيق، مثل الصنف فلاسك المُستخدم في إنشاء النسخة الفعلية من التطبيق، والدالة ()render_template لتصيير قوالب HTML، والكائن request المسؤول عن التعامل مع الطلبيات، والدالة ()url_for لبناء روابط الوجهات، والدالة ()redirect لإعادة توجيه المُستخدمين من صفحة لأُخرى. بعد ذلك، استوردنا الصنف SQLAlchemy من الإضافة Flask-SQLAlchemy، والذي يسمح لنا بالوصول إلى جميع الدوال والأصناف الخاصة بالإضافة SQLAlchemy، إضافةً إلى المُساعدات والآليات التي تدعم عمل فلاسك مع هذه الإضافة، والتي سنستخدمها لإنشاء كائن قاعدة بيانات يتصل مع تطبيق فلاسك سامحًا لنا بإنشاء وتعديل الجداول باستخدام أصناف وكائنات ودوال بايثون دون الحاجة لاستخدام لغة SQL. كما استوردنا المساعد func من الوحدة sqlalchemy.sql للوصول إلى دوال SQL، والتي سنستخدمها في نظام إدارة الطلاب لضبط الوقت والتاريخ افتراضيًا لدى إنشاء سجل طالب جديد. وبعد الانتهاء من الاستدعاءات، سنعيّن مسار ملف قاعدة البيانات، ونستنسخ تطبيق فلاسك، كما سنضبط ونؤسس اتصالًا للتطبيق مع الإضافة SQLAlchemy، ومن ثمّ سنضيف الشيفرة التالية: basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] =\ 'sqlite:///' + os.path.join(basedir, 'database.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) أنشأنا في الشيفرة السابقة مسارًا لملف قاعدة البيانات SQLite، إذ حددنا المجلد الحالي ليكون المجلد الرئيسي، كما استخدمنا الدالة ()os.path.abspath للحصول على المسار الكامل لمجلد الملف الحالي، إذ يتضمّن المتغير الخاص __file__ اسم مسار الملف الحالي app.py، لنخزّن المسار الكلي للمجلد الأساسي ضمن المتغير basedir. ثمّ أنشأنا نسخةً فعليةً من تطبيق فلاسك باسم app لنستخدمها في ضبط مفتاحي الضبط الخاصين بالإضافة Flask-SQLAlchemy، وهما: SQLALCHEMY_DATABASE_URI: وهو معرّف الموارد الموحّد الخاص بقاعدة البيانات database URI المسؤول عن تحديد قاعدة البيانات المراد إنشاء اتصال معها، وفي هذه الحالة تكون صيغة هذا المعرّف بالشكل sqlite:///path/to/database.db، إذ نستخدم الدالة ()op.path.join لتحقيق الربط المدروس بين المجلد الأساسي الذي أنشاناه وخزنّا مساره في المتغير basedir، وبين اسم الملف database.db، الأمر الذي يسمح بالاتصال مع ملف قاعدة البيانات database.db الموجود في المجلد flask.app، إذ سيُنشأ هذا الملف فور تهيئة قاعدة البيانات. SQLALCHEMY_TRACK_MODIFICATIONS: وهو إعداد لتمكين أو إلغاء تمكين ميزة تتبع تغيرات الكائنات، وقد عيّناه إلى القيمة false لإلغاء التتبع وبالتالي تحقيق استخدام أقل لموارد الذاكرة. ملاحظة: من الممكن استخدام أي محرّك قواعد بيانات آخر، مثل PostgreSQL، أو MySQL، وفي هذه الحالة يجب استخدام معرّف الموارد الموحد URI المناسب؛ ففي حال استخدام PostgreSQL، سيتّبع الصيغة: postgresql://username:password@host:port/database_name أمّا في حال استخدام MySQL: mysql://username:password@host:port/database_name وبعد الانتهاء من ضبط الإضافة SQLAIchemy من خلال تعيين معرّف الموارد الموحّد لقاعدة البيانات وإلغاء تمكين ميزة تتبّع التغييرات، سننشئ كائن قاعدة بيانات باستخدام صنف الإضافة SQLAIchemy، ممررين إليه نسخة التطبيق بغية ربط تطبيق فلاسك مع الإضافة SQLAIchemy، وسنخزّن كائن قاعدة البيانات هذا ضمن متغير باسم db، والذي سنستخدمه في التخاطب مع قاعدة البيانات. التصريح عن الجداول بعد تأسيس الاتصال مع قاعدة البيانات وإنشاء كائن قاعدة البيانات، سنستخدم هذا الكائن لإنشاء جدول للطلاب في قاعدة البيانات، والمُمثّل بنموذج model، وهو صنف بايثون يرث من صنف رئيسي توفرّه الإضافة Flask-SQLAlchemy من خلال نسخة قاعدة البيانات db التي أنشأناها مُسبقًا، لذا وبغية تعريف جدول الطلاب مثل نموذج، سنضيف الصنف التالي إلى الملف app.py على النحو التالي: # ... class Student(db.Model): id = db.Column(db.Integer, primary_key=True) firstname = db.Column(db.String(100), nullable=False) lastname = db.Column(db.String(100), nullable=False) email = db.Column(db.String(80), unique=True, nullable=False) age = db.Column(db.Integer) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) bio = db.Column(db.Text) def __repr__(self): return f'<Student {self.firstname}>' أنشأنا في الشيفرة السابقة النموذج Student الممثّل لجدول الطلاب والذي يرث من الصنف db.Model، كما استخدمنا الصنف db.Column لتعريف أعمدة الجدول، إذ يمثّل الوسيط الأوّل نوع العمود، أمّا باقي الوسطاء فتمثّل إعدادات العمود. وقد عرفنا الأعمدة التالية للنموذج Student: id: وهو معرّف الطالب، ويحتوي على بياناتٍ من نوع رقم صحيح وذلك باستخدام db.Integer و primary_key=True التي تعرّفه مفتاحًا أساسيًا، الذي يخصّص قيمةً فريدةً في قاعدة البيانات من أجل كل سجل (والسجل هو الطالب في حالتنا). firstname: الاسم الأول للطالب، وهو سلسلة نصية بعدد محارف أعظمي يساوي 100، وتشير التعليمة nullable=False إلى أن هذا العمود لا يجب أن يحتوي على قيمٍ فارغة. lastname: الاسم الأخير للطالب، وهو سلسلة نصية بعدد محارف أعظمي يساوي 100، وتشير التعليمة nullable=False إلى أن هذا العمود لا يجب أن يحتوي على قيمٍ فارغة. email: عنوان البريد الإلكتروني للطالب، وهو سلسلة نصية بعدد محارف أعظمي يساوي 80، وتشير التعليمة unique=True إلى أنّ البريد الإلكتروني يجب أن يكون فريدًا لكل طالب، كما تشير التعليمة nullable=False إلى أن هذا العمود لا يجب أن يحتوي على قيمٍ فارغة. age: عمر الطالب. created_at: يحتوي على تاريخ ووقت إنشاء سجل الطالب في قاعدة البيانات، إذ استخدمنا الوحدة db.DateTime لتعريفه مثل كائن وقت وتاريخ datetime في بايثون. تمكّن التعليمة timezone=True خاصية دعم المنطقة الزمنية، وتعيّن الوحدة server_default القيمة الزمنية الافتراضية في قاعدة البيانات لحظة إنشاء الجدول وبذلك تتعامل قاعدة البيانات مع القيم الافتراضية عوضًا عن النموذج، الذي نمرّر إليه الدالة ()func.now التي تستدعي دالة ()now المسؤولة عن تحديد الوقت والتاريخ بلغة SQL، لتُصيّر في SQLite بالشّكل CURRENT_TIMESTAMP عند إنشاء جدول الطلاب. bio: السيرة الذاتية للطالب، إذ تشير ()db.Text إلى أنّ هذا العمود يضم نصوصًا طويلة. تمكننا الدالة الخاصة __repr__ من تمثيل كل كائن بصيغة سلسلة نصية، الأمر الذي يساعد في التعرّف على هذا الكائن لأغراض التنقيح، واستخدمنا في حالتنا الاسم الأوّل للطالب لهذا الغرض. وبذلك سيبدو الملف app.py حاليًا بالشّكل التالي: import os from flask import Flask, render_template, request, url_for, redirect from flask_sqlalchemy import SQLAlchemy from sqlalchemy.sql import func basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] =\ 'sqlite:///' + os.path.join(basedir, 'database.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) class Student(db.Model): id = db.Column(db.Integer, primary_key=True) firstname = db.Column(db.String(100), nullable=False) lastname = db.Column(db.String(100), nullable=False) email = db.Column(db.String(80), unique=True, nullable=False) age = db.Column(db.Integer) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) bio = db.Column(db.Text) def __repr__(self): return f'<Student {self.firstname}>' نحفظ الملف ونغلقه. إنشاء قاعدة بيانات الآن وبعد الانتهاء من إعداد كل من الاتصال مع قاعدة البيانات ونموذج الطالب، سنستخدم صدفة فلاسك لإنشاء كل من قاعدة البيانات وجدول الطالب اعتمادًا على النموذج المسمّى Student. نستخدم -بعد التأكّد من تفعيل البيئة الافتراضية- متغير البيئة FLASK_APP لتحديد الملف app.py على أنه يمثّل تطبيق فلاسك، ومن ثمّ نفتح صَدفة فلاسك باستخدام الأمر التالي وذلك أثناء وجودنا ضمن المجلد flask_app: (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ flask shell فتُفتح صَدفة بايثون تفاعلية خاصّة، تعمل على تشغيل الأوامر ضمن تطبيق فلاسك، وبذلك نضمن انّ دوال Flask-SQLAlchemy التي سنستدعيها ستتصل مع تطبيق فلاسك. الآن، سنستورد كائن قاعدة البيانات ونماذج الطلاب، ومن ثمّ سنشغّل الدالة ()db.create_all بغية إنشاء الجداول الموافقة لتلك النماذج، ولكن في حالتنا لا يوجد سوى نموذج واحد، ما يعني أنّ استدعاء الدالة سينتج عنه إنشاء جدول وحيد في قاعدة البيانات على النحو التالي: >>> from app import db, Student >>> db.create_all() نبقي الصدفة في حالة تشغيل، ونفتح نافذة طرفية جديدة وننتقل إلى المجلد flask_app، فنجد ملفًا جديدًا باسم database.db ضمنه. ملاحظة: لا تنشئ الدالة ()db.create_all أو تحدّث جدولًا موجودًا أصلًا، فمثلًا لو عدلنا النموذج الذي أنشأناه بإضافة عمود جديد إليه، ومن ثمّ شغلنا الدالة ()db.create_all، فلن تُطبّق التغييرات التي أجريناها على النموذج على الجدول في حال كان الجدول موجودًا أصلًا في قاعدة البيانات، ويكون الحل لهذه المشكلة بحذف كافّة الجداول الموجودة في قاعدة البيانات باستخدام الدالة ()db.drop_all ومن ثمّ إعادة إنشائها باستخدام الدالة ()db.create_all، كما يلي: >>> db.drop_all() >>> db.create_all() ستعمل هذه الشيفرة على تطبيق التعديلات على النماذج، كما ستحذف كافّة البيانات الموجودة في قاعدة البيانات، فلو أردت تحديث قاعدة البيانات مع الحفاظ على البيانات الموجودة فيها، لا بدّ من استخدام أداة ترحيل ملف تخطيط قاعدة البيانات schema migration، ويمكن باستخدامها تعديل الجداول مع الحفاظ على البيانات. لاستخدام هذه الأداة من الممكن الاستعانة بالإضافةFlask-Migrate لتنفيذ ترحيل ملف تخطيط قاعدة البيانات باستخدام الإضافة SQLAIchemy من خلال واجهة أسطر الأوامر في فلاسك. وفي حال ظهور رسالة خطأ، يجب التأكّد من كون التصريح عن كل من النموذج ومعرّف الموارد الموحّد لقاعدة البيانات صحيحًا. ملء الجدول الآن وبعد إنشاء كلٍ من قاعدة البيانات وجدول الطالب، سنستخدم صدفة فلاسك لإضافة بعض الطلاب إلى قاعدة البيانات من خلال النموذج Student. سنستخدم نفس صدفة فلاسك المُشغَّلة أصلًا، كما من الممكن فتح صَدفة جديدة بعد التأكد من تشغيل البيئة الافتراضية ضمن المجلد flask_app: (env)user@localhost:$ flask shell لإضافة طالب إلى قاعدة البيانات، لا بُدّ من استيراد كل من كائن قاعدة البيانات والنموذج Student، ثمّ بعد ذلك استنساخ النموذج لنمرر إليه بيانات الطالب عبر وسطائه الأساسية، على النحو التالي: >>> from app import db, Student >>> student_john = Student(firstname='john', lastname='doe', >>> email='jd@example.com', age=23, >>> bio='Biology student') إذ يمثّل الكائن student_john الطالب المُراد إضافته إلى قاعدة البيانات، إلّا أنّ هذا الكائن لم يُنسخ إلى قاعدة البيانات بعد، لذا سنتحقّق من هذا الكائن في صدفة فلاسك للحصول على السلسلة النصية المُمثّلة له والتي أنشأناها باستخدام التابع ()__repr__: >>> student_john فيظهر لنا خرج بالشّكل: <Student john> ومن الممكن الحصول على قيم الأعمدة باستخدام سمات الصنف المُعرّفة أصلًا في النموذج Student: >>> student_john.firstname >>> student_john.bio ويكون الخرج: 'john' 'Biology student' بما أن هذا الطالب لم يُضاف إلى قاعدة البيانات بعد، ستكون قيمة مُعرّفه None: >>> print(student_john.id) ويكون الخرج: None لإضافة هذا الطالب إلى قاعدة البيانات، يجب إضافته أولًا إلى جلسة قاعدة بيانات database session التي تدير إجراءات قاعدة البيانات، إذ توفّر الإضافة Flask-SQLAlchemy الكائن db.session، الذي ندير تغيرات قاعدة البيانات من خلاله. نضيف الكائن student_john إلى الجلسة باستخدام التابع ()db.session.add الذي يجهّزه للنسخ على قاعدة البيانات: >>> db.session.add(student_john) سيتطلّب إدخال الطالب في قاعدة البيانات استخدام التعليمة INSERT، ولكن لن يُخصَّص معرّف قبل تأكيد إجراءات قاعدة البيانات، ولتأكيد الإجراءات وحفظ تغيرات قاعدة البيانات، سنستخدم التابع ()db.session.commit كما يلي: >>> db.session.commit() والآن وبعد إضافة الطالب John إلى قاعدة البيانات، أصبح من الممكن الحصول على معرّفه، بالشّكل: >>> print(student_john.id) ويكون الخرج: 1 كما من الممكن استخدام التابع ()db.session.add لتحرير عنصر ما في قاعدة البيانات، فعلى سبيل المثال، من الممكن تعديل البريد الإلكتروني الخاص بالطالب على النحو التالي: >>> student_john.email = 'john_doe@example.com' >>> db.session.add(student_john) >>> db.session.commit() سنضيف الآن باستخدام صدفة فلاسك بضعة طلاب إلى قاعدة البيانات: >>> sammy = Student(firstname='Sammy', >>> lastname='Shark', >>> email='sammyshark@example.com', >>> age=20, >>> bio='Marine biology student') >>> carl = Student(firstname='Carl', >>> lastname='White', >>> email='carlwhite@example.com', >>> age=22, >>> bio='Marine geology student') >>> db.session.add(sammy) >>> db.session.add(carl) >>> db.session.commit() من الممكن الآن الاستعلام عن كافّة السجلات في جدول الطلاب باستخدام السمة query مع التابع ()all، كما يلي: >>> Student.query.all() فيظهر الخرج التالي: [<Student john>, <Student Sammy>, <Student Carl>] ومع نهاية هذه الخطوة أصبح لدينا ثلاثة طلاب في قاعدة البيانات، وسنعمل في الخطوة التالية على إنشاء وجهة فلاسك للصفحة الرئيسية للتطبيق بغية عرض جميع الطلاب الموجودين ضمن قاعدة البيانات في هذه الصفحة. الخطوة 3 – عرض جميع السجلات سنعمل في هذه الخطوة على إنشاء وجهة وقالب لعرض كافّة الطلاب الموجودين في قاعدة البيانات ضمن الصفحة الرئيسية للتطبيق. لذلك، نبقي صَدفة فلاسك قيد التشغيل، ونفتح نافذة طرفية جديدة. نفتح ملف app.py لنضيف إليه وجهةً للصفحة الرئيسية: (env)user@localhost:$ nano app.py ونضيف الوجهة التالية إلى نهاية الملف: # ... @app.route('/') def index(): students = Student.query.all() return render_template('index.html', students=students) نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة المُزخرف ()app.route لإنشاء دالة عرض باسم ()index، والتي نستعلم فيها ضمن قاعدة البيانات لجلب كافّة الطلاب باستخدام السمة query من النموذج Student، التي تمكننا من جلب عنصر أو أكثر من قاعدة البيانات باستخدام توابع مُختلفة، واستخدمنا التابع ()all لجلب كافّة مُدخلات الطلاب من قاعدة البيانات، لنخزّن نتائج الاستعلام ضمن متغير باسم students ممررين إياه إلى ملف قالب باسم index.html والذي سنصيّره باستخدام الدالة المساعدة ()render_template. الآن، وقبل إنشاء ملف القالب index.htmlالمسؤول عن عرض الطلاب الموجودين ضمن قاعدة البيانات في الصفحة الرئيسية للتطبيق، سننشئ ملف قالب رئيسي ليتضمّن كافة شيفرات HTML الأساسية اللازمة لترثها لاحقًا القوالب الأُخرى وهذا ما يجنبنا تكرار الشيفرات، ثمّ سننشئ ملف قالب الصفحة الرئيسية index.html المُصيّر أصلًا باستخدام الدالة ()index. لذا سننشئ مجلدًا للقوالب باسم templates، وسننشئ ضمنه ملف قالب باسم base.html، الذي سيمثّل القالب الأساسي لبقية القوالب: (env)user@localhost:$ mkdir templates (env)user@localhost:$ nano templates/base.html وسنكتب فيه الشيفرة التالية: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %} {% endblock %} - FlaskApp</title> <style> .title { margin: 5px; } .content { margin: 5px; width: 100%; display: flex; flex-direction: row; flex-wrap: wrap; } .student { flex: 20%; padding: 10px; margin: 5px; background-color: #f3f3f3; inline-size: 100%; } .bio { padding: 10px; margin: 5px; background-color: #ffffff; color: #004835; } .name a { color: #00a36f; text-decoration: none; } nav a { color: #d64161; font-size: 3em; margin-left: 50px; text-decoration: none; } </style> </head> <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="#">Create</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% block content %} {% endblock %} </div> </body> </html> نحفظ الملف ونغلقه. يتضمّن القالب الأساسي هذا كافّة الشيفرات المتداولة التي سنحتاجها في القوالب الأُخرى. ستُستبدل كتلة العنوان title لاحقًا بعنوان كل صفحة وكتلة المحتوى content بمحتواها، أمّا عن شريط التصفح فسيتضمّن ثلاثة روابط، الأوّل ينقل المُستخدم إلى الصفحة الرئيسية للتطبيق باستخدام الدالة المساعدة ()url_for، والثاني إلى صفحة إضافة طالب جديد Create، أمّا الأخير لصفحة المعلومات حول التطبيق About في حال قررت تضمينها في تطبيقك، مع ملاحظة أنّنا سنعدّل هذا الملف لاحقًا بعد إنشاء صفحة إضافة طالب جديد بغية جعل الرابط إلى هذه الصفحة فعّالًا. الآن، سننشئ ملف قالب باسم index.html وهو الاسم الذي حددناه في الملف app.py: (env)user@localhost:$ nano templates/index.html ونضيف ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1 class="title">{% block title %} Students {% endblock %}</h1> <div class="content"> {% for student in students %} <div class="student"> <p><b>#{{ student.id }}</b></p> <b> <p class="name">{{ student.firstname }} {{ student.lastname }}</p> </b> <p>{{ student.email }}</p> <p>{{ student.age }} years old.</p> <p>Joined: {{ student.created_at }}</p> <div class="bio"> <h4>Bio</h4> <p>{{ student.bio }}</p> </div> </div> {% endfor %} </div> {% endblock %} نحفظ الملف ونغلقه. اعتمدنا في الشيفرة السابقة على الوراثة من ملف القالب base.html من خلال تعليمة extends، واستبدلنا محتوى كتلة المحتوى content مُستخدمين تنسيق العنوان من المستوى الأوّل <h1> الذي يفي أيضًا بالغرض عنوانًا للصفحة. استخدمنا في السطر البرمجي {% for post in posts %} حلقة for من تعليمات محرّك القوالب جينجا jinja، والهدف من استخدام هذه الحلقة هو المرور على كل طالب ضمن المتغير students المُمرّر من الدالة ()index إلى هذا القالب، إذ سنعرض معرّف الطالب واسمه الأوّل والأخير وتاريخ إضافته إلى قاعدة البيانات وسيرته الذاتية. الآن ومع وجودنا ضمن المجلد flask_app ومع كون البيئة الافتراضية مُفعّلة، سنُعلم فلاسك بالتطبيق المُراد تشغيله (وهو في حالتنا الملف app.py) باستخدام متغير البيئة FLASK_APP، في حين يحدّد متغير البيئة FLASK_ENV وضع التشغيل وهنا قد اخترنا الوضع development ما يعني أنّ التطبيق سيعمل في وضع التطوير مع تشغيل مُنقّح الأخطاء. لمزيدٍ من المعلومات حول مُنقّح الأخطاء في فلاسك ننصحك بقراءة المقال كيفية التعامل مع الأخطاء في تطبيقات فلاسك، ولتنفيذ ما سبق سنشغّل الأوامر التالية: (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development والآن سنشغّل التطبيق باستخدام الأمر flask run: (env)user@localhost:$ flask run وبعد التأكد من كون خادم التطوير ما يزال قيد التشغيل، نذهب إلى الرابط التالي باستخدام المتصفح: http://127.0.0.1:5000/ فيظهر لنا الطلاب المُضافين إلى قاعدة البيانات في صفحة مشابهة للصورة التالية: عرضنا في هذه الخطوة كافّة الطلاب الموجودين ضمن قاعدة البيانات في الصفحة الرئيسية للتطبيق، أمّا في الخطوة التالية فسنعمل على إنشاء وجهة لصفحة كل طالب، وسنعرض فيها تفاصيل كل طالب بصورةٍ منفردة. الخطوة 4 – عرض سجل مفرد سنستخدم في هذه الخطوة صدفة فلاسك للاستعلام عن الطلاب من خلال معرّفاتهم، كما سننشئ وجهة وقالب لعرض التفاصيل الخاصة بكل طالب في صفحة مُخصّصة لذلك. سيعرض الرابط http://127.0.0.1:5000/1 في نهاية هذه الخطوة صفحةً تعرض الطالب الأول (لأن الرابط يحوي على المعرّف رقم 1)، بينما يعرض الرابط http://127.0.0.1:5000/ID الطالب ذو المعرّف ID الموافق له إن وُجد. نبقي خادم التطوير قيد التشغيل، ونفتح نافذة طرفية جديدة. الآن سنفتح صدفة فلاسك لنبيّن كيفيّة الاستعلام عن الطلاب، كما يلي: (env)user@localhost:$ flask shell توفّر الإضافة Flask-SQLAlchemy السمة query ضمن صنف النموذج للاستعلام عن السجلات وجلب البيانات الموافقة من قاعدة البيانات، ويمكن استخدام توابع هذه السمة لجلب السجلات وفقًا لمُرشّح مُعيّن. على سبيل المثال، من الممكن استخدام تابع الترشيح ()filter_by مع معامل مثل firstname الذي يوافق عمودًا معينًا في الجدول مع استخدام وسيط لجلب طالب معيّن: >>> from app import db, Student >>> Student.query.filter_by(firstname='Sammy').all() فيكون الخرج على النحو التالي: [<Student Sammy>] جلبنا في الشيفرة السابقة جميع الطلاب الذين يحملون القيمة Sammy في الاسم الأوّل، كما استخدمنا التابع ()all للحصول على قائمة بجميع النتائج الموافقة، أمّا للحصول على النتيجة الأولى فقط (وهي النتيجة الوحيدة في حالتنا)، نستخدم التابع ()first كما يلي: >>> Student.query.filter_by(firstname='Sammy').first() فيكون الخرج بالشّكل: <Student Sammy> يمكننا استخدام التعليمة (filter_by(id=ID للحصول على الطالب من خلال معرّفه على النحو التالي: >>> Student.query.filter_by(id=3).first() أو يمكننا استخدام التابع الأقصر ()get والذي يمكنّنا من جلب عنصر معين باستخدام مفتاحه الأساسي، كما يلي: >>> Student.query.get(3) ستعطي كلا الطريقتين الخرج نفسه: <Student Carl> أصبح من الممكن الآن الخروج من الصدفة: >>> exit() لجلب بيانات طالب من خلال معرّفه، أنشأنا وجهةً جديدةً تُصيّر صفحةً لكل طالب، إذ استخدمنا التابع ()get_or_404 الذي توفرّه الإضافة Flask-SQLAlchemy بديلًا للتابع ()get التقليدي؛ إذ يكمن الفرق بينهما في أنّ التابع ()get يعيد القيمة None في حال عدم وجود نتيجة موافقة للمعرّف المطلوب، في حين يعيد التابع ()get_or_404 خطأ HTTP من النوع 404 Not Found، لذا سنفتح الملف app.py لتعديله، بالشّكل: (env)user@localhost:$ nano app.py ونضيف الوجهة التالية إلى نهاية الملف: # ... @app.route('/<int:student_id>/') def student(student_id): student = Student.query.get_or_404(student_id) return render_template('student.html', student=student) نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة الوجهة /<int:student_id>/، إذ يمثّل المعامل :int محوّلًا لتحويل السلسلة النصية الافتراضية في الرابط إلى نوع عدد صحيح، ويمثل student_id متغير الرابط الذي سيُحدّد الطالب المطلوب عرضه في الصفحة. مررنا المعرّف من الرابط إلى دالة العرض ()student من خلال المعامل student_id، التي نستعلم فيها عن مجموعة الطلاب لنجلب منها الطالب من خلال المعرّف الخاص به باستخدام التابع ()get_or_404، الأمر الذي يؤدي إلى حفظ بيانات الطالب (في حال وجوده) ضمن المتغير student، وإلّا ستُعرض استجابة خطأ HTTP من النوع 404 Not Found في حال عدم وجود طالب موافق للمعرّف المطلوب في قاعدة البيانات، ثمّ صيّرنا أخيرًا قالبًا باسم student.html ممررين إليه بيانات الطالب المُستخرجة. لذا، سنفتح ملف قالب جديد باسم student.html: (env)user@localhost:$ nano templates/student.html ونكتب فيه الشيفرة التالية، والأمر مشابه للقالب index.html باستثناء أنّ هذا الملف سيعرض طالبًا واحدًا كل مرة: {% extends 'base.html' %} {% block content %} <span class="title"> <h1>{% block title %} {{ student.firstname }} {{ student.lastname }}{% endblock %}</h1> </span> <div class="content"> <div class="student"> <p><b>#{{ student.id }}</b></p> <b> <p class="name">{{ student.firstname }} {{ student.lastname }}</p> </b> <p>{{ student.email }}</p> <p>{{ student.age }} years old.</p> <p>Joined: {{ student.created_at }}</p> <div class="bio"> <h4>Bio</h4> <p>{{ student.bio }}</p> </div> </div> </div> {% endblock %} نحفظ الملف ونغلقه. اعتمدنا في الشيفرة السابقة على الوراثة من ملف القالب base.html من خلال تعليمة extends، وجعلنا اسم الطالب الكامل عنوانًا للصفحة، كما عرضنا كلًا من معرّف الطالب واسمه الأول والأخير وبريده الإلكتروني وتاريخ إنشاء السجل وسيرته الذاتية. ننتقل الآن باستخدام متصفح الويب إلى رابط الطالب الثاني كما يلي: http://127.0.0.1:5000/2 فتظهر صفحةٌ مشابهةٌ لما يلي: ومن ثمّ سنحرّر ملف القالب index.html لنربط اسم كل طالب مع الصفحة الخاصة به: (env)user@localhost:$ nano templates/index.html ونعدّل حلقة for التكرارية على النحو التالي: {% for student in students %} <div class="student"> <p><b>#{{ student.id }}</b></p> <b> <p class="name"> <a href="{{ url_for('student', student_id=student.id)}}"> {{ student.firstname }} {{ student.lastname }} </a> </p> </b> <p>{{ student.email }}</p> <p>{{ student.age }} years old.</p> <p>Joined: {{ student.created_at }}</p> <div class="bio"> <h4>Bio</h4> <p>{{ student.bio }}</p> </div> </div> {% endfor %} نحفظ الملف ونغلقه. أضفنا في الشيفرة السابقة وسم الرابط <a> إلى الاسم الكامل للطالب، بغية ربطه مع صفحة الطالب الموافقة باستخدام الدالة ()url_for، ممررين معرّف الطالب ID المُخزّن في الكائن student.id إلى دالة العرض ()student. ننتقل إلى الصفحة الرئيسية للتطبيق ونحدّثها: http://127.0.0.1:5000/ وبذلك نلاحظ أنّ اسم كل طالب أصبح مرتبطًا بصفحة الطالب الموافقة. الآن، وبعد الانتهاء من إنشاء صفحة لكل طالب، سنعمل في الخطوة التالية على إنشاء صفحة لإضافة طلاب جدد إلى قاعدة البيانات. الخطوة 5 – إنشاء سجل جديد سنعمل في هذه الخطوة على إضافة وجهة جديدة للتطبيق بغية إضافة طلاب جدد إلى قاعدة البيانات باستخدام نماذج ويب، إذ سنعمل على تصيير صفحة تتضمّن نموذج ويب يمكّن المستخدمين من إدخال بيانات الطالب، ثم سنوفّر الآليات اللازمة للتعامل مع النموذج المُرسل، وسننشئ كائنًا للطالب الجديد المضاف باستخدام النموذج Student لنضيفه إلى الجلسة، ونهايةً سنحفظ الإجراءات بما يشبه الآلية المُتبعة لدى إضافة مُدخلات الطالب في الخطوة 2 من هذا المقال. سنفتح نافذة طرفية جديدة مع بقاء خادم التطوير قيد التشغيل، وسنفتح منها بدايةً الملف app.py: (env)user@localhost:$ nano app.py وسنضيف في نهايته الوجهة التالية: # ... @app.route('/create/', methods=('GET', 'POST')) def create(): return render_template('create.html') نحفظ الملف ونغلقه. نمرّر في هذه الوجهة متغير صف tuple يحتوي على القيم ('GET', 'POST') إلى المعامل methods بغية السماح بكلا نوعي طلبيات HTTP وهما GET و POST، إذ تتخصّص الطلبيات من النوع GET بجلب البيانات من الخادم، أمّا الطلبيات من النوع POST فهي مُتخصّصة بإرسال البيانات إلى وجهة مُحدّدة، مع ملاحظة أنّ الطلبيات من النوع GET هي الوحيدة المسموحة افتراضيًا، وحالما يطلب المستخدم الوجهة create/ باستخدام طلبية من النوع GET، سيُصيّر ملف قالب باسم "create.html". سنعدّل هذه الوجهة لاحقًا لتتعامل أيضًا مع الطلبيات POST اللازمة لدى ملء المُستخدمين للنماذج وإرسالها بغية إضافة طلاب جدد. الآن، سننشئ ملفًا باسم "create.html" داخل مجلد القوالب "templates" على النحو التالي: (env)user@localhost:$ nano templates/create.html ونكتب ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1 style="width: 100%">{% block title %} Add a New Student {% endblock %}</h1> <form method="post"> <p> <label for="firstname">First Name</label> <input type="text" name="firstname" placeholder="First name"> </input> </p> <p> <label for="lastname">Last Name</label> <input type="text" name="lastname" placeholder="Last name"> </input> </p> <p> <label for="email">Email</label> <input type="email" name="email" placeholder="Student email"> </input> </p> <p> <label for="age">Age</label> <input type="number" name="age" placeholder="Age"> </input> </p> <p> <label for="bio">Bio</label> <br> <textarea name="bio" placeholder="Bio" rows="15" cols="60" ></textarea> </p> <p> <button type="submit">Submit</button> </p> </form> {% endblock %} نحفظ الملف ونغلقه. اعتمدنا في الشيفرة السابقة على الوراثة من ملف القالب base.html من خلال تعليمة extends، كما عيّنا ترويسة لتكون عنوانًا، واستخدمنا الوسم <form> وفيه ضبطنا السمة method التي تُحدّد نوع طلبية HTTP لتكون من النوع post، ما يعني أنّ نموذج الويب هذا سيرسل طلب من النوع "POST". كما أضفنا صندوقي حقل نصي باسم firstname و lastname، إذ سنستخدم هذه الأسماء لاحقًا للوصول إلى بيانات النموذج الذي أرسلها المستخدم في دالة العرض، كما أضفنا حقلًا نصيًا للبريد الالكتروني باسم email، وحقل بيانات عددية لإدخال عمر الطالب، وحقلاً نصياً مُتعدّد الأسطر لإضافة السيرة الذاتية للطالب، وأخيراً أضفنا زرًا لتأكيد النموذج وإرساله Submit إلى نهاية النموذج. الآن، وأثناء عمل خادم التطوير، نستخدم المتصفح للانتقال إلى الوجهة /create: http://127.0.0.1:5000/create فستظهر صفحة إضافة طالب جديد Add a New Student تتضمّن نموذج ويب، وزرًا لتأكيد إرسال هذا النموذج Submit، كما في الشّكل: يرسل نموذج الإدخال هذا طلبًا من النوع POST إلى الخادم، ولكن حتى هذه اللحظة لا يوجد شيفرة مسؤولة عن معالجة هذا الطلب في الوجهة create/، وبالتالي لن يحدث شيء في حال ملء النموذج الآن وإرساله. الآن، سنفتح الملف app.py بهدف تعديله ليتعامل مع الطلبات من النوع POST المُرسلة من قبل المستخدم: (env)user@localhost:$ nano app.py ونعدّل الوجهة create/ لتصبح كما يلي: @app.route('/create/', methods=('GET', 'POST')) def create(): if request.method == 'POST': firstname = request.form['firstname'] lastname = request.form['lastname'] email = request.form['email'] age = int(request.form['age']) bio = request.form['bio'] student = Student(firstname=firstname, lastname=lastname, email=email, age=age, bio=bio) db.session.add(student) db.session.commit() return redirect(url_for('index')) return render_template('create.html') نحفظ الملف ونغلقه. تُعامل الطلبيات من النوع POST ضمن العبارة الشرطية if request.method == 'POST'، إذ يُستخرج كل من اسم الطالب الأوّل والأخير وبريده الإلكتروني وعمره وسيرته الذاتية التي أرسلها المُستخدم، من الكائن request.form، ليُحوّل العمر المُمرّر مثل سلسة نصيّة إلى نوع عدد صحيح باستخدام دالة بايثون()int، ليُبنى بعدها كائن للطالب باسم student باستخدام النموذج Student، ومن ثم نضيف هذا الكائن إلى جلسة قاعدة البيانات، ونحفظ التغييرات. ونهايةً، نعيد توجيه المستخدم إلى الصفحة الرئيسية للتطبيق فيظهر الطالب المُضاف أسفل الطلاب الموجودين أصلًا. الآن وأثناء عمل خادم التطوير، نستخدم المتصفح للانتقال إلى الوجهة /create: http://127.0.0.1:5000/create نملأ النموذج ببعض البيانات ونرسله، فيُعاد التوجيه إلى الصفحة الرئيسية، إذ سنجد الطالب المُضاف للتو، وبذلك أصبحت لدينا الآلية اللازمة لإضافة طلاب جدد، الآن، لا بدّ من إضافة رابط إلى صفحة الإضافة Create في شريط التصفح، لذا نفتح ملف القالب base.html: (env)user@localhost:$ nano templates/base.html سنحرّر جزء الوسم <body> بتعديل قيمة سمة الرابط href لتصبح مساوية لرابط صفحة الإنشاء Create. <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="{{ url_for('create') }}">Create</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% block content %} {% endblock %} </div> </body> نحفظ الملف ونغلقه. وبتحديث الصفحة الرئيسية للتطبيق نلاحظ أنّ الرابط Create في شريط التصفّح بات فعّالًا. ومع نهاية هذه الخطوة أصبح لدينا صفحة تتضمّن نموذج ويب لإضافة طلاب جدد، وللمزيد حول نماذج الويب ننصحك بقراءة المقال كيفية استخدام نماذج الويب في تطبيقات فلاسك، ولمزيدٍ من المعلومات المتقدمة ولتتعرّف على كيفية إدارة نماذج الويب بأمان ننصحك بقراءة المقال استخدام والتحقق من نماذج الويب ذات واجهة المستخدم التفاعلية في فلاسك باستخدام الإضافة Flask-WTF. سنعمل في الخطوة التالية على إضافة صفحة لتعديل بيانات الطلاب الموجودين أصلًا في قاعدة البيانات. الخطوة 6 – تعديل سجل سنعمل في هذه الخطوة على إضافة صفحة جديدة إلى التطبيق بغية تعديل بيانات الطلاب الموجودة أصلًا في قاعدة البيانات. إذ سنضيف وجهة جديدة /ID/edit/ لتعديل بيانات الطلاب بناءً على معرّف ID كل منهم. بدايةً، نفتح الملف app.py: (env)user@localhost:$ nano app.py ثمّ نضيف الوجهة التالية إلى نهاية الملف، والتي ستجلب المُدخل المراد تعديله من بيانات الطالب باستخدام المعرّف الموافق، لتستخرج البيانات الجديدة المُرسلة من نموذج ويب مُخصّص لهذا الغرض سننشئه لاحقًا، لتعدّل بيانات الطالب ومن ثمّ تعيد توجيه المستخدم إلى صفحة التطبيق الرئيسية: # ... @app.route('/<int:student_id>/edit/', methods=('GET', 'POST')) def edit(student_id): student = Student.query.get_or_404(student_id) if request.method == 'POST': firstname = request.form['firstname'] lastname = request.form['lastname'] email = request.form['email'] age = int(request.form['age']) bio = request.form['bio'] student.firstname = firstname student.lastname = lastname student.email = email student.age = age student.bio = bio db.session.add(student) db.session.commit() return redirect(url_for('index')) return render_template('edit.html', student=student) نحفظ الملف ونغلقه. تتعامل الوجهة /int:student_id>/edit>/ مع طلبيات HTTP من النوع POST و GET، إذ تستخدم student_id مثل متغير رابط مسؤول عن تمرير المعرّف ID إلى دالة العرض ()edit. استخدمنا تابع الاستعلام ()get_or_404 على النموذج Student للحصول على الطالب الموافق للمعرّف المطلوب، والذي سيستجيب بخطأ من النوع "404 Not Found" في حال عدم تطابق أي طالب في قاعدة البيانات مع المعرّف المطلوب؛ أمّا في حال وجود طالب موافق للمعرّف المطلوب، فسيُستكمل تنفيذ الشيفرة وصولًا إلى العبارة الشرطية 'if request.method == 'POST، فإذا كانت الطلبية من النوع GET فهذا يعني أنّ المُستخدم لم يملأ نموذج التعديل ويرسله، وبالتالي لن يتحقق الشرط السابق وسنتخطى التعليمات الواردة ضمنه وصولًا إلى السطر البرمجي: return render_template('edit.html', student=student) المسؤول عن تصيير القالب "edit.html" الذي سننشئة لاحقًا، مُمرّرًا إليه كائن الطالب المستخرج من قاعدة البيانات لتُعرض بيانات الطالب الحالية في نموذج الويب. في حال عدّل المستخدم بيانات الطالب في نموذج التعديل وأرسله، فعندها سيتحقق الشرط if request.method == 'POST' وبالتالي ستُنفّذ التعليمات الواردة ضمنه، إذ ستُستخرج بيانات الطالب المُرسلة من الكائن request.form إلى المتغير الموافق لكل منها، لتُضبط كل سمة من سمات كائن الطالب student لتوافق القيم الجديدة المُرسلة من النموذج عبر تعديل قيم الأعمدة بنفس الآلية المُتبعة في الخطوة 2 من هذا المقال؛ وفي حال عدم تعديل أي من حقول نموذج الويب، ستبقى قيم الأعمدة في قاعدة البيانات على حالها. بعد ضبط بيانات الطالب إلى تلك الجديدة المرسلة، سنضيف الكائن student إلى جلسة قاعدة البيانات ونحفظ التغييرات، ونهايةً سنعيد توجيه المستخدم إلى الصفحة الرئيسية للتطبيق. والآن سنعمل على إنشاء صفحة تسمح للمستخدمين بإجراء التعديلات، لذا سننشئ ملف القالب edit.html: user@localhost:$ nano templates/edit.html سيحتوي هذا الملف الجديد على نموذج ويب مشابه لذلك الموجود في ملف القالب create.html، بحيث يُملأ افتراضيًا بالبيانات الحالية للطالب، لذا سنكتب ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1 style="width: 100%"> {% block title %} Edit {{ student.firstname }} {{ student.lastname }}'s Details {% endblock %} </h1> <form method="post"> <p> <label for="firstname">First Name</label> <input type="text" name="firstname" value={{ student.firstname }} placeholder="First name"> </input> </p> <p> <label for="lastname">Last Name</label> <input type="text" name="lastname" value={{ student.lastname }} placeholder="Last name"> </input> </p> <p> <label for="email">Email</label> <input type="email" name="email" value={{ student.email }} placeholder="Student email"> </input> </p> <p> <label for="age">Age</label> <input type="number" name="age" value={{ student.age }} placeholder="Age"> </input> </p> <p> <label for="bio">Bio</label> <br> <textarea name="bio" placeholder="Bio" rows="15" cols="60" >{{ student.bio }}</textarea> </p> <p> <button type="submit">Submit</button> </p> </form> {% endblock %} نحفظ الملف ونغلقه. يضم العنوان كلًا من الاسم الأوّل والأخير للطالب، وتُضبط خاصية القيمة value لكل حقل إدخال وللصندوق النصي المُتعدّد الأسطر المُخصّص للسيرة الذاتية لتساوي القيمة الموافقة من كائن الطالب student المُمرر من الدالة ()edit إلى ملف القالب "edit.html". سننتقل الآن إلى الرابط التالي لنعدّل تفاصيل أوّل طالب: http://127.0.0.1:5000/1/edit فتظهر صفحة مشابه للشكل التالي: نعدّل بيانات الطالب ونرسل النموذج، فيُعاد توجيهنا إلى الصفحة الرئيسية للتطبيق فنلاحظ أنّ معلومات الطالب قد حُدثّت فعلًا، ثم سنضيف زر تعديل Edit أسفل كل طالب في الصفحة الرئيسية لنربط كل منهم بصفحة التعديل الخاصة به، لذا نفتح ملف القالب index.html: (env)user@localhost:$ nano templates/index.html نعدل حلقة for التكرارية في هذا الملف لتبدو تمامًا كما يلي: {% for student in students %} <div class="student"> <p><b>#{{ student.id }}</b></p> <b> <p class="name"> <a href="{{ url_for('student', student_id=student.id)}}"> {{ student.firstname }} {{ student.lastname }} </a> </p> </b> <p>{{ student.email }}</p> <p>{{ student.age }} years old.</p> <p>Joined: {{ student.created_at }}</p> <div class="bio"> <h4>Bio</h4> <p>{{ student.bio }}</p> </div> <a href="{{ url_for('edit', student_id=student.id) }}">Edit</a> </div> {% endfor %} نحفظ الملف ونغلقه. أضفنا في الشيفرة السابقة وسم الرابط <a> للربط مع دالة العرض ()edit ممررين إليها القيمة student.id للربط مع صفحة التعديل لكل طالب باستخدام رابط تعديل صفحة Edit. ومع نهاية هذه الخطوة أصبح لدينا صفحة لتعديل بيانات الطلاب الموجودين في قاعدة البيانات، وسنعمل فيما يلي على إضافة زر حذف Delete لحذف طلاب من قاعدة البيانات. الخطوة 7– حذف سجل سنعمل في هذه الخطوة على إضافة وجهة جديدة وزر حذف Delete يسمح بحذف الطلاب الموجودين أصلًا في قاعدة البيانات. بدايةً، سنضيف وجهةً جديدةً للحذف، وهي /id/delete تتعامل مع الطلبات من النوع POST، إذ ستستقبل دالة الحذف الجديدة delete() رقم معرّف الطالب ID المراد حذفه ممرّرة إياه إلى تابع الاستعلام ()get_or_404 في النموذج Student لمعرفة ما إذا كان موجودًا، للاستجابة بصفحة خطأ من النوع "404 Not Found" في حال عدم وجود طالب موافق للمعرّف المُمرّر في قاعدة البيانات. الآن، افتح الملف app.py: (env)user@localhost:$ nano app.py أضِف الوجهة التالية إلى نهاية الملف: # ... @app.post('/<int:student_id>/delete/') def delete(student_id): student = Student.query.get_or_404(student_id) db.session.delete(student) db.session.commit() return redirect(url_for('index')) نحفظ الملف ونغلقه. استخدمنا المزخرف app.post المُقدّم في الإصدار 2.0.0 من فلاسك بدلًا من استخدام المزخرف app.route المعتاد، لإضافة اختصارات للعديد من توابع HTTP الشائعة، فعلى سبيل المثال الأمر: @app.post("/login") هو اختصار للأمر: @app.route("/login", methods=["POST"]) ما يعني أن دالة العرض هذه تتعامل فقط مع طلبيات من النوع POST، وبزيارة الوجهة ID/delete/ في المتصفّح سيظهر الخطأ "405 Method Not Allowed" لأن المتصفحات تستخدم طريقة GET افتراضيًا للطلبات، لذا بغية حذف طالب ما، سنضيف زر أوامر عندما يضغط المستخدم عليه، تُرسَل إلى الوجهة طلبًا من النوع POST. تستقبل دالة العرض ()delete في الشيفرة السابقة معرّف الطالب المراد حذفه من خلال متغير الرابط student_id، إذ يُستخدم التابع ()get_or_404 لجلب بيانات الطالب وحفظها في متغير باسم student، أو الاستجابة بخطأ من النوع 404 Not Found في حال عدم وجود طالب موافق. كما استخدمنا التابع delete في الجلسة المفتوحة مع قاعدة البيانات في السطر البرمجي (db.session.delete(student ممررين إليه الكائن الممثل للطالب، وهذا ما يجعل الجلسة بالنتيجة تحذف الطالب الموافق بمجرّد تأكيد الإجراءات، إذ ما من حاجة لإجراء أي تعديلات إضافية كون الإجراءات تُؤكَّد مباشرةً باستخدام الأمر ()db.session.commit، ونهايةً أعدنا توجيه المستخدم إلى الصفحة الرئيسية للتطبيق. سنعدّل القالب index.html بإضافة زر لحذف طالب Delete Student: user@localhost:$ nano templates/index.html وسنعدّل حلقة for التكرارية بإضافة وسم نموذج <form> مباشرةً أسفل رابط التعديل Edit: {% for student in students %} <div class="student"> <p><b>#{{ student.id }}</b></p> <b> <p class="name"> <a href="{{ url_for('student', student_id=student.id)}}"> {{ student.firstname }} {{ student.lastname }} </a> </p> </b> <p>{{ student.email }}</p> <p>{{ student.age }} years old.</p> <p>Joined: {{ student.created_at }}</p> <div class="bio"> <h4>Bio</h4> <p>{{ student.bio }}</p> </div> <a href="{{ url_for('edit', student_id=student.id) }}">Edit</a> <hr> <form method="POST" action="{{ url_for('delete', student_id=student.id) }}"> <input type="submit" value="Delete Student" onclick="return confirm('Are you sure you want to delete this entry?')"> </form> </div> {% endfor %} نحفظ الملف ونغلقه. أنشأنا في الملف السابق نموذج ويب يُرسل طلبًا من النوع POST إلى دالة العرض ()delete ممررين القيمة student.id وسيطًا للمعامل student_id لتحديد مُدخل الطالب المراد حذفه، كما استخدمنا التابع confirm() المتوفّر في متصفحات الويب لعرض رسالة تأكيد قبل إرسال الطلب. الآن وبتحديث الصفحة الرئيسية للتطبيق، سيظهر زر أوامر لحذف الطالب Delete Student أسفل كل مُدخل طالب، وبالنقر عليه ستظهر رسالة لتأكيد عملية الحذف، ومن ثمّ سيعيد التطبيق التوجيه إلى صفحته الرئيسية فنلاحظ حذف الطالب فعلًا. وبذلك أصبح لدينا آلية لحذف الطلاب من قاعدة البيانات في تطبيق إدارة الطلاب هذا. الخاتمة عملنا في هذا المقال على بناء تطبيق ويب مُصغّر في فلاسك لإدارة الطلاب باستخدام كل من إطار عمل فلاسك والإضافة Flask-SQLAlchemy مع قاعدة بيانات SQLite، كما تعلمنا كيفية الاتصال مع قاعدة البيانات وإعداد نماذج قاعدة البيانات التي تمثل الجداول، وكيفية إضافة العناصر إلى قاعدة البيانات وإنشاء الاستعلامات ضمن جدول معين، وكيفيّة تعديل البيانات في قاعدة البيانات. مكنّنا استخدام الإضافة SQLAlchemy في التطبيق من استخدام أصناف وكائنات بايثون لإدارة قاعدة بيانات SQL، وكان من الممكن استخدام أي محرّك قواعد بيانات بدلًا من SQLite، وفي هذه الحالة لن نضطر إلى تغيير أي شيء في شيفرة التطبيق الرئيسية باستثناء ضبط SQLALCHEMY_DATABASE_URI المسؤول عن عملية الاتصال، ما يسمح لنا بالانتقال من محرّك قواعد البيانات SQL إلى أي محرّك آخر بأقل تعديلات على الشيفرة البرمجية. ترجمة -وبتصرف- للمقال How to Use Flask-SQLAlchemy to Interact with Databases in a Flask Application لصاحبه Abdelhadi Dyouri. اقرأ أيضًا تجهيز إضافة Flask-SQLAlchemy تجهيز جدولي المقالات والمستخدمين باستعمال إضافة Flask-SQLAlchemy استخدام علاقة many-to-many مع فلاسك Flask ومحرك قواعد البيانات SQLite
-
مع نهاية المقال السابق من هذه السلسلة حول كيفية التعامل مع الملفات والمسارات في بايثون، يجب أن تكون قد بنيت تصورًا حول نظام الملفات وبالتالي كيفية التعامل مع المسارات في بايثون، الأمر الضروري لمساعدتك في إدارة حاسوبك للمضي في إعداد البيئة البرمجية وفهم واجهة سطر الأوامر والتعامل معها. وتعرف عملية إعداد البيئة بأنها مجمل عمليات تنظيم الحاسوب اللازمة لاستخدامه في كتابة الشيفرات البرمجية، الأمر الذي يتضمن تثبيت أي أدوات ضرورية وإعدادها، والتعامل مع أي مشاكل قد تواجهك أثناء عملية الإعداد. هناك أنواع كثيرة من الحواسيب بأنظمة تشغيل مختلفة ذات إصدارات متنوعة، وتحتوي هذه الحواسيب أيضًا على إصدارات مختلفة من مفسر أوامر بايثون، وبالتالي لا يوجد عملية إعداد موحدة للجميع. سنحاول في هذا المقال توصيف بعض المبادئ الأساسية لمساعدتك في إدارة وتنظيم حاسوبك باستخدام واجهة سطر الأوامر في بايثون. سطر الأوامر تعد نافذة سطر الأوامر Command line برنامجًا معتمدًا على النصوص، يسمح بإدخال الأوامر بغية التفاعل مع نظام التشغيل وتنفيذ البرامج، كما تُعرف هذه النافذة أيضًا باسم واجهة سطر الأوامر command line interface CLI أو موجه الأوامر command prompt أو نافذة موجه الأوامر (الوجهة النهائية للصَدَفة) terminal أو الصدفة shell أو وحدة التحكم console. بغض النظر عن التسمية، تؤمن نافذة سطر الأوامر بديلًا عن واجهة المستخدم الرسومية GUI، التي تتيح للمستخدم التفاعل مع الحاسوب بطريقة بصرية تتعدى حدود الواجهة النصية، إذ تعرض واجهة المستخدم الرسومية معلومات مرئية للمستخدم موجهةً إياه خلال تنفيذه للمهام بطريقة أسهل من تلك التي توفرها نافذة سطر الأوامر. يتعامل معظم مستخدمو الحاسوب مع نافذة سطر الأوامر على أنها ميزة متقدمة مخصصة للمحترفين، فلا يستخدمونها مطلقًا، ولعل جزءًا من خوفهم هذا يعود لغياب التلميحات حول كيفية استخدامها، ففي حين أن واجهة المستخدم الرسومية تعرض مثلًا زر أوامر يُظهر أين يجب أن نضغط، فإن الواجهة السوداء لنافذة موجه الأوامر لا تقدم أي تلميحات حول ما يجب كتابته. ومع ذلك، يوجد العديد من الأسباب الوجيهة الدافعة لنصبح بارعين في استخدام نافذة سطر الأوامر، ففي حال أردنا إعداد البيئة، فعادةً ما يُفرض علينا استخدام سطر الأوامر بدلًا من الواجهات الرسومية، ناهيك عن أن إدخال الأوامر قد يكون أسرع من الضغط على نوافذ رسومية باستخدام الفأرة. كما أن الأوامر النصية قد تكون أوضح من التعامل مع أيقونات متعددة متداخلة، كما تعد الأوامر النصية ذات طابع آلي مؤتمت إذ من الممكن دمج عدة أوامر محددة في نصوص برمجية لتنفيذ عمليات معقدة. ويوجد برنامج سطر الأوامر ضمن ملف تنفيذي في الحاسوب، ولهذا السبب عادةً ما ندعوه بالصدفة أو برنامج الصدفة، فبتشغيل برنامج الصدفة تظهر الوجهة النهائية المطلوبة من داخلها terminal وهي نافذة موجه الأوامر: يقع برنامج الصدفة في نظام تشغيل ويندوز ضمن المسار: C:\Windows\System32\cmd.exe يقع برنامج الصدفة في نظام تشغيل ماك أو إس ضمن المسار /bin/bash. يقع برنامج الصدفة في نظام تشغيل أوبنتو لينكس ضمن المسار /bin/bash. ابتكر المبرمجون على مدى الأعوام العديد من برامج الصَدَفة لنظام التشغيل لينكس مثل Bourne Shell (الذي يقع ضمن ملف تنفيذي يدعى sh)، ومن ثم برنامجًا يدعى Bourne Again Shell (الذي يقع ضمن ملف تنفيذي يدعى Bash). في حين يستخدم نظام التشغيل لينكس برنامج باش Bash افتراضيًا، أما نظام ماك أو إس فيستخدم شبيه باش وهو برنامج Zsh، أو Z shell في الإصدار كاتالينا والإصدارات الأحدث. أما بالنسبة لنظام ويندوز، ونظرًا لتاريخ تطوره الفريد، فيستخدم صَدَفَة تدعى بموجه الأوامر. وبالنتيجة تؤدي جميع هذه البرامج الوظيفة ذاتها، فهي تعرض واجهة سطر أوامر نصية، يدخل المستخدمون فيها الأوامر ويستخدموها في تشغيل البرامج. في هذا المقال ستتعرف على بعض المفاهيم الأساسية في سطر الأوامر، إضافةً إلى بعض الأوامر الشائعة، إذ يمكنك إتقان عدد كبير من الأوامر المشفرة لتتعامل مع الحاسوب بما يشبه السحر، إلا أن معرفة ربما نحو العشرات منها سيكون كافٍ لحل معظم المشكلات، وقد تختلف قليلًا الأسماء الدقيقة للأوامر باختلاف أنظمة التشغيل، إلا أن المفهوم والغاية منها تبقى واحدة. فتح نافذة موجه الأوامر لفتح نافذة موجه الأوامر نقوم بما يلي: في نظام التشغيل ويندوز، نضغط على زر ابدأ ونكتب Command Prompt أو cmd ونضغط زر ENTER. في نظام التشغيل ماك أو إس، نضغط على أيقونة Spotlight في الزاوية العلوية اليمنى ونكتب Terminal ثم نضغط زر ENTER. في نظام التشغيل أوبنتو لينكس، نضغط زر WIN لتظهر لنا نافذة Dash، وفيها نكتب Terminal ونضغط زر ENTER، أو مباشرةً من خلال لوحة المفاتيح بالضغط على الاختصار CTRL+ALT+T. وبشكل مشابه للصدفة التفاعلية التي تعرض نافذة سطر أوامر ذات رمز انتظار الأوامر <<<، فإن نافذة موجه الأوامر terminal تعرض نافذة سطر أوامر خاصة بالصَدَفَة shell prompt حيث يمكنك كتابة الأوامر. في نظام ويندوز، سيكون رمز انتظار الأوامر عبارة عن المسار الكامل للمجلد الحالي، بالشكل: C:\Users\Al>هنا نكتب الأوامر أما في نظام التشغيل ماك أو إس، فيظهر موجه الأوامر مع رمز انتظار أوامر مكون من اسم الحاسوب متبوعًا بعلامة النقطتين الرأسيتين ثم اسم دليل العمل الحالي، مع التعبير عن المجلد الرئيسي بعلامة (~) يليها اسم المستخدم متبوعًا بإشارة ($)، بالشكل: Als-MacBook-Pro:~ al$ هنا نكتب الأوامر ويكون رمز انتظار التعليمات في موجه الأوامر الخاص بنظام التشغيل أبونتو لينكس مشابهًا لذلك الموجود في نظام ماك أو إس، باستثناء أن الأول يبدأ باسم المستخدم مع الرمز (@)، بالشكل: al@al-VirtualBox:~$ هنا نكتب الأوامر ومن الجدير بالذكر أنه في الكثير من الكتب والمقالات يُمثل رمز انتظار الأوامر كإشارة $ فقط، وذلك بغية تبسيط شكل الأمثلة الواردة فيها، كما من الممكن تخصيص رمز انتظار التعليمات في نافذة موجه الأوامر، إلا أن هذا الأمر خارج مجال اهتمامنا حاليًا. تشغيل البرامج باستخدام نافذة سطر الأوامر لتشغيل برنامج أو أمر ما، يكفي أن نُدخل اسمه في نافذة سطر الأوامر ببساطة، فعلى سبيل المثال لتشغيل برنامج الآلة الحاسبة الافتراضي الذي يأتي مرفقًا مع نظام التشغيل، نكتب الأوامر التالية في سطر الأوامر حسب نظام التشغيل المستخدم: في نظام التشغيل ويندوز نكتب calc.exe. في نظام التشغيل ماك أو إس نكتب open -a Calculator (يُشغّل هذا الأمر في البداية برنامج فتح الملفات والمجلدات open الذي بدوره يشغل برنامج الآلة الحاسبة). في نظام التشغيل لينكس نكتب gnome-calculator. ومن الجدير بالذكر أن أسماء البرامج والأوامر حساسة لحالة الأحرف فقط في نظام التشغيل لينكس، ما يعني أنه يجب كتابة gnome-calculator حرفيًا في نظام التشغيل لينكس، لكن يمكنك كتابة Calc.exe في نظام التشغيل ويندوز أو OPEN -a Calculator في نظام التشغيل ماك أو إس ولن يؤثر ذلك على النتيجة. إن إدخال أسماء برنامج الآلة الحاسبة إلى نوافذ سطر الأوامر مشابه تمامًا لتشغيلها من قائمة ابدأ أو Finder أو Dash، إذ تعمل أسماء برنامج الآلة الحاسبة هذه كأوامر، لأن كل من البرامج calc.exe و open و gnom-calculator موجودة في مجلدات مضمنة في متغيرات البيئة الخاصة بالمسارات PATH. لكن يكفي أن نقول أنه عندما ندخل اسم برنامج إلى نافذة سطر الأوامر، تتحقق الصدفة من وجود هذا الاسم في أحد المجلدات المضمنة في متغير البيئة PATH، إذ تبحث الصدفة في نظام التشغيل ويندوز عن البرنامج ضمن دليل العمل الحالي (الظاهر في موجه الأوامر) قبل البحث في مجلدات المتغير PATH، ولتحقيق الأمر ذاته في نظامي التشغيل ماك أو إس ولينكس لا بد أن نكتب /. قبل اسم الملف. إن لم يكن البرنامج موجودًا في مجلد ضمن المتغير PATH، فعندها نحن أمام خياران: إما أن نستخدم الأمر cd لتغيير دليل العمل الحالي ليصبح هو المجلد الذي يحتوي على ذلك البرنامج، ثم ندخل اسمه، فيمكننا على سبيل المثال إدخال الأمرين التاليين: cd C:\Windows\System32 calc.exe أو أن نُدخل المسار الكامل للملف التنفيذي للبرنامج، فعلى سبيل المثال يمكن أن نُدخل الأمر C:\Windows\System32\calc.exe بدلًا من إدخال calc.exe. إن كان البرنامج في نظام التشغيل ويندوز ينتهي باللاحقة exe. أو bat. فإن كتابة اللاحقة ضمن الأمر هو شيء اختياري، إذ أن كتابة calc وحدها تؤدي نفس النتيجة لدى كتابة calc.exe، في حين لا تمتلك الملفات التنفيذية في نظامي التشغيل ماك أو إس ولينكس على لاحقة تميزها إلا أنها تمتلك مجموعة السماحيات التنفيذية اللازمة. استخدام وسطاء نافذة سطر الأوامر وسطاء نافذة سطر الأوامر هي عبارة عن أجزاء نصية توضع بعد اسم الأمر، وهي شبيهة بتلك التي نمررها إلى دوال بايثون من حيث أنها تزود الأمر بخيارات محددة أو وجهات إضافية، فعند تنفيذ الأمر cd C:\Users مثلًا، فإن الجزء C:\Users عبارة عن وسيط للأمر cd يبين له المجلد المطلوب تغيير دليل العمل الحالي إليه، أو مثلًا لدى تشغيل شيفرة بايثون من نافذة موجه الأوامر باستخدام الأمر python yourScript.py، فإن الجزء yourScript.py يعد وسيطًا، يبين لبرنامج بايثون الملف المطلوب البحث فيه عن التعليمات المراد تنفيذها. إن خيارات نافذة سطر الأوامر (والمسماة أيضًا الأعلام flags أو رموز الانتقال switches أو ببساطة الخيارات) هي عبارة عن وسطاء، تتكون عادةً من حرف واحد أو كلمة قصيرة. تبدأ خيارات سطر الأوامر في نظام التشغيل ويندوز عادة بالخط المائل الأمامي (/)، بينما في كل من نظامي التشغيل ماك أو إس ولينكس فتبدأ بشرطة علوية مفردة (-) أو مزدوجة (--). وكما تلاحظ فإننا قد استخدمنا الخيار a- لتشغيل الأمر open -a Calculator في نظام التشغيل ماك أو إس، ومن الجدير بالذكر أن خيارات نافذة سطر الأوامر في كلا نظامي التشغيل ماك أو إس ولينكس حساسة لحالة الأحرف، على خلاف نظام التشغيل ويندوز، ومن الممكن الفصل ما بين عدة خيارات في نافذة سطر الأوامر باستخدام المسافات spaces. تعد أسماء الملفات والمجلدات أيضًا وسطاء شائعة في واجهة سطر الأوامر، وفي حال كون اسم المجلد أو الملف متضمنًا لمسافات، فعندها يجب حصره ضمن علامة اقتباس مزدوجة لتجنب حدوث أخطاء في نافذة سطر الأوامر، على سبيل المثال إن أردنا تغيير دليل العمل إلى مجلد يدعى Vacation Photos من خلال إدخال الأمر cd Vacation Photos، سيظن برنامج سطر الأوامر بأننا قد أدخلنا وسيطين هما Vacation و Photos؛ وعلى هذا الأساس بالذات سوف ندخل الأمر على النحو الموالي: cd "Vacation Photos" C:\Users\Al>cd "Vacation Photos" C:\Users\Al\Vacation Photos> كما يعد الوسيط help-- شائعًا جدًا للعديد من الأوامر في نظامي ماك أو إس ولينكس، يقابله الوسيط في نظام ويندوز، ويعد هذا الوسيط مسؤولًا عن توفير المعلومات المتعلقة بالأمر المحدد، فمثلًا في حال تشغيل الأمر ?/ في نظام ويندوز، فستعرض الصَدَفَة معلومات حول الأمر cd متضمنةً وظيفته مع قائمة بكافة الوسطاء التي تعمل معه، بالشكل: C:\Users\Al>cd /? Displays the name of or changes the current directory. CHDIR [/D] [drive:][path] CHDIR [..] CD [/D] [drive:][path] CD [..] .. Specifies that you want to change to the parent directory. Type CD drive: to display the current directory in the specified drive. Type CD without parameters to display the current drive and directory. Use the /D switch to change current drive in addition to changing current directory for a drive. --snip-- تشير معلومات المساعدة هذه إلى أن الأمر cd الخاص بنظام ويندوز يدعى أيضًا chdir. (لا يستخدم معظم الأشخاص الأمر chdir بما أن الأمر cd مختصر ويؤدي نفس الغرض تمامًا)، وتتضمن الأقواس المتوسطة على الوسطاء الإضافية الاختيارية، فعلى سبيل المثال يشير CD [/D] [drive:][path] إلى أنه يمكننا تحديد محرك أقراص أو مسار باستخدام الخيار D/. ولكن ورغم أن الوسطاء ?/ و help-- توفر معلومات غنية عن الأوامر مقدمةً تذكيرات ببعض الوسطاء المهمة التي قد تغيب عن ذهن حتى المستخدمين المحترفين، إلا أن شروحاتها قد تكون غامضة نسبيًا ما يجعلها غير مفيدة للمستخدمين المبتدئين، وهنا يكون الحل البديل لأفضل هو الاستعانة بالمراجع من كتب أو مقالات على الإنترنت. تشغيل شفيرة بايثون من نافذة سطر الأوامر باستخدام الأمر c- إن أردت تشغيل جزء صغير من شيفرة بايثون لمرة واحدة، فبإمكانك تمرير رمز الانتقال c- إلى البادئة python.exe في حال استخدامك لنظام ويندوز، أو python3 لأنظمة ماك أو إس ولينكس، ثم نكتب الشيفرة المراد تشغيلها بعد c- مضمنةً في علامة اقتباس مزدوجة، فمثلًا بكتابة التالي ضمن نافذة سطر الأوامر: C:\Users\Al>python -c "print('Hello, world')" Hello, world إن رمز الانتقال c- مفيد لرؤية ناتج تنفيذ تعليمة بايثون واحدة دون الحاجة لفتح الصَدَفَة التفاعلية، فمثلًا من الممكن عرض خرج الدالة ()help بسرعة والعودة بعدها مباشرةً للمتابعة في سطر الأوامر، بالشكل: C:\Users\Al>python -c "help(len)" Help on built-in function len in module builtins: len(obj, /) Return the number of items in a container. C:\Users\Al> تشغيل برامج بايثون من سطر الأوامر إن برامج بايثون عبارة عن ملفات نصية ذات اللاحقة (py.)، وهي ليست ملفات تنفيذية، بل إن مفسرات لغة بايثون تقرأها وتنفذ تعليمات بايثون الموجودة ضمنها. في حين أن للمفسر ملف تنفيذي وهو python.exe في نظام ويندوز، و python3 في أنظمة ماك أو إس ولينكس (كما أن ملف بايثون الأساسي يتضمن مفسر الإصدار الثاني من بايثون). وبتشغيل الأوامر python yourScript.py أو python3 yourScript.py سيتم تشغيل تعليمات بايثون المحفوظة في الملف المسمى yourScript.py. تشغيل البرنامج py.exe يثبت بايثون البرنامج py.exe ضمن المجلد C:\Windows وذلك في نظام التشغيل ويندوز، وهو متطابق مع البرنامج python.exe إلا أن الأخير يتميز بامتلاكه وسيطًا إضافيًا يجعله قادرًا على تشغيل أي من إصدارات بايثون المثبة على حاسوبك، ويمكنك تشغيل الأمر py من أي من المجلدين وذلك لكون المجلد C:\Windows مضمنًا في متغير البيئة PATH. وفي حال وجود عدة إصدارات من بايثون مثبتة على نفس الجهاز، ولدى تشغيل الأمر py فسيتم تلقائيًا تشغيل الإصدار الأحدث، أما لتشغيل إصدار بحد ذاته، فمن الممكن استخدام وسيد مثل 2- لتشغيل الإصدار الثاني أو 3- لتشغيل الإصدار الثالث، كما من الممكن تحديد الإصدار بطريقة أكثر تفصيلًا باستخدام وسيط مثل 3.6- أو 2.7- لتشغيل نسخة بحد ذاتها من بايثون. وبعد تحديد الإصدار من الممكن تمرير وسطاء سطر الأوامر بنفس الطريقة سواءً باستخدام البرنامج py.exe أو python.exe. لنشغل الآن الأوامر التالية باستخدام نافذة سطر الأوامر في ويندوز: C:\Users\Al>py -3.6 -c "import sys;print(sys.version)" 3.6.6 (v3.6.6:4cf1f54eb7, Jun 27 2018, 03:37:03) [MSC v.1900 64 bit (AMD64)] C:\Users\Al>py -2.7 Python 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:25:58) [MSC v.1500 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> وبذلك نجد أن أحد مزايا استخدام البرنامج py.exe هي إمكانية تحديد إصدار محدد من بايثون لتشغيله في حال وجود عدة إصدارات مثبتة على الحاسوب. تشغيل الأوامر من برنامج بايثون إن دالة بايثون ()subprocess.run الموجودة في الوحدة subprocess قادرة على تشغيل أوامر الصدفة ضمن برامج بايثون، لتعرض خرج هذه الأوامر كسلاسل نصية، فعلى سبيل المثال تشغل الشيفرة التالية الأمر ls -al: >>> import subprocess, locale 1 >>> procObj = subprocess.run(['ls', '-al'], stdout=subprocess.PIPE) 2 >>> outputStr = procObj.stdout.decode(locale.getdefaultlocale()[1]) >>> print(outputStr) total 8 drwxr-xr-x 2 al al 4096 Aug 6 21:37 . drwxr-xr-x 17 al al 4096 Aug 6 21:37 .. -rw-r--r-- 1 al al 0 Aug 5 15:59 spam.py مررنا في الشيفرة السابقة القائمة ['ls', '-al'] إلى الدالة ()subprocess.run (السطر المشار له بالرقم 1)، حيث تحتوي هذه القائمة على الأمر ls متبوعًا بوسطائه، كل منها كسلسلة مستقلة، لأن تمرير السلسلة بالشكل ['ls, -al'] لن يعمل. ثم خزّنا خرج الأمر كسلسلة نصية ضمن المتغير outputStr (السطر المشار له بالرقم 2). وللمزيد من المعلومات حول الدالتين ()subprocess.run و ()locale.getdefaultlocale القادرتين على جعل شيفرة بايثون تعمل على أي نظام تشغيل، ننصحك بالاطلاع على التوثيقات والمقالات المتوفرة على الإنترنت حولهما. تخفيف عبء الكتابة اليدوية باستخدام الإكمال التلقائي نظرًا لكون المستخدمين المحترفين يمضون ساعاتٍ في كتابة الأوامر يوميًا، فأصبحت نوافذ سطر الأوامر الحديثة توفر ميزاتٍ من شأنها تقليل كم الكتابة اللازم، إذ تسمح ميزة الإكمال التلقائي باستخدام المفتاح Tab من لوحة المفاتيح للمستخدم بكتابة المحارف الأولى فقط من اسم الملف أو المجلد ومن ثم الضغط على مفتاح Tab لتكمل الصَدَفَة الكتابة تلقائيًا. فعلى سبيل المثال عندما نكتب cd c:\u ونضغط على مفتاح Tab في نظام التشغيل ويندوز، فإن هذا الأمر يتحقق من الملفات والمجلدات في المسار \:C التي تبدأ بالحرف u، مكملًا المسار ليصبح c:\Users، كما يصحح حالة الحرف الصغير u ليصبح حرفًا كبيرًا U. (في نظامي التشغيل ماك أو إس ولينكس لا يقوم الاكمال باستخدام زر Tab بتصحيح حالة الحرف من كبير إلى صغير أو بالعكس). وفي حال وجود عدة ملفات أو مجلدات تبدأ بالحرف U ضمن المجلد \:C، فعندها من الممكن مواصلة الضغط على مفتاح Tab للتنقل فيما بينها وصولًا إلى المطلوب، ولتقليل عدد الخيارات، يمكننا كتابة العبارة cd c:\us، وبالتالي تقل الاحتمالات لتضم فقط الملفات أو المجلدات التي تبدأ بالحرفين us. والضغط المتكرر هذا على مفتاح Tab يعمل أيضًا في نظامي التشغيل ماك أو إس ولينكس، وفي المثال التالي كتب المستخدم العبارة cd D ثم ضغط مرتين على المفتاح Tab بالشكل: al@al-VirtualBox:~$ cd D Desktop/ Documents/ Downloads/ al@al-VirtualBox:~$ cd D بالضغط مرتين على مفتاح Tab بعد كتابة الحرف D، ستعرض الصدفة جميع الاحتمالات الممكنة، ومن ثم تعيد كتابة الأمر كما كتبته أنت أصلًا بانتظار قرارك، وهنا يمكنك مثلًا كتابة الحرف e ومن ثم الضغط على المفتاح Tab فتكمل الصدفة الأمر ليصبح /cd Desktop. يلعب الإكمال التلقائي باستخدام المفتاح TAB دورًا مهمًا ذو فائدة كبيرة، لدرجة أن معظم بيئات التطوير المتكاملة ذات واجهات المستخدم الرسومية ومحررات النصوص قد ضمنت هذه الميزة، ولكن بآلية مختلفة عنها في نافذة سطر الأوامر، إذ تعرض هذه البرامج ذات واجهات المستخدم الرسومية قائمة صغيرة من الكلمات أسفل الأحرف التي نبدأ بكتابتها، ما يسمح لنا باختيار واحدة منها لإكمال الأمر. عرض الأوامر المخزنة: تاريخ الأوامر تتذكر برامج الصَدفات الحديثة من خلال ما يسمى تاريخ الأوامر كافة الأوامر التي قد سبق وأدخلتها، وبذلك وبمجرد الضغط على مفتاح السهم الصاعد في لوحة المفاتيح، يملأ سطر الأوامر بآخر أمر قد استخدمته، وبالنقر المتكرر على هذا المفتاح يمكنك العودة إلى الأوامر الأقدم، كما يمكنك الضغط على مفتاح السهم الهابط للعودة إلى الأوامر الأحدث، وفي أي لحظة ترغب بها في إلغاء الأمر الحالي في موجه الأوامر والبدء من الصفر، كل ما عليك فعله هو الضغط على مفتاحي CTRL+C. من الممكن عرض تاريخ الأوامر في نظام التشغيل ويندوز بإدخال الأمر doskey /history (إن هذا الاسم الغريب doskey يعود إلى حقبة نظام التشغيل DOS الذي كان يسبق ويندوز)، أما في نظامي ماك أو إس ولينكس، فيمكن عرض تاريخ الأوامر باستخدام الأمر history. الخلاصة يتضمن ضبط البيئة كافة الخطوات الضرورية لجعل الحاسوب قادرًا على تشغيل برامجنا بسهولة، وتتطلب هذه العملية منا معرفة بعض مفاهيم الأولية حول كيفية عمل الحاسوب، كفهم نافذة سطر الأوامر ومتغيرات البيئة. يمتلك سطر الأوامر العديد من الأسماء حسب نظام التشغيل، مثل نافذة موجه الأوامر والصدفة ووحدة التحكم، إلا أنها تشير جميعها إلى الشيء ذاته: برنامج يعتمد على النصوص يتيح لنا إدخال الأوامر. فرغم اختلاف موجه الأوامر وأسماء الأوامر الشائعة باختلاف أنظمة التشغيل، إلا أنها تؤدي بالنتيجة المهام ذاتها. قد يستغرق الأمر وقتًا لتعتاد التعامل مع موجه الأوامر، نظرًا لكثرة الأوامر ووسطاء كل منها، لذا لا تتردد في البحث مطولًا على الإنترنت عن المساعدة، فهو الأمر الطبيعي الذي يقوم به مطورو البرامج يوميًا. ترجمة -وبتصرف- للفصل الثاني "إعداد البيئة وواجهة سطر الأوامر" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: التعامل مع الملفات والمسارت في بايثون تثبيت بايثون 3 وإعداد بيئتها البرمجية أصول طلب المساعدات البرمجية في بايثون عبر الإنترنت
-
نحتاج عادةً إلى قاعدة بيانات في تطبيقات الويب، وهي مجموعةٌ مُنطمّةٌ من البيانات نستخدمها لتخزين وتنظيم البيانات الدائمة، موفّرةً لنا إمكانية استرجاع هذه البيانات ومعالجتها بفعالية، إذ نحتاج مثلًا في تطبيق ما للتواصل الاجتماعي إلى قاعدة بيانات لتخزين بيانات المستخدم (من معلومات شخصية ومنشورات وتعليقات ومتابعين) لنتمكّن من معالجتها بفعالية، إذ توفّر قاعدة البيانات إمكانية إضافة بيانات جديدة إليها، أو استرجاع بيانات مُخزّنة أصلًا، أو التعديل عليها، أو حذفها بكل مرونة وذلك اعتمادًا على المتطلبات والشروط والظروف المُختلفة، ففي تطبيق ويب ما قد تكون هذه المتطلبات مثلًا هي حالة إضافة المُستخدم لمنشور جديد، أو حذف منشور سابق، أو حذف حسابه مع كافّة منشوراته أو مع الإبقاء عليها، بمعنى أنّ طريقة معالجة البيانات تعتمد أولًا وأخيرًا على الميزات التي يتيحها التطبيق، فمثلًا قد لا ترغب بالسماح لمُستخدمي تطبيقك بإضافة منشورات غير معنونة. يُعد فلاسك إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا عدد أدوات وميزات من شأنها إنشاء تطبيقات ويب في لغة بايثون. أمّا MongoDB، فهو برنامج قواعد بيانات عام الأغراض ومستندي التوجّه document-oreiented من نوع NoSQL، الذي يستخدم المستندات المشابهة لصيغة JSON لتخزين البيانات. تتيح قواعد البيانات المُعتمدة على المستندات المشابهة لصيغة JSON تخطيطًا ديناميكيًا مرنًا لقاعدة البيانات مع الحفاظ على بساطتها على عكس العلاقات المتعددة بين الجداول المُستخدمة في قواعد البيانات العلّاقية، فعمومًا تتميز قواعد البيانات من النوع NoSQL بالتوسّع الأفقي، ما يجعلها مناسبةً للبيانات الضخمة وتطبيقات الوقت الحقيقي. سنعمل في هذا المقال على بناء تطبيق ويب مصغر لإنشاء قوائم المهام ومن خلاله سنوضّح كيفية استخدام المكتبة PyMongo، وهي محرك قواعد بيانات يتيح إمكانية التخاطب مع قاعدة بيانات MongoDB باستخدام لغة بايثون، وسنستخدمها ضمن فلاسك Flask لتأدية مهام التطبيق الأساسية، مثل الاتصال بخادم قاعدة البيانات وإنشاء التجميعات Collections التي من شأنها تخزين مجموعة من المستندات في قاعدة البيانات MongoDB وإدخال البيانات ضمن تجميعة ما، إضافةً إلى جلب وحذف البيانات من التجميعات. مستلزمات العمل قبل المتابعة في هذا المقال لا بُدّ من: توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو flask_app. توفّر محرّك قواعد بيانات MongoDB مثبّتٍ على حاسوبك، وفي هذا الصدد ننصحك بقراءة المقال كيفية تثبيت وتأمين MongoDB على أوبونتو 18.04 للمزيد حول كيفية إعداد قاعدة بيانات MongoDB. الفهم الجيد لأساسيات فلاسك، مثل مفهوم الوجهات ودوال العرض والقوالب، وفي هذا الصدد يمكنك الاطلاع على المقالين كيفية بناء موقعك الإلكتروني الأول باستخدام إطار عمل فلاسك Flask من لغة بايثون وكيفية استخدام القوالب في تطبيقات فلاسك Flask لفهم مبادئ فلاسك. فهم أساسيات لغة HTML. الخطوة 1 - إعداد مكتبة PyMongo وإطار فلاسك سنعمل في هذه الخطوة على تثبيت كل من فلاسك والمكتبة PyMongo. لذا، وبعد التأكّد من تفعيل البيئة الافتراضية، نستخدم أمر تثبيت الحزم pip لتثبيت كل من فلاسك ومكتبة PyMongo على النحو التالي: (env)user@localhost:$ pip install Flask pymongo وبمجرّد انتهاء التثبيت بنجاح، سيظهر في نهاية الخرج سطر شبيه بما يلي مؤكدًا نجاح العملية: Successfully installed Flask-2.0.2 Jinja2-3.0.3 MarkupSafe-2.0.1 Werkzeug-2.0.2 click-8.0.3 itsdangerous-2.0.1 pymongo-4.0.1 وبذلك نكون قد ثبتنا حزم بايثون اللازمة، وفيما يلي سنعمل على الاتصال بخادم قاعدة البيانات MongoDB وإنشاء تجميعة. الخطوة 2 - الاتصال بخادم MongoDB وإنشاء تجميعة سنستخدم في هذه الخطوة مكتبة PyMongo لإنشاء عميل سنستخدمه في التخاطب مع خادم MongoDB، وإنشاء قاعدة بيانات، ومن ثم إنشاء تجميعة لتخزين المهام ضمنها. الآن وبعد التأكّد من تفعيل البيئة الافتراضية، ننشئ ملفًا جديدًا باسم app.py لتحريره ضمن المجلد flask_app على النحو التالي: (env)user@localhost:$ nano app.py سيستورد هذا الملف الصنف والمساعدات الضرورية من حزمة فلاسك ومن مكتبة PyMongo، إذ سنتخاطب مع خادم MongoDB بغية إنشاء قاعدة بيانات وتجميعة لتخزين المهام، لذلك سنكتب الشيفرات التالية ضمن الملف app.py: from flask import Flask from pymongo import MongoClient app = Flask(__name__) client = MongoClient('localhost', 27017) db = client.flask_db todos = db.todos نحفظ الملف ونغلقه. استوردنا في الشيفرة السابقة الصنف Flask الذي سنستخدمه في إنشاء نسخة فعلية من التطبيق باسم app، كما استوردنا الوحدة MongoClient التي سنستخدمها لإنشاء كائن عميل باسم client لنسخة قاعدة البيانات MongoDB، والذي يسمح لنا بالاتصال والتخاطب مع خادم MongoDB. عند استنساخ الدالة ()MongoClient فإننا نمرّر لها كل من اسم مضيف خادم MongoDB وهو في حالتنا الحاسوب نفسه localhost، ورقم المنفذ وهو هنا 27017. ملاحظة: من الضروري اتباع إجراءات أمان صارمة لدى تثبيت MongoDB وذلك باتباع الخطوات الواردة في المقال كيفية تثبيت وتأمين MongoDB على أوبونتو 18.04، وبمجرد تأمينها يمكنك البدء بضبط إعداداتها للتعامل مع الاتصالات عن بعد. بمجرّد تمكين خاصية الاستيثاق في MongoDB، سيتوجّب علينا لدى إنشاء نسخة من دالة ()MongoClient بتمرير معاملين إضافيين لاسم المستخدم وكلمة المرور، وهما: username و password على النحو التالي: client = MongoClient('localhost', 27017, username='username', password='password') ثمّ استخدمنا في الشيفرة نسخة كائن العميل client لإنشاء قاعدة بيانات MongoDB باسم flask_db، وحفظ مرجع لها ضمن متغير باسم db. بعدها أنشأنا تجميعة باسم todos ضمن قاعدة البيانات flask_db باستخدام المتغير db، إذ تُعد التجميعات مسؤولة عن تخزين مجموعة من المستندات في قاعدة البيانات MongoDB على غرار مبدأ الجداول في قواعد البيانات العلاقية. نلاحظ إنشاء كل من قواعد البيانات والتجميعات في MongoDB اعتماديًا، بمعنى أنّه حتى لو جرى تنفيذ الملف app.py، فلن تُنفّذ أي من الشيفرات الخاصة بقاعدة البيانات فعليًا إلى أن يُنشَأ المستند الأول. سننشئ في الخطوة التالية تطبيق فلاسك مُصغّر يحتوي على صفحة تمكّن المستخدمين من إدخال مستندات مهامهم ضمن التجميعة المسماة todos، حيث سيُنشأ ملف قاعدة البيانات flask_db والتجميعة todos ضمن خادم MongoDB بمجرّد إضافة أوّل مستند مهام. وللحصول على قائمة بقواعد البيانات الموجودة حاليًا، نفتح نافذة طرفية terminal جديدة ونشغّل صدفة mongo باستخدام الأمر التالي: (env)user@localhost:$ mongo فتظهر نافذة موجه أوامر، سنستخدم فيها الأمر التالي للتحقّق من قواعد البيانات الموجودة: > show dbs وفي حال كون MongoDB مُثبّتة حديثًا فلن يظهر ضمن القائمة في الخرج سوى قواعد البيانات admin و config و local، كما نلاحظ عدم ظهور قاعدة البيانات flask_db في هذه المرحلة. أمّا الآن فسنترك صدفة mongo قيد التشغيل ضمن نافذة الطرفية وننتقل إلى الخطوة التالية. الخطوة 3 - إنشاء صفحة ويب لإضافة واستعراض المهام سنعمل في هذه الخطوة على إنشاء صفحة ويب تسمح للمستخدمين بإضافة المهام واستعراضها ضمن نفس الصفحة. لذا، سننشئ ملفًا باسم app.py لتحريره وذلك أثناء كون البيئة البرمجية مُفعّلة، كما يلي: (env)user@localhost:$ nano app.py وفيه سنضيف بدايةً الاستدعاءات التالية من حزمة فلاسك: from flask import Flask, render_template, request, url_for, redirect from pymongo import MongoClient # ... استدعينا في الشيفرة السابقة الدالة المساعدة ()render_template والتي سنستخدمها في تصيير قالب HTML، كما استدعينا الكائن request لاستخدامه في الوصول إلى البيانات التي سيرسلها المستخدم عبر التطبيق؛ أما الدالة ()url_for فستنشئ عناوين URLs، وستعيد الدالة ()redirect توجيه المستخدم إلى صفحة التطبيق الرئيسية بعد إضافته مهمّةً جديدة. والآن سنضيف الوجهة التالية إلى نهاية الملف app.py، على النحو التالي: # ... @app.route('/', methods=('GET', 'POST')) def index(): return render_template('index.html') نحفظ الملف ونغلقه. نمرّر في هذه الوجهة متغير صف tuple يحتوي على القيم ('GET', 'POST') إلى المعامل methods بغية السماح بكلا نوعي طلبيات HTTP وهما GET و POST؛ إذ تتخصّص الطلبيات من النوع GET بجلب البيانات من الخادم؛ أمّا الطلبيات من النوع POST فهي مُتخصّصة بإرسال البيانات إلى وجهة مُحدّدة، مع ملاحظة أنّ الطلبيات من النوع GET هي الوحيدة المسموحة افتراضيًا، وحالما يطلب المستخدم الوجهة / باستخدام طلبية من النوع GET، سيُصيّر ملف قالب باسم index.html. سنعدّل هذه الوجهة لاحقًا لتتعامل أيضًا مع الطلبيات من نوع POST اللازمة لدى ملء المُستخدمين نموذج الويب وإرساله بغية إنشاء مهام جديدة، ثمّ سننشئ مجلدًا للقوالب ضمن المجلد flask_app، وسنفتح ضمنه ملف القالب index.html كما يلي: (env)user@localhost:$ mkdir templates (env)user@localhost:$ nano templates/index.html وسنكتب الشيفرة التالية ضمن ملف القالب index.html: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>FlaskApp</title> <style> .todo { padding: 20px; margin: 10px; background-color: #eee; } </style> </head> <body> <h1>FlaskTODO</h1> <hr> <div class="content"> <form method="post"> <p> <b><label for="content">Todo content</label></b> </p> <p> <input type="text" name="content" placeholder="Todo Content"></input> </p> <p> <b><label for="degree">Degree</label></b> </p> <p> <input id="degree-0" name="degree" required type="radio" value="Important"> <label for="degree-0">Important</label> </p> <p> <input id="degree-1" name="degree" required type="radio" value="Unimportant"> <label for="degree-1">Unimportant</label> </p> <button type="submit">Submit</button> </form> </div> </body> </html> نحفظ الملف ونغلقه. وبذلك أصبح لدينا صفحة HTML أساسية تحتوي على عنوان وبعض التنسيقات وترويسة ونموذج ويب، إذ ضبطنا الخاصية method في نموذج الويب لتكون post للدلالة على كون نموذج الويب سيرسل إلى الخادم طلبيات من النوع POST، كما أضفنا إلى نموذج الويب هذا حقل إدخال نصي باسم content، وهو مُخصّص لإدخال محتوى المهمّة، والذي سنستخدمه لاحقًا للوصول إلى بيانات العنوان في الوجهة /، كما أضفنا للنموذج زري انتقاء باسم degree ليحدّد من خلالهما المستخدم درجة أهمية كل عنصر مهام، بحيث يمكّنه من انتقاء خيار أهمية المهمّة عند إنشائها (مهمة Important أو غير مهمة Unimportant)، وأضفنا في نهاية النموذج زر أوامر Submit لتأكيد النموذج وإرساله. سنعلم بعد ذلك فلاسك بموقع التطبيق (وهو الملف app.py في هذه الحالة) باستخدام متغير البيئة FLASK_APP وذلك أثناء وجودنا ضمن المجلد flask_app ومع تفعيل البيئة الافتراضية، كما سنضبط متغير البيئة FLASK_ENV المسؤول عن تحديد وضع التشغيل، وهنا قد اخترنا الوضع development ما يعني أنّ التطبيق سيعمل في وضع التطوير مع تشغيل مُنقّح الأخطاء. لمزيدٍ من المعلومات حول مُنقّح الأخطاء في فلاسك ننصحك بقراءة المقال كيفية التعامل مع الأخطاء في تطبيقات فلاسك، ولتنفيذ ما سبق سنشغّل الأوامر التالية: (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development والآن سنشغّل التطبيق باستخدام الأمر flask run: (env)user@localhost:$ flask run ملاحظة:لدى محاولة تشغيل التطبيق قد تظهر لك الرسالة 'ModuleNotFoundError: No module named 'pymongo والتي تفيد بعدم وجود وحدة باسم pymongo، ولإصلاح هذا الخطأ ما عليك سوى إلغاء تفعيل البيئة الافتراضية وإعادة تفعيلها مجدّدًا، وبعدها أعد تشغيل الأمر flask run من جديد. وبعد التأكد من كون خادم التطوير ما يزال قيد التشغيل، نذهب إلى الرابط التالي باستخدام المتصفح: http://127.0.0.1:5000/ فستظهر لك الصفحة الرئيسية للتطبيق متضمّنةً حقلًا لإدخال محتوى المهمّة وزريّ انتقاء لاختيار درجة الأهمية وزر أوامر لإرسال النموذج، كما في الشكل أدناه: ولمزيدٍ من المعلومات المتقدمة ولتتعرّف على كيفية إدارة نماذج الويب بأمان ننصحك بقراءة مقال استخدام والتحقق من نماذج الويب ذات واجهة المستخدم التفاعلية في فلاسك باستخدام الإضافة Flask-WTF. يرسل نموذج الإدخال هذا طلبًا من النوع POST إلى الخادم، ولكن حتى هذه اللحظة لا توجد شيفرة مسؤولة عن معالجة هذا الطلب في الوجهة /، وبالتالي لن يحدث شيء في حال ملء النموذج الآن وإرساله. نفتح نافذة طرفية جديدة أثناء تشغيل الخادم، ثمّ نفتح الملف app.py لتعديله ليتعامل مع الطلبات من النوع POST المُرسلة من قبل المستخدم، بحيث يرسلها إلى تجميعة تخزين المهام ليعرضها ضمن صفحة التطبيق الرئيسية: (env)user@localhost:$ nano app.py ونعدّل الوجهة / لتصبح كما يلي: @app.route('/', methods=('GET', 'POST')) def index(): if request.method=='POST': content = request.form['content'] degree = request.form['degree'] todos.insert_one({'content': content, 'degree': degree}) return redirect(url_for('index')) all_todos = todos.find() return render_template('index.html', todos=all_todos) احفظ الملف واغلقه. وبعد هذه التغييرات، سيجري التعامل مع الطلبيات من النوع POST باستخدام العبارة الشرطية 'if request.method == 'POST، ففي حال تحقق الشرط سيُستخرج من الكائن request.form كلًا من محتوى المهمّة ودرجة الأهمية التي أرسلها المستخدم. استخدمنا التابع ()insert_one على تجميعة المهام بغية إضافة مستند مهام إليها، كما وفرّنا بيانات المهمة ضمن قاموس بايثون عبر ضبط قيمة المفتاح المسمّى 'content' لتساوي القيمة التي أدخلها المستخدم أصلًا ضمن الحقل النصي المُخصّص لمحتوى المهمّة في النموذج، كما ضبطنا قيمة المفتاح المسمّى 'degree' لتساوي قيمة زر الانتقاء الذي اختاره المُستخدم. بعد ذلك، نعيد توجيه المُستخدم إلى الصفحة الرئيسية للتطبيق، إذ تُحدَّث هذه الصفحة لتعرض عنصر المهام الجديد المضاف. استخدمنا التابع ()find بغية عرض المهام المحفوظة خارج الجزء المسؤول عن التعامل مع الطلبيات من النوع POST من الشيفرة، والذي يعيد كافّة مستندات المهام المتوفرّة ضمن التجميعة todos، إذ تُحفظ المهام المُستخرجة من قاعدة البيانات ضمن متغير باسم all_todos، ثمّ عدلنا استدعاء التابع ()render_template بحيث يمرر قائمة مستندات المهام إلى ملف القالب index.html، والتي ستتواجد ضمن هذا القالب في متغير باسم todos. إذا حدّثت الصفحة الرئيسية للتطبيق، سيعرض المتصفح رسالة يطلب فيها تأكيد إعادة إرسال النموذج، وفي حال التأكيد سيُضاف عنصر المهام الذي حاولنا إرساله قبل إضافة الشيفرة المسؤولة عن التعامل مع الطلبيات من النوع POST إلى قاعدة البيانات، وذلك نظرًا لكون الشيفرة اللازمة للتعامل مع الطلبيات POST أصبحت متوفرّةً في الوجهة، ولكن بما أنّنا لم نضف حتى الآن شيفرة خاصّة بعرض عناصر المهام، فلن يظهر العنصر الذي أضفناه، ومن الممكن رؤية العنصر الأخير المُضاف عبر فتح صدفة mongo والاتصال مع قاعدة البيانات flask_db باستخدام الأمر التالي: > use flask_db سنستخدم بعد ذلك الدالة ()find لجلب كافّة عناصر المهام الموجودة في قاعدة البيانات: > db.todos.find() ففي حال إرسال أي بيانات ستظهر في الخرج هنا. الآن، سنفتح ملف القالب index.html لتعديله بغية عرض محتويات قائمة المهام todos التي مررناها مُسبقًا إليه، على النحو التالي: (env)user@localhost:$ nano templates/index.html نعدّل الملف بإضافة وسم فاصل أفقي <hr> وحلقة تكرارية for من تعليمات جينجا jinja وذلك بعد الجزء الخاص بالنموذج، بحيث يصبح الملف كما يلي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>FlaskApp</title> <style> .todo { padding: 20px; margin: 10px; background-color: #eee; } </style> </head> <body> <h1>FlaskTODO</h1> <hr> <div class="content"> <form method="post"> <p> <b><label for="content">Todo content</label></b> </p> <p> <input type="text" name="content" placeholder="Todo Content"></input> </p> <p> <b><label for="degree">Degree</label></b> </p> <p> <input id="degree-0" name="degree" required type="radio" value="Important"> <label for="degree-0">Important</label> </p> <p> <input id="degree-1" name="degree" required type="radio" value="Unimportant"> <label for="degree-1">Unimportant</label> </p> <button type="submit">Submit</button> </form> <hr> {% for todo in todos %} <div class="todo"> <p>{{ todo['content'] }} <i>({{ todo['degree']}})</i></p> </div> {% endfor %} </div> </body> </html> نحفظ الملف ونغلقه. أضفنا في الملف السابق وسم الفاصل الأفقي <hr> بغية فضل نموذج الويب عن قائمة المهام المعروضة، كما استخدمنا حلقة for التكرارية ضمن السطر البرمجي الآتي: {% for todo in todos %} بغية المرور على كافة عناصر المهام في قائمة todos، لتُعرض محتويات المهمّة ودرجة أهميتها ضمن وسم فقرة <p>. بتحديث صفحة التطبيق الرئيسية وملء النموذج وإرساله، ستظهر المهمّة الجديدة المُضافة أسفل النموذج، أمّا في الخطوة التالية فسنعمل على إضافة زر أوامر يتيح للمستخدم إمكانية حذف إحدى المهام الموجودة أصلًا. الخطوة 4 - حذف مهام سنعمل في هذه الخطوة على إضافة وجهة تمكّن المستخدمين من حذف المهام باستخدام زر أوامر مُخصّص لهذا الغرض. بدايةً، سنضيف وجهةً جديدةً للحذف، وهي /id/delete تتعامل مع الطلبات من النوع POST، إذ ستستقبل دالة الحذف الجديدة delete() رقم معرّف التدوينة ID المراد حذفها من خلال الرابط URL لتستخدم هذا المعرّف لحذف المهمّة الموافقة. لحذف مهمة ما، سنجلب بدايةً معرّفها مثل سلسلة نصية والتي لا بُدّ من تحويلها إلى النوع ObjectId (نمط البيانات BSON وهو التمثيل الثنائي للنمط JSON) قبل تمريره إلى تابع الحذف من التجميعة، لذا يجب استيراد الصنف ()ObjectId من الوحدة bson، وهي المسؤولة عن التعامل مع ترميز وفك ترميز نمط البيانات BSON (أي JSON الثنائي). الآن، سنفتح الملف app.py لتحريره: (env)user@localhost:$ nano app.py وبدايةً سنضيف الاستدعاء التالي إلى بداية الملف، على النحو التالي: from bson.objectid import ObjectId # ... استُدعي الصنف ()ObjectId لاستخدامه في تحويل المعرفات IDs إلى كائنات من النوع ObjectId. سنضيف الآن الوجهة التالية إلى نهاية الملف app.py: # ... @app.post('/<id>/delete/') def delete(id): todos.delete_one({"_id": ObjectId(id)}) return redirect(url_for('index')) نحفظ الملف ونغلقه. استخدمنا في الشيفرة السابقة المزخرف app.post المُقدّم في الإصدار 2.0.0 من فلاسك بدلًا من استخدام المزخرف app.route المعتاد، والذي أضاف اختصارات للعديد من توابع HTTP الشائعة، فعلى سبيل المثال الأمر ("app.post("/login@ هو اختصار للأمر: @app.route("/login", methods=["POST"]) ما يعني أن دالة العرض هذه تتعامل فقط مع طلبيات من النوع POST، وبزيارة الوجهة /ID/delete في المتصفّح سيظهر الخطأ 405 Method Not Allowed لأن المتصفحات تستخدم طريقة GET افتراضيًا للطلبات. لذلك، بغية حذف مهمة ما، سنضيف زر أوامر عندما يضغط المستخدم عليه، تُرسَل إلى الوجهة طلبية من النوع POST. تستقبل الدالة معرّف مستند المهمّة المراد حذفها، ليُمرّر إلى التابع ()delete_one المُطبّق على التجميعة todos، ليُحوّل المعرّف المُستقبل من نوع سلسلة نصية إلى كائن من النوع ObjectId باستخدام الصنف ()ObjectId المستورد سابقًا. وبعد حذف مستند المهمة، نعيد توجيه المستخدم إلى الصفحة الرئيسية للتطبيق، أمّا الآن فسنعمل على تحرير ملف القالب index.html لإضافة زر الأوامر الخاص بحذف المهام، على النحو التالي: (env)user@localhost:$ nano templates/index.html سنعدّل حلقة for التكرارية بإضافة وسم نموذج <form> جديد، كما يلي: {% for todo in todos %} <div class="todo"> <p>{{ todo['content'] }} <i>({{ todo['degree']}})</i></p> <form method="POST" action="{{ url_for('delete', id=todo['_id']) }}" > <input type="submit" value="Delete Todo" onclick="return confirm('Are you sure you want to delete this entry?')"> </form> </div> {% endfor %} نحفظ الملف ونغلقه. وبذلك يكون لدينا نموذج ويب يُرسل طلبيةً من النوع POST إلى دالة العرض ()delete، إذ مررنا المعامل ['todo['_id لتحديد المهمّة المطلوب حذفها، كما استخدمنا التابع ()confirm المتوفّر في متصفّح الويب لعرض رسالة تأكيد قبل إرسال الطلب. الآن، بعد تحديث الصفحة الرئيسية للتطبيق، سيظهر زر أوامر لحذف المهمة أسفل كل عنصر مهام، وعند النقر عليه ستظهر رسالة لتأكيد عملية الحذف، ومن ثمّ سيعيد التطبيق التوجيه إلى صفحته الرئيسية فنلاحظ حذف المهمة فعلًا. وبذلك أصبح تطبيق فلاسك يوفّر طريقةً لحذف المهام غير المرغوبة من قاعدة البيانات mongoDB. ولتأكيد الحذف، نفتح صدفة mongo ونستخدم الدالة ()find على النحو التالي: > db.todos.find() وعندها سنلاحظ أنّ العناصر المحذوفة لم تعد موجودةً في التجميعة todos. الخاتمة أنشأنا في هذا المقال تطبيق ويب في فلاسك لإدارة المهام، إذ يتخاطب هذا التطبيق مع قاعدة بيانات MongoDB، وتعرفنا على كيفية الاتصال مع خادم قاعدة البيانات MongoDB، وأنشأنا تجميعات تُخزّن مجموعة من المستندات، واستعرضنا كيفية إدخال البيانات إلى تجميعة وجلبها أو حذفها منها. ترجمة -وبتصرف- للمقال How To Use MongoDB in a Flask Application لصاحبه Abdelhadi Dyouri. اقرأ أيضًا كيفية استخدام القوالب في تطبيقات فلاسك Flask استخدام MongoDB و Redis في PHP دمج قاعدة البيانات MongoDB في تطبيقك Node
-
نحتاج عادةً إلى قاعدة بيانات في تطبيقات الويب، وهي مجموعة مُنطمّة من البيانات، نستخدمها لتخزين وتنظيم البيانات الدائمة، موفّرةً لنا إمكانية استرجاع هذه البيانات ومعالجتها بفعالية، إذ نحتاج مثلًا في تطبيق ما للتواصل الاجتماعي إلى قاعدة بيانات لتخزين بيانات المستخدم (من معلومات شخصية ومنشورات وتعليقات ومتابعين) بحيث نتمكّن من معالجة البيانات هذه بفعالية، إذ توفّر قاعدة البيانات إمكانية إضافة بيانات جديدة إليها، أو استرجاع بيانات مُخزّنة أصلًا، أو التعديل عليها، أو حذفها بكل مرونة وذلك اعتمادًا على المتطلبات والشروط والظروف المُختلفة، ففي تطبيق ويب ما قد تكون هذه المتطلبات مثلًا هي حالة إضافة المُستخدم لمنشور جديد، أو حذف منشور سابق، أو حذف حسابه مع كافّة منشوراته أو مع الإبقاء عليها، بمعنى أنّ طريقة معالجة البيانات تعتمد أولًا وأخيرًا على الميزات التي يتيحها التطبيق، فمثلًا قد لا ترغب بالسماح لمُستخدمي تطبيقك بإضافة منشورات غير معنونة. يُعد فلاسك إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا عدة أدوات وميزات من شأنها إنشاء تطبيقات ويب في لغة بايثون. أمّا PostgreSQL أو Postgres فهو نظام لإدارة قواعد البيانات العلاقية، ويوفّر تطبيقًا للغة الاستعلامات SQL، ومتوافق مع معاييرها، ناهيك عن امتلاكه العديد من الميزات المتطورة من عمليات موثوقة وتحكّم متزامن مُتعدّد النسخ دون قيود على قراءة البيانات من القاعدة. سنعمل في هذا المقال على بناء تطبيق ويب مُصغّر لتقييم الكتب والذي سنوضّح من خلاله كيفية استخدام المكتبة psycopg2 ومحوّل قاعدة بيانات PostgreSQL، الذي يسمح لنا بالتخاطب والتفاعل مع قاعدة بيانات PostgreSQL باستخدام لغة بايثون، إذ سنبني التطبيق باستخدام إطار العمل فلاسك ليؤدّي بعض المهام الأساسية من اتصال بخادم قاعدة البيانات وإنشاء الجداول وإدخال بيانات في جدول، إضافةً إلى جلب بيانات من جدول. مستلزمات العمل قبل المتابعة في هذا المقال لا بُدّ من: توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو "flask_app". الفهم الجيد لأساسيات فلاسك، مثل مفهوم الوجهات ودوال العرض والقوالب، وفي هذا الصدد يمكنك الاطلاع على المقالين كيفية بناء موقعك الإلكتروني الأول باستخدام إطار عمل فلاسك Flask من لغة بايثون وكيفية استخدام القوالب في تطبيقات فلاسك Flask لفهم مبادئ فلاسك. فهم أساسيات لغة HTML. توفّر PostgreSQL مثبت على حاسوبك مع وصول إلى نافذة أسطر الأوامر الخاصة به. الخطوة 1 - إنشاء كل من قاعدة بيانات ومستخدم PostgreSQL سنعمل في هذه الخطوة على إنشاء قاعدة بيانات باسم "flask_db" ومستخدم لها باسم user وذلك لاستخدامهما لاحقًا في تطبيق فلاسك المراد إنشاؤه. يُنشأ خلال تثبيت postgres مستخدم بصلاحيات على مستوى نظام التشغيل باسم postgres ليتوافق مع حساب المستخدم المسؤول كامل الصلاحيات postgres الخاص بنظام PostgreSQL، إذ سنستخدم هذا الحساب في المهام التي تتطلّب صلاحيات مسؤول، فحينها سنستخدم الأمر sudo ممرّرين بعده اسم المستخدم مع استخدام الخيار iu-. لذا، سجّل دخولك الآن إلى جلسة عمل تفاعلية في Postgres مُستخدمًا الأمر التالي: user@localhost:$ sudo -iu postgres psql فستظهر لك طرفية PostgreSQL والتي من الممكن إعداد المتطلبات من خلالها. بدايةً، سننشئ قاعدة بيانات لمشروعنا: postgres=# CREATE DATABASE flask_db; ملاحظة: يجب أن تنتهي كل تعليمة من تعليمات Postgres بفاصلة منقوطة، ففي حال ظهور أي أخطاء، تأكّد من انتهاء كل سطر برمجي بفاصلة منقوطة. ومن ثمّ سننشئ حساب مستخدم لقاعدة البيانات، وهنا لا بدّ من اختيار كلمة مرور آمنة: postgres=# CREATE USER user WITH PASSWORD 'password'; والآن سنعطي هذا المستخدم صلاحيات المسؤول على قاعدة البيانات: postgres=# GRANT ALL PRIVILEGES ON DATABASE flask_db TO sammy; وللتأكّد من إتمام إنشاء قاعدة البيانات، نكتب الأمر التالي المسؤول عن إظهار قائمة بقواعد البيانات المُنشأة: postgres=# \l فستظهر قاعدة البيانات المسماة "flask_db" ضمن القائمة السابقة. وعند الانتهاء نغلق نافذة طرفية PstgreSQL باستخدام الأمر: postgres=# \q ومع نهاية هذه الخطوة نكون قد أعددنا sPostgre وأصبح من الممكن الاتصال وإدارة المعلومات المخزّنة في قواعد بياناته باستخدام لغة بايثون معتمدين على المكتبة psycopg2، أمّا في الخطوة التالية فسنعمل على تثبيت هذه المكتبة جنبًا إلى جنب مع حزمة فلاسك. الخطوة 2 - تثبيت فلاسك ومكتبة psycopg2 سنعمل في هذه الخطوة على تثبيت كل من فلاسك ومكتبة psycopg2 مما يمكننا من التخاطب مع قاعدة البيانات باستخدام لغة بايثون. لذا، وبعد التأكّد من كون البيئة الافتراضية مُفعّلة، نستخدم أمر تثبيت الحزم pip لتثبيت كل من فلاسك ومكتبة psycopg2 على النحو التالي: (env)user@localhost:$ pip install Flask psycopg2-binary وبمجرّد انتهاء التثبيت بنجاح، سيظهر في نهاية الخرج سطر شبيه بما يلي مؤكدًا نجاح العملية: Successfully installed Flask-2.0.2 Jinja2-3.0.3 MarkupSafe-2.0.1 Werkzeug-2.0.2 click-8.0.3 itsdangerous-2.0.1 psycopg2-binary-2.9.2 مع انتهاء هذه الخطوة، نكون قد ثبتنا كافّة الحزم اللازمة في البيئة الافتراضية، وسنعمل في الخطوة التالية على إعداد قاعدة البيانات والاتصال بها. الخطوة 3 - إعداد قاعدة بيانات سننشئ في هذه الخطوة -ضمن مجلد المشروع المسمّى "flask_app"- ملف بايثون وظيفته الاتصال مع قاعدة البيانات "flask_db"، كما سننشئ ضمن قاعدة البيانات هذه جدولًا لتخزين الكتب، وسندخل ضمنه يدويًا بعض الكتب مع نبذة عن كل منها. الآن وبعد التأكّد من كون البيئة الافتراضية مُفعّلة، نفتح ملفًا جديدًا باسم "init_db.py" ضمن المجلد "flask_app" على النحو التالي: (env)user@localhost:$ nano init_db.py سيفتح هذا الملف الاتصال مع قاعدة البيانات "flask_db"، مُنشئًا فيها جدولًا للكتب باسم "books"، مالئًا إياه ببيانات تجريبية، ولإنجاز ذلك نكتب الشيفرة التالية ضمن الملف "init_db.py": import os import psycopg2 conn = psycopg2.connect( host="localhost", database="flask_db", user=os.environ['DB_USERNAME'], password=os.environ['DB_PASSWORD']) # Open a cursor to perform database operations cur = conn.cursor() # Execute a command: this creates a new table cur.execute('DROP TABLE IF EXISTS books;') cur.execute('CREATE TABLE books (id serial PRIMARY KEY,' 'title varchar (150) NOT NULL,' 'author varchar (50) NOT NULL,' 'pages_num integer NOT NULL,' 'review text,' 'date_added date DEFAULT CURRENT_TIMESTAMP);' ) # Insert data into the table cur.execute('INSERT INTO books (title, author, pages_num, review)' 'VALUES (%s, %s, %s, %s)', ('A Tale of Two Cities', 'Charles Dickens', 489, 'A great classic!') ) cur.execute('INSERT INTO books (title, author, pages_num, review)' 'VALUES (%s, %s, %s, %s)', ('Anna Karenina', 'Leo Tolstoy', 864, 'Another great classic!') ) conn.commit() cur.close() conn.close() نحفظ الملف ونغلقه. استوردنا في الشيفرة السابقة الوحدة os المُستخدمة في الوصول إلى متغيرات البيئة التي خزّنا فيها سابقًا كل من اسم المستخدم وكلمة المرور الخاصين بقاعدة البيانات، وهذا يضمن عدم ظهورهما ضمن الشيفرة المصدرية. كما استوردنا المكتبة psycopg2، ومن ثمّ فتحنا اتصالًا مع قاعدة البيانات flask_db باستخدام الدالة ()psycopg2.connect، وفيها حددنا المضيف وهو الحاسوب المحلي في حالتنا، كما مرّرنا اسم قاعدة البيانات إلى المُعامل database. وقد وفرّنا كل من اسم المستخدم وكلمة المرور اللازمين للدخول إلى قاعدة البيانات باستخدام الكائن os.environ الذي يُمكنّنا من الوصول إلى متغيرات البيئة التي أعددناها سابقًا ضمن البيئة البرمجية، ليُخزّن اسم المستخدم ضمن متغير بيئة باسم DB_USERNAME وكلمة المرور ضمن متغير بيئة باسم DB_PASSWORD، وبهذا نكون قد خزّنا كل من اسم المستخدم وكلمة المرور خارج الشيفرة المصدرية، ما يضمن أمان هذه المعلومات الحساسة ويمنع كشفها لدى حفظ نسخة من الشيفرة المصدرية في المتحكّم المركزي، أو لدى رفعها على خادم عامل على شبكة الإنترنت. كما أنشأنا مؤشر cur باستخدام التابع ()connection.cursor، الذي يمكّن شيفرات بايثون من تنفيذ أوامر PostgreSQL خلال جلسات عمل قاعدة البيانات. استخدمنا تابع المؤشر ()execute لحذف أي جداول موجودةٍ مسبقًا باسم "books" في حال وجودها، وذلك لتجنُّب أي تضاربٍ، أو نتائج عشوائية ناتجةٍ عن تشابه أسماء الجداول (مثل حالة وجود جدولين بنفس الاسم وبأعمدة مُختلفة في قاعدة البيانات)، ولكن وفي حالتنا الآن وكوننا لم ننشئ أي جداول بعد، فلن يُنفَّذ هذا السطر في الوقت الراهن، ومن الجدير بالملاحظة أنّ هذا الأمر سيحذِف كل المحتويات في قاعدة البيانات في كل مرة تشغِّل فيها الملف "init_db.py"، ولكن في حالة مثالنا سننفذ هذا الملف لمرّة واحدة فقط لتهيئة قاعدة البيانات، إلّا أنّك قد ترغب مُستقبلًا بتنفيذه مُجدّدًا بغية تفريغ قاعدة البيانات من كافّة محتوياتها والبدء من جديد بقاعدة بيانات فارغة. ثمّ استخدمنا الأمر CREATE TABLE books لإنشاء جدول للكتب ضمن قاعدة البيانات باسم books يحتوي على الأعمدة التالية: 'id': يحتوي على بياناتٍ من نمط رقم تسلسلي serial، أي أعداد صحيحة متزايدة تلقائيًا، ويمثّل هذا العمود مفتاحًا أساسيًا والذي قد حدّدناه باستخدام الكلمات المفتاحية PRIMARY KEY. وستُخصّص قاعدة البيانات قيمة فريدة لهذا المفتاح من أجل كل سجل مُدخل (والسجل هو الكتاب في حالتنا). "title": يمثّل عنوان الكتاب، ويحتوي على بيانات من النوع varchar، وهو نوع بيانات يحتوي على محارف بطول معين، وفي حالتنا (varchar (150 تعني أنّ عدد المحارف المتاحة للعنوان هي حتى 150 محرف، أمّا التعليمة NOT NULL فتعني أنّه من غير المسموح ترك هذا العمود فارغًا. author: يحتوي على اسم مؤلف الكتاب، على ألّا يتجاوز 50 محرف، أمّا التعليمة NOT NULL فتعني أنّه من غير المسموح ترك هذا العمود فارغًا. pages_num: يحتوي على عدد صحيح يمثّل عدد صفحات الكتاب، وتعني التعليمة NOT NULL أنّه من غير المسموح ترك هذا العمود فارغًا. review: يحتوي على نبذة عن الكتاب، وبياناته من النوع text، ما يعني أنّ محتويات هذا العمود هي سلاسل نصية بأي طول. 'date_added': يحتوي على تاريخ إضافة الكتاب إلى الجدول، والقيمة DEFAULT تعني ضبط القيمة الافتراضية إلى CURRENT_TIMESTAMP، التي تمثِّل وقت إضافة الكتاب إلى قاعدة البيانات، وكما هو الحال في عمود id، لا يتوجب عليك تحديد قيم لهذا العمود، إذ أنها تُملأ تلقائيًا. بعد الانتهاء من إنشاء الجدول، استخدمنا تابع المؤشّر ()execute لإدخال كتابين في الجدول، الأول بعنوان "A Tale of Two Cities" للكاتب "Charles Dickens"، والثاني بعنوان "Anna Karenina" للمؤلف "Leo Tolstoy"، وقد استخدمنا الموضع المؤقت s% لتمرير القيم إلى تعليمات SQL، إذ تتعامل مكتبة psycopg2 مع عمليات الإدخال في الخلفية بطريقة تضمن الحماية من هجمات حقن تعليمات SQL. بعد الانتهاء من إدخال بيانات الكتب إلى الجدول، استخدمنا التابع ()connection.commit لتأكيد العملية وتطبيق التغييرات على قاعدة البيانات، ونهايةً أغلقنا كل من المؤشر والاتصال باستخدام التابعين ()cur.close و ()conn.close على التوالي. الآن، سنشغّل الأوامر التالية لتأسيس الاتصال مع قاعدة البيانات، وإعداد كل من متغيري البيئة DB_USERNAME و DB_PASSWORD، ولكن استخدم اسم المستخدم وكلمة المرور الخاصين بك: (env)user@localhost:$ export DB_USERNAME="user" (env)user@localhost:$ export DB_PASSWORD="password" والآن، سنشغّل الملف "init_db.py" ضمن نافذة الطرفية باستخدام الأمر python كما يلي: (env)user@localhost:$ python init_db.py وفور انتهاء تنفيذ الملف بنجاح، سيُضاف جدول جديد باسم "books" إلى قاعدة البيانات "flask_db". والآن سجّل الدخول إلى جلسة Postgres تفاعلية للتحقّق من الجدول الجديد المُسمّى "books"، على النحو التالي: (env)user@localhost:$ sudo -iu postgres psql نتصل مع قاعدة البيانات "flask_db" باستخدام الأمر c\: postgres=# \c flask_db سنستخدم الآن التعليمة SELECT للحصول على عناوين الكتب وأسماء مؤلفيها من جدول الكتب books على النحو التالي: postgres=# SELECT title, author FROM books; فيظهر الخرج كما يلي: title | author ----------------------+------------------ A Tale of Two Cities | Charles Dickens Anna Karenina | Leo Tolstoy نُغلق الجلسة التفاعلية باستخدام الأمر q\. سنعمل في الخطوة التالية على إنشاء تطبيق فلاسك مُصغّر، يتصل بقاعدة البيانات ليجلب منها نبذةً عن كلا الكتابين اللذين أدخلناهما سابقًا في قاعدة البيانات ليعرضهما في صفحته الرئيسية. الخطوة 4 - عرض الكتب سنعمل في هذه الخطوة على إنشاء تطبيق فلاسك ذو صفحة رئيسية مسؤولة عن عرض الكتب الموجودة في قاعدة البيانات بعد جلبها منها. لذا وأثناء كون البيئة البرمجية مُفعّلة وبعد تثبيت فلاسك، سنفتح ملف "app.py" ضمن المجلد "flask_app" لتحريره: (env)user@localhost:$ nano app.py سيعمل هذا الملف على إعداد الاتصال مع قاعدة البيانات وإنشاء وجهة فلاسك وحيدة قادرة على استخدام هذا الاتصال، لذا سنكتب ضمنه الشيفرة التالية: import os import psycopg2 from flask import Flask, render_template app = Flask(__name__) def get_db_connection(): conn = psycopg2.connect(host='localhost', database='flask_db', user=os.environ['DB_USERNAME'], password=os.environ['DB_PASSWORD']) return conn @app.route('/') def index(): conn = get_db_connection() cur = conn.cursor() cur.execute('SELECT * FROM books;') books = cur.fetchall() cur.close() conn.close() return render_template('index.html', books=books) نحفظ الملف ونغلقه. استوردنا بدايةً في الشيفرة السابقة كل من الوحدة os والمكتبة psycopg2 والصنف Flask ودالة تصيير القوالب ()render_template من حزمة فلاسك، كما أنشأنا نسخة من التطبيق باسم "app"، ثمّ عرفنا دالةً باسم ()get_db_connection لإنشاء اتصال مع قاعدة البيانات "flask_db" باستخدام اسم المستخدم وكلمة المرور المخزنيْن في متغيرات البيئة DB_USERNAME و DB_PASSWORD على التوالي، ويعيد التابع كائن الاتصال conn، الذي سنستخدمه للوصول إلى قاعدة البيانات. استخدمنا بعد ذلك المُزخرف ()app.route لإنشاء وجهة رئيسية / ودالة عاملة في فلاسك باسم ()index، وفتحنا اتصالًا مع قاعدة البيانات باستخدام الدالة get_db_connection()، ثم أنشأنا مؤشرًا ونفذنا التعليمة ;SELECT * FROM books من تعليمات SQL بغية الحصول على كل الكتب الموجودة في قاعدة البيانات، إذ أنّنا نستدعي التابع fetchall() لحفظ البيانات ضمن متغير باسم books، ومن ثم نغلق كل من المؤشر والاتصال مع قاعدة البيانات. ونهايةً نجعل القيمة المعادة استدعاءً للدالة ()render_template لإخراج ملف قالب باسم "index.html" ممررين له قائمة الكتب التي جلبناها من قاعدة البيانات إلى المتغير books. الآن، وبغية عرض الكتب المُخزنة ضمن قاعدة البيانات في الصفحة الرئيسية للتطبيق، سننشئ بدايةً ملف قالب ليتضمّن كافة شيفرات HTML الأساسية اللازمة لتستخدمها لاحقًا القوالب الأُخرى، وهذا ما يجنبنا تكرار الشيفرات، ثمّ سننشئ ملف قالب الصفحة الرئيسية "index.html" المُصيّر أصلًا باستخدام الدالة ()index. سننشئ مجلدًا للقوالب باسم "templates" وننشئ ضمنه ملف قالب باسم "base.html"، والذي سيمثّل القالب الأساسي لبقية القوالب على النحو التالي: (env)user@localhost:$ mkdir templates (env)user@localhost:$ nano templates/base.html وسنكتب ضمنه الشيفرة التالية: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %} {% endblock %}- FlaskApp</title> <style> nav a { color: #d64161; font-size: 3em; margin-left: 50px; text-decoration: none; } .book { padding: 20px; margin: 10px; background-color: #f7f4f4; } .review { margin-left: 50px; font-size: 20px; } </style> </head> <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% block content %} {% endblock %} </div> </body> </html> احفظ الملف واغلقه. يتضمّن القالب الأساسي كافّة الشيفرات المتداولة التي سنحتاجها في القوالب الأُخرى. ستُستبدل لاحقًا كتلة العنوان title بعنوان كل صفحة وكتلة المحتوى content بمحتواها، أمّا عن شريط التصفح فسيتضمّن رابطين، الأوّل ينقل المُستخدم إلى الصفحة الرئيسية للتطبيق باستخدام الدالة المساعدة ()url_for لتحقيق الربط مع دالة العرض ()index، والآخر لصفحة المعلومات حول التطبيق About في حال قررت تضمينها في تطبيقك. الآن، سنفتح ملف القالب باسم "index.html" وهو الاسم الذي حددناه في الملف "app.py": (env)user@localhost:$ nano templates/index.html ونضيف ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1>{% block title %} Books {% endblock %}</h1> {% for book in books %} <div class='book'> <h3>#{{ book[0] }} - {{ book[1] }} BY {{ book[2] }}</h3> <i><p>({{ book[3] }} pages)</p></i> <p class='review'>{{ book[4] }}</p> <i><p>Added {{ book[5] }}</p></i> </div> {% endfor %} {% endblock %} نحفظ الملف ونغلقه. اعتمدنا في الشيفرة السابقة على الوراثة من ملف القالب "base.html" من خلال تعليمة extends، واستبدلنا محتوى كتلة المحتوى content مُستخدمين تنسيق العنوان من المستوى الأوّل <h1> الذي يفي أيضًا بالغرض مثل عنوان للصفحة. استخدمنا في السطر البرمجي {% for book in books %} حلقة for من تعليمات محرّك القوالب جينجا jinja، والهدف من استخدام هذه الحلقة هو المرور على كل عنصر في القائمة books، إذ نظهر بدايةً رقم معرّف الكتاب ID والذي يمثّل العنصر الأوّل باستخدام الأمر [book[0، ثمّ نظهر كلًا من عنوان الكتاب واسم مؤلفه وعدد صفحاته والنبذة التعريفية عنه وتاريخ إضافته. الآن ومع وجودنا ضمن المجلد flask_app ومع كون البيئة الافتراضية مُفعّلة، نُعلم فلاسك بالتطبيق المراد تشغيله (وهو في حالتنا الملف "app.py") باستخدام متغير البيئة FLASK_APP، ومن ثمّ نعيّن وضع التشغيل من خلال متغير البيئة FLASK_ENV واخترنا هنا العمل في وضع التطوير development، مع تشغيل مُنقّح الأخطاء. للمزيد من المعلومات حول مُنقّح الأخطاء في فلاسك ننصحك بقراءة المقال كيفية التعامل مع الأخطاء في تطبيقات فلاسك. لتنفيذ ما سبق سنشغّل الأوامر التالية: (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development وهنا لا بُدّ من التأكد من تعيين متغيرات البيئة DB_PASSWORD و DB_USERNAME في حال عدم إعدادها مُسبقًا: (env)user@localhost:$ export DB_USERNAME="user" (env)user@localhost:$ export DB_PASSWORD="password" والآن سنشغّل التطبيق باستخدام الأمر التالي: (env)user@localhost:$ flask run وبعد التأكد من كون خادم التطوير ما يزال قيد التشغيل، نذهب إلى الرابط التالي باستخدام المتصفح: http://127.0.0.1:5000/ وعندها ستظهر لك الكتب التي أضفتها بدايةً إلى قاعدة البيانات بالشّكل التالي: ومع نهاية هذه الخطوة نكون قد تمكنّا من عرض الكتب المُخزنة ضمن قاعدة البيانات مُسبقًا في الصفحة الرئيسية للتطبيق، أمّا في الخطوة التالية فسنعمل على إضافة وجهة جديدة من شأنها السماح للمُستخدمين بإضافة كتب جديدة إلى قاعدة البيانات. الخطوة 5 - إضافة كتب جديدة سنعمل في هذه الخطوة على إضافة وجهة جديدة للسماح للمُستخدمين بإضافة كتب جديدة ونبذات عنها إلى قاعدة البيانات. لذلك، سنضيف للتطبيق صفحة جديدة تتضمّن نموذج ويب لإدخال كل من عنوان الكتاب واسم مؤلفه وعدد صفحاته ونبذة عنه من قبل المستخدم. سنفتح نافذة طرفية جديدة مع بقاء خادم التطوير قيد التشغيل، ثمّ سنفتح الملف "app.py": (env)user@localhost:$ nano app.py للتعامل مع نموذج الويب، لا بُد أولًا من استيراد التالي من حزمة فلاسك: الكائن request العام المسؤول عن الوصول إلى بيانات الطلب المُرسَل من قبل المُستخدم. الدالة url_for() لتوليد عناوين الروابط. الدالة redirect() لإعادة توجيه المستخدم إلى صفحة التطبيق الرئيسية بعد إضافة كتاب إلى قاعدة البيانات. أضِف هذه الاستيرادات إلى السطر الأوّل من الملف "app.py" كما يلي: from flask import Flask, render_template, request, url_for, redirect # ... ثمّ أضِف الوجهة التالية إلى نهايته: # ... @app.route('/create/', methods=('GET', 'POST')) def create(): return render_template('create.html') نحفظ الملف ونغلقه. مرّرنا في هذه الوجهة صفًا tuple، يحتوي على القيم ('GET', 'POST') إلى المعامل methods بغية السماح بكلا نوعي طلبات HTTP وهما GET و POST، إذ تتخصّص الطلبات من النوع GET بجلب البيانات من الخادم، أمّا الطلبات من النوع POST فهي مُتخصّصة بإرسال البيانات إلى وجهة مُحدّدة، مع ملاحظة أنّ الطلبات من النوع GET هي الوحيدة المسموحة افتراضيًا. وحالما يطلب المستخدم الوجهة create/ باستخدام طلب من النوع GET، سيُصيّر ملف قالب باسم "create.html"، وسنعدّل هذه الوجهة لاحقًا لتتعامل أيضًا مع الطلبات POST اللازمة لدى ملء المُستخدمين للنماذج وإرسالها بغية إضافة كتب جديدة. الآن، افتح ملف "create.html" الجديد داخل مجلد القوالب "templates" على النحو التالي: (env)user@localhost:$ nano templates/create.html واكتب ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1>{% block title %} Add a New Book {% endblock %}</h1> <form method="post"> <p> <label for="title">Title</label> <input type="text" name="title" placeholder="Book title"> </input> </p> <p> <label for="author">Author</label> <input type="text" name="author" placeholder="Book author"> </input> </p> <p> <label for="pages_num">Number of pages</label> <input type="number" name="pages_num" placeholder="Number of pages"> </input> </p> <p> <label for="review">Review</label> <br> <textarea name="review" placeholder="Review" rows="15" cols="60" ></textarea> </p> <p> <button type="submit">Submit</button> </p> </form> {% endblock %} احفظ الملف واغلقه. اعتمدنا في الشيفرة السابقة على الوراثة من ملف القالب "base.html" من خلال تعليمة extends، وعيّنا وسم ترويسة ليكون عنوانًا؛ واستخدمنا الوسم <form> وضبطنا فيه السمة method التي تحدّد نوع طلب HTTP لتكون من النوع POST، ما يعني أنّ نموذج الويب هذا سيرسل طلبًا من النوع POST؛ كما أضفنا صندوقًا نصيًا باسم title لنستخدمه للوصول إلى بيانات عنوان الكتاب في الوجهة create/؛ وأضفنا صندوقًا نصيًا مخصصًا لاسم مؤلف الكتاب، وصندوق عددي مخصص لعدد صفحات الكتاب؛ كما أضفنا حقلًا نصيًا مُتعدّد الأسطر مُخصّص للنبذة حول الكتاب، ونهايةً أضفنا إلى نهاية النموذج زر Submit لتأكيد إرساله. الآن، وأثناء خادم التطوير، استخدم المتصفح للانتقال إلى الوجهة /create: http://127.0.0.1:5000/create فستظهر لك صفحة إضافة كتاب جديد مع صندوق نصي لإدخال عنوان الكتاب، وآخر لإدخال اسم مؤلفه، ومربع لإدخال عدد صفحاته وحقل نصي مُتعدّد الأسطر لإدخال نبذة حول الكتاب، وزر لتأكيد إرسال النموذج، كما في الشّكل التالي: يرسل نموذج الإدخال هذا طلبًا من النوع "POST" إلى الخادم، ولكن حتى هذه اللحظة لا يوجد شيفرة مسؤولة عن معالجة هذا الطلب في الوجهة create/، وبالتالي لن يحدث شيء في حال ملء النموذج الآن وإرساله. الآن، افتح الملف "app.py" لتعديله بحيث يتعامل مع الطلبات من النوع "POST" المُرسلة من قبل المستخدم: (env)user@localhost:$ nano app.py ونعدّل الوجهة create/ لتصبح كما يلي: # ... @app.route('/create/', methods=('GET', 'POST')) def create(): if request.method == 'POST': title = request.form['title'] author = request.form['author'] pages_num = int(request.form['pages_num']) review = request.form['review'] conn = get_db_connection() cur = conn.cursor() cur.execute('INSERT INTO books (title, author, pages_num, review)' 'VALUES (%s, %s, %s, %s)', (title, author, pages_num, review)) conn.commit() cur.close() conn.close() return redirect(url_for('index')) return render_template('create.html') احفظ الملف واغلقه. تَمكَّنا باستخدام العبارة الشرطية if request.method == 'POST' التي تقارن قيمة request.method مع القيمة POST من التحقُّق بأنّ التعليمات التالية لها لن تُنفّذ إلّا إذا كان الطلب الحالي هو فعلًا بطريقة POST، ومن ثمّ قرأنا قيم كل من عنوان الكتاب واسم مؤلفه وعدد صفحاته والنبذة المرسلة عنه من الكائن request.form الذي يمكِّننا من الوصول إلى بيانات نموذج الإدخال المُضمّنة في الطلب. فتحنا بعد ذلك اتصالًا مع قاعدة البيانات باستخدام الدالة get_db_connection()، وأنشأنا مؤشرًا، ونفّذنا الأمر INSERT INTO من أوامر SQL باستخدام التابع ()execute لإدخال كل من عنوان الكتاب واسم مؤلفه وعدد صفحاته ونبذة عنه إلى جدول الكتب books في قاعدة البيانات وفق ما أُدخل أصلًا من قبل المُستخدم في النموذج. نهايةً، حفظنا التغييرات باستخدام الدالة ()connection.commit، وأُغلق الاتصال مع قاعدة البيانات باستخدام الدالة ()connection.close، ثم أعدنا توجيه المًستخدم إلى الصفحة الرئيسية من التطبيق ليرى الكتاب الجديد المُضاف أسفل الكتب السابقة الموجودة أصلًا. الآن وأثناء عمل خادم التطوير، استخدم المتصفح للانتقال إلى الوجهة /create باستخدام الرابط: http://127.0.0.1:5000/create املأ النموذج بالبيانات التي تريد، وحال إرسال النموذج سيُعاد توجيهك إلى الصفحة الرئيسية للتطبيق لترى الكتاب الجديد فيها. سنفتح الآن ملف القالب الأساسي base.html لإضافة رابط في شريط التصفّح للوصول إلى صفحة إضافة كتاب جديد: (env)user@localhost:$ nano templates/base.html ونعدّل الملف ليصبح كما يلي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %} {% endblock %} - FlaskApp</title> <style> nav a { color: #d64161; font-size: 3em; margin-left: 50px; text-decoration: none; } .book { padding: 20px; margin: 10px; background-color: #f7f4f4; } .review { margin-left: 50px; font-size: 20px; } </style> </head> <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="{{ url_for('create') }}">Create</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% block content %} {% endblock %} </div> </body> </html> نحفظ الملف ونغلقه. أضفنا في الشيفرة السابقة وسم رابط <a> جديد إلى شريط التصفُّح والذي يشير إلى صفحة إضافة كتاب جديد، إذ سيظهر الرابط الجديد بتحديث الصفحة الرئيسية للتطبيق ضمن شريط التصفُّح. ومع نهاية هذه الخطوة أصبح لدينا صفحة ذات نموذج إدخال لإضافة نبذات عن كتب جديدة، ولمزيدٍ من المعلومات حول النماذج وكيفية جعلها آمنة، ننصحك بقراءة المقال استخدام والتحقق من نماذج الويب ذات واجهة المستخدم التفاعلية في فلاسك باستخدام الإضافة Flask-WTF والمقال كيفية استخدام نماذج الويب في تطبيقات فلاسك. الخاتمة أنشأنا في هذا المقال تطبيق ويب مصغّر لتقييمات الكتب والذي يتخاطب مع قاعدة بيانات PostgreSQL، مع كامل الوظائف الأساسية من إضافة بيانات جديدة إلى قاعدة البيانات، وجلب بيانات منها لعرضها في صفحات التطبيق. ترجمة -وبتصرف- للمقال How To Use a PostgreSQL Database in a Flask Application لصاحبه Abdelhadi Dyouri. اقرأ أيضًا المرجع الشامل إلى تعلم PostgreSQL تجهيز قاعدة البيانات PostgreSQL والتّعريف بمفهومي ORM وإضافات Flask
-
بدأنا هذه السلسلة بمقال عن كيفية التعامل مع رسائل الأخطاء في بايثون وسنتابع الحديث في هذا المقال عن كيفية التعامل مع الملفات والمسارات في بايثون. وبايثون هي لغة برمجة عالية المستوى، وتفاعلية وكائنية. وتتمتع بمقروئية عالية، إذ تستخدم كلمات إنجليزية بسيطة، على خلاف اللغات الأخرى التي تستخدم الرموز، كما أنّ قواعدها الإملائية والصياغية بسيطة، ما يجعل تعلمها سهلًا موازنةً بلغات برمجة أخرى. ولعلّ الخطوة الأولى قبل الإبحار في تعلّم بايثون هي إعداد البيئة، والتي تعرّف بأنها عملية تنظيم الحاسوب اللازمة لاستخدامه في كتابة الشيفرات البرمجية، الأمر الذي يتضمن تثبيت أي أدوات ضرورية وإعدادها، والتعامل مع أي مشاكل قد تواجهك أثناء عملية الإعداد، ولإنجاز هذه الخطوة لابد من فهمك الجيد لنظام الملفات وكيفية التعامل مع المسارات في بايثون. سنحاول في هذا المقال توصيف بعض المبادئ الأساسية لمساعدتك في إدارة حاسوبك باستخدام نظام الملفات. قد يبدو فهم هذه المبادئ دون جدوى، فنحن في نهاية الأمر نسعى لكتابة الشيفرات لا، إلا أن اكتساب هذه المهارات الأساسية سيوفر عليك الوقت على المدى الطويل. نظام الملفات إن نظام الملفات هو الوسيلة التي يعتمدها نظام التشغيل لتنظيم البيانات وتخزينها واستعادتها، إذ يضم الملف خاصيتين رئيسيتين: اسم الملف (وهو عبارة عن كلمة واحدة عادةً) ومساره. لنفرض مثلًا وجود ملف على حاسوب محمول ذو نظام التشغيل ويندوز 10 يدعى project.docx ضمن المسار C:\Users\Al\Documents، يحدد المسار موقع الملف في الحاسوب، أما الجزء الذي يلي اسم الملف بعد النقطة فهو لاحقة الملف التي تبين نوعه. فيشير مثلًا اسم الملف project.docx إلى أن هذا الملف مستند لبرنامج معالج النصوص Word أما Users و Al و Documents فهي اسماء مجلدات، والتي يمكن أن تحتوي على ملفات ومجلدات أخرى. في مثالنا السابق يوجد الملف project.docx ضمن مجلد المستندات Documents الذي يوجد في المجلد Al الموجود بدوره ضمن مجلد المستخدمين Users، ويبين الشكل التالي ترتيب هذه المجلدات. ملف ضمن تسلسل هرمي للمجلدات إذ يمثل الجزء \:C من مسار الملف المجلد الأساسي الذي يحتوي على جميع المجلدات الأخرى. يعطى المجلد الأساسي في نظام التشغيل ويندوز Windows الاسم \:C، ويسمى أيضًا القرص :C، بينما في أنظمة التشغيل ماك أو إس Mac os ولينكس Linux فيشار إليه بالشكل /. سنعبّر عن المجلد الأساسي بطريقة نظام التشغيل ويندوز، أي بالشكل التالي \:C، أما إن كنت ستكتب أوامر أمثلة الصَدفة التفاعلية في نظام التشغيل ماك أو إس أو لينكس فأدخل / بدلاً عنها. هناك نماذج أخرى مثل محركات الأقراص DVD ومحركات الأقراص القابلة للإزالة USB flash drive والتي ستظهر بشكل مختلف في أنظمة التشغيل المختلفة، ففي نظام التشغيل ويندوز تظهر كمجلد أساسي جديد يحمل رمز حرف معين مثلاً \:D أو \:E، بينما في نظام ماك أو إس فتظهر كمجلد جديد ضمن المجلد Volumes/، وفي نظام التشغيل لينكس تظهر كمجلد جديد ضمن المجلد /mnt ("mount") ومن الجدير بالملاحظة أن أسماء المجلدات والملفات ليست حساسة لحالة الأحرف في نظامي ويندوز أو ماك أو إس، إلا أنها كذلك في نظام تشغيل لينكس. المسارات في بايثون يُستخدم الخط المائل العكسي \ للفصل بين أسماء المجلدات والملفات في نظام التشغيل ويندوز، لكن في نظامي ماك أو إس ولينكس فيتولى الخط المائل الأمامي / تلك المهمة، لذا وبدلًا عن كتابة كل شيفرة بالطريقتين لجعل النص البرمجي المكتوب بلغة بايثون قابلاً للتطبيق على مختلف أنظمة التشغيل، يمكننا استخدام الوحدة pathlib والمعامل / بدلًا من ذلك. من الممكن استيراد مكتبة pathlib من خلال تنفيذ الأمر from pathlib import Path، ولأن صنف المسار Path هو أكثر الأصناف استخدامًا في وحدة pathlib، فمن المسموح كتابة Path وحدها بدلًا من كتابة pathlib.Path. وبإمكاننا أن نمرر إلى الدالة ()Path سلسة نصية هي عبارة عن اسم المجلد أو اسم الملف، وذلك بغية إنشاء كائن مسار Path لاسم هذا المجلد أو الملف. وبما أن أن الكائن في أقصى يسار السطر البرمجي ما هو إلا كائن مسار Path، فيمكننا الآن استخدام المعامل / لدمج كائنات أو سلاسل هذا المسار مع بعضها. والآن لنكتب الشيفرة التالية في الصدفة التفاعلية: >>> from pathlib import Path >>> Path('spam') / 'bacon' / 'eggs' WindowsPath('spam/bacon/eggs') >>> Path('spam') / Path('bacon/eggs') WindowsPath('spam/bacon/eggs') >>> Path('spam') / Path('bacon', 'eggs') WindowsPath('spam/bacon/eggs') ونلاحظ بأنه كوننا ننفذ الشيفرة السابقة على حاسوب يعمل بنظام تشغيل ويندوز، فإن الدالة ()Path تعيد كائن مسار ويندوز windowspath، في حين ستتم إعادة الكائن PosixPath في حال استخدام أحد نظامي التشغيل ماك أو إس أو لينكس. (تعد POSIX مجموعة معايير لأنظمة التشغيل الشبيهة بنظام يونيكس ولن نتطرق لها في دراستنا). ومن الممكن تمرير كائن المسار إلى أي دالة تتطلب اسم ملف في المكتبة المعيارية لبايثون. فعلى سبيل المثال يعد استدعاء الدالة التالية: open(Path('C:\\') / 'Users' / 'Al' / 'Desktop' / 'spam.py') مكافئًا لاستدعاء الدالة التالية: open(r'C:\Users\Al\Desktop\spam.py') المجلد الرئيسي ما من مستخدم إلا ولديه مجلد رئيسي يخزن فيه ملفاته على الحاسوب، ومن الممكن الحصول على كائن مسار لهذا المجلد الرئيسي باستدعاء التابع ()path.home بالشكل: >>> Path.home() WindowsPath('C:/Users/Al') وتوجد المجلدات الرئيسية في مكان محدد بناءً على نوع نظام التشغيل: توجد المجلدات الرئيسية في نظام التشغيل ويندوز ضمن C:\Users. توجد المجلدات الرئيسية في نظام التشغيل ماك أو إس ضمن Users/. توجد المجلدات الرئيسية في نظام التشغيل لينكس ضمن home/. تمتلك النصوص البرمجية غالبًا أذونات القراءة والكتابة على الملفات في المجلد الرئيسي وبالتالي تعد هذه المجلدات مكانًا مثاليًا لتخزين الملفات التي ستتعامل معها برامج بايثون. مجلد العمل الحالي لكل برنامج يعمل على الحاسوب مجلد عمل حالي (cwd) توجد فيه جميع أسماء الملفات والمسارات التي لا تبدأ بالمجلد الأساسي. وعلى الرغم من أن كلمة "مجلد folder" هي الاسم الحديث للدليل Directory، يعد cwd (أو ما يعرف بدليل العمل الحالي) هو المصطلح المعياري وليس "مجلد العمل الحالي". يمكننا الحصول على دليل العمل في هيئة كائن مسار باستخدام الدالة ()Path.cwd وتغييره باستخدام الدالة ()os.chdir، ولإنجاز ذلك سنكتب الشيفرة التالية في الصدفة التفاعلية: >>> from pathlib import Path >>> import os 1 >>> Path.cwd() WindowsPath('C:/Users/Al/AppData/Local/Programs/Python/Python38') 2 >>> os.chdir('C:\\Windows\\System32') >>> Path.cwd() WindowsPath('C:/Windows/System32') اعتبرنا في الشيفرة السابقة أن مسار دليل العمل هو: C:\Users\Al\AppData\Local\Programs\Python\Python381 وبالتالي سيكون مسار الملف project.docx كالآتي: C:\Users\Al\AppData\Local\Programs\Python\Python381\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' ويعد استخدام الدالة ()os.getcwd من الوحدة os هو الطريقة الأقدم للحصول على مسار دليل العمل في هيئة سلسلة نصية. المسارات المطلقة والنسبية يوجد طريقتان لتحديد مسار ملف: المسار المطلق وهو الذي يبدأ دائمًا بالمجلد الأساسي. المسار النسبي والذي يشير عادةً إلى دليل العمل الخاص بالبرنامج. وهناك أيضًا المجلدات ذات النقطة (.) أو النقطتين (..). وهي ليست مجلدات حقيقية لكنها أسماء خاصة يمكن استخدامها في المسار. فالنقطة الوحيدة (.) تعد اختصارًا للدلالة على المجلد الحالي أو "هذا المجلد"، أما وجود نقطتين (..) فيشير إلى "المجلد الأب". ويبين الشكل التالي مثالًا لبعض المجلدات والملفات، فعندما يكون دليل العمل بالشكل C:\bacon تكون المسارات النسبية للمجلدات والملفات الأخرى كما هو مبين في الشكل أدناه. أما وجود الرمز في بداية المسار النسبي فهو أمر اختياري، فعلى سبيل المثال يشير كل من spam.txt. و spam.txt إلى الملف ذاته. المسارات النسبية للملفات والمجلدات في دليل العمل C:\bacon البرامج والعمليات يعرف البرنامج بأنه أي تطبيق برمجي يمكن تشغيله، كمتصفح الويب وتطبيق جدول البيانات ومعالج النصوص، في حين أن العملية عبارة عن نسخة قيد التشغيل running instance من البرنامج، فعلى سبيل المثال يبين الشكل أدناه خمس عمليات قيد التشغيل لنفس برنامج الآلة الحاسبة. تشغيل برنامج الآلة حاسبة عدة مرات على شكل عدة عمليات منفصلة إذ تبقى العمليات منفصلة عن بعضها حتى التابعة منها لنفس البرنامج، فمثلًا إن شغلنا عدة نسخ من برنامج بايثون في نفس الوقت ضمن عمليات مستقلة، فيمكن أن يكون لكل منها قيم متغيرات خاصة بها ومختلفة، فلكل عملية دليل العمل ومتغيرات البيئة الخاصة بها وذلك حتى بالنسبة للعمليات التابعة لنفس البرنامج، وعمومًا تُشغّل نوافذ سطر الأوامر عملية واحدة في كل مرة (ومن الممكن أيضًا فتح عدة نوافذ أوامر في نفس الوقت). ولكل نظام تشغيل طريقته لعرض قائمة العمليات قيد التنفيذ، ففي نظام ويندوز من الممكن تشغيل تطبيق مدير المهام عبر الضغط على مفاتيح CTRL+SHIFT+ESC، أما في نظام ماك أو إس فيمكن الوصول إليها من قائمة التطبيقات Application ومنها نختار الخدمات Utilities ومن ثم مراقب الأداء Activity Monitor، أما في نظام أبونتو لينكس يمكننا الضغط على مفاتيح CTRL+ALT+DEL معًا لتشغيل تطبيق يدعى أيضًا بمدير المهام، ويمكن لمدير المهام أيضًا إنهاء عملية ما قيد التشغيل إن لم تكن تستجيب. الخلاصة يتضمن ضبط البيئة كافة الخطوات الضرورية لجعل الحاسوب قادرًا على تشغيل برامجنا بسهولة، وتتطلب هذه العملية منا معرفة بعض مفاهيم الأولية حول كيفية عمل الحاسوب، كفهم نظام الملفات والمسارات والعمليات. إن نظام الملفات هو الآلية التي ينظم بها الحاسوب الملفات، فلكل ملف مسار مطلق كامل أو مسار نسبي بالنسبة لدليل العمل الحالي، ويمكنك التنقل في نظام الملفات من خلال سطر الأوامر. قد يستغرق الأمر وقتًا لتعتاد التعامل مع نظام الملفات باستخدام موجه الأوامر، لذا لا تتردد في البحث مطولًا على الإنترنت عن المساعدة، فهو الأمر الطبيعي الذي يقوم به مطورو البرامج يوميًا. ترجمة -وبتصرف- للفصل الثاني "إعداد البيئة وواجهة سطر الأوامر" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: أصول طلب المساعدات البرمجية في بايثون عبر الإنترنت التعامل مع الملفات النصية في بايثون كيفية التعامل مع الملفات النصية في بايثون 3 التعامل مع الصفوف، المجموعات والقواميس في بايثون
-
نحتاج عادةً إلى قاعدة بيانات في تطبيقات الويب، وهي مجموعةٌ مُنظمّةٌ من البيانات نستخدمها لتخزين وتنظيم البيانات الدائمة، موفّرةً لنا إمكانية استرجاع هذه البيانات وإدارتها بفعالية، إذ نحتاج مثلًا في تطبيق ما للتواصل الاجتماعي إلى قاعدة بيانات لتخزين بيانات المستخدم (من معلومات شخصية ومنشورات وتعليقات ومتابعين) بحيث نتمكّن من معالجة هذه البيانات بفعالية، إذ توفّر قاعدة البيانات إمكانية إضافة بيانات جديدة إليها، أو استرجاع بيانات مُخزّنة أصلًا، أو التعديل عليها، أو حذفها بكل مرونة وذلك اعتمادًا على المتطلبات والشروط والظروف المُختلفة؛ ففي تطبيق ويب ما قد تكون هذه المتطلبات مثلًا هي حالة مُستخدم يضيف منشورًا جديدًا، أو يحذف منشورًا سابقًا، أو يحذف حسابه مع كافّة منشوراته أو مع الإبقاء عليها، بمعنى أنّ طريقة معالجة البيانات تعتمد أولًا وأخيرًا على الميزات التي يتيحها التطبيق، فمثلًا قد لا ترغب بالسماح لمُستخدمي تطبيقك بإضافة منشورات غير معنونة. يُعد فلاسك إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا عدة أدوات وميزات من شأنها إنشاء تطبيقات ويب في لغة بايثون. أمّا SQLite فهو محّرك قواعد بيانات مفتوح المصدر، يمتاز ببساطته وسرعته ويُستخدم مع بايثون لتخزين بيانات التطبيق ومعالجتها، إذ يعمل مع بايثون بكفاءة، نظرًا لكون مكتبة بايثون المعيارية توفّر الوحدة sqlite3 المُختصّة بالتخاطب مع أي قاعدة بيانات SQLite دون الحاجة لتثبيت أي أدوات إضافية، فاستخدام قواعد بيانات SQLite تحديدًا مع بايثون يتطلّب كمًّا قليلًا من التثبيتات مقارنةً بمحركات قواعد البيانات الأُخرى. سنعمل في هذا المقال على إنشاء تطبيق ويب مُصغّر لنبيّن من خلاله كيفية استخدام قواعد بيانات SQLite في فلاسك لإجراء عمليات المعالجة الأساسية للبيانات من إنشاء Create وقراءة Read وتحديث Update وحذف Delete، أو ما يسمّى اختصارًا CRUD، إذ سيكون تطبيق الويب مدونة تعرض التدوينات ضمن صفحتها الرئيسية متيحةً لمستخدميها إمكانية إنشاء وتعديل وحذف أي تدوينة. مستلزمات العمل قبل المتابعة في هذا المقال لا بُدّ من: توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو "flask_app". الفهم الجيد لأساسيات فلاسك، مثل مفهوم الوجهات ودوال العرض والقوالب، ويمكنك في هذا الصدد الاطلاع على المقالين كيفية بناء موقعك الإلكتروني الأول باستخدام إطار عمل فلاسك Flask من لغة بايثون وكيفية استخدام القوالب في تطبيقات فلاسك Flask لفهم مبادئ فلاسك. فهم أساسيات لغة HTML. فهم أساسيات استخدام SQLite. الخطوة الأولى - إعداد قاعدة البيانات سنعمل في هذه الخطوة على إعداد قاعدة بيانات من النوع SQLite والتي سنخزّن فيها بيانات التطبيق (وهي التدوينات في حالة تطبيقنا)، ثم سنملؤها ببعض المُدخلات التجريبية، وسنستخدم الوحدة sqlite3 المتوفرة بسهولة في مكتبة بايثون المعيارية بغية التخاطب والتفاعل مع قاعدة البيانات. بدايةً، وبما أنّ تخزين البيانات في SQLite يكون ضمن جداول وأعمدة، وكون بيانات تطبيقنا هي تدوينات، سننشئ جدولًا باسم "posts" يحتوي على كافّة الأعمدة اللازمة، إذ سننشئ ملفًا بلاحقة sql. يحتوي على أوامر SQL اللازمة لإنشاء الجدول "posts" وأعمدته اللازمة، ثم سنستخدم ملف تخطيط قاعدة البيانات هذا لإنشاء قاعدة البيانات. لذا، افتح ملف تخطيط قاعدة البيانات المُسمى "schema.sql" الموجود في المجلد "flask_app": $ nano schema.sql واكتب تعليمات SQL التالية داخل هذا الملف: DROP TABLE IF EXISTS posts; CREATE TABLE posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, title TEXT NOT NULL, content TEXT NOT NULL ); احفظ الملف واغلقه. يعمل أوّل أمر من أوامر SQL -في ملف تخطيط قاعدة البيانات السابق- على حذف أي جداول موجودةٍ مسبقًا باسم "posts" لتجنُّب أي تضارب، أو نتائج عشوائية ناتجةٍ عن تشابه أسماء الجداول (مثل حالة وجود جدولين بنفس الاسم وبأعمدة مُختلفة في قاعدة البيانات)، ولكن وفي حالتنا الآن وكوننا لم ننشئ أي جداول بعد، فلن يُنفَّذ هذا السطر في الوقت الراهن، ومن الجدير بالملاحظة أنّ هذا الأمر سيحذِف كل المحتويات في قاعدة البيانات في كل مرة تُشغِّل فيها أوامر SQL هذه، ولكن في حالة مثالنا سننفذ هذا الملف لمرّة واحدة فقط، إلّا أنّك قد ترغب مُستقبلًا بتنفيذه مُجدّدًا بغية تفريغ قاعدة البيانات من كافّة محتوياتها والبدء من جديد بقاعدة بيانات فارغة. بينما ينشئ الأمر الثاني من أوامر SQL وهو: CREATE TABLE posts جدولًا باسم "posts" له الأعمدة التالية: 'id': ويحتوي على بياناتٍ من نوع رقم صحيح ويمثّل مفتاحًا أساسيًا يحتوي على قيمةٍ فريدة في قاعدة البيانات من أجل كل سجل (والسجل هو التدوينة في حالتنا). أمّا الأمر AUTOINCREMENT فيعمل على زيادة قيمة المُعرّف "ID" للتدوينات تلقائيًا، بمعنى أنّ قيمة المعرّف ID للتدوينة الأولى ستكون "1"، وستكون للتدوينة المُضافة بعدها تلقائيًا "2"، وهكذا، وستحافظ كل تدوينة على رقم معرّفها حتى في حال حذف تدوينات سابقة أو لاحقة لها. 'created': يحتوي على تاريخ ووقت إنشاء التدوينة، وتشير NOT NULL إلى أن هذا العمود يجب ألا يحتوي على قيمٍ فارغة، أما القيمة الافتراضية فهي CURRENT_TIMESTAMP، والتي تمثِّل تاريخ ووقت إضافة التدوينة إلى قاعدة البيانات، وكما هو الحال في عمود id، لا يتوجب عليك تحديد قيم لهذا العمود، إذ أنها تُملأ تلقائيًا. title: عنوان التدوينة. content: محتوى التدوينة. والآن سنستخدم ملف تخطيط قاعدة البيانات "schema.sql" لإنشاء قاعدة البيانات، لذا سنُنشئ ملف بايثون سيبني بدوره ملف قاعدة بيانات SQLite بلاحقة db. اعتمادًا على الملف schema.sql. افتح الملف "init_db.py" ضمن المجلد "flask_app" لتحريره (باستخدام محرِّر النصوص المفضل لديك، وهو محرر نانو nano هنا): $ nano init_db.py ثم اكتب ضمنه الشيفرة التالية: import sqlite3 connection = sqlite3.connect('database.db') with open('schema.sql') as f: connection.executescript(f.read()) cur = connection.cursor() cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)", ('First Post', 'Content for the first post') ) cur.execute("INSERT INTO posts (title, content) VALUES (?, ?)", ('Second Post', 'Content for the second post') ) connection.commit() connection.close() استوردنا في الشيفرة السابقة وحدة sqlite3، ثم أنشأنا اتصالًا مع ملف قاعدة بيانات باسم "database.db"، والذي يُنشأ تلقائيًا فور تشغيل ملف بايثون هذا، ومن ثم استخدمنا الدالة ()open لفتح الملف schema.sql، ثم نفّذنا باستخدام التابع ()executescript محتويات هذا الملف، الذي ينفِّذ عدة عبارات SQL معًا، وهكذا يُنشأ الجدول "posts"، كما استخدمنا كائن المؤشر Cursor، الذي يمكِّننا من معالجة السجلات، وفي حالتنا استخدمنا تابعه ()execute لتنفيذ تعليمتي إدخال INSERT في SQL لإضافة تدوينتين معًا إلى جدول التدوينات "posts"، وفي النهاية أُرسلت هذه الأوامر إلى قاعدة البيانات وأُغلِق الاتصال المفتوح معها. اِحفظ الملف وأغلقه، ثم شغِّله من موجّه الأوامر باستخدام أمر بايثون التالي: $ python init_db.py وحالما ينتهي التنفيذ، سيكون لديك ملفٌ جديدٌ باسم "database.db" ضمن مجلد "flask_app"، وهذا يدل على نجاح عملية إعداد قاعدة البيانات. سنعمل في الخطوة التالية على إنشاء تطبيق فلاسك مُصغّر لقراءة التدوينات المُضافة إلى قاعدة البيانات وعرضها في صفحته الرئيسية. الخطوة 2 – عرض كل التدوينات سنعمل في هذه الخطوة على إنشاء تطبيق فلاسك ذو صفحة رئيسية مسؤولة عن عرض التدوينات الموجودة في قاعدة البيانات. بعد تفعيل البيئة البرمجية وتثبيت فلاسك، سنفتح ملفًا باسم "app.py" ضمن المجلد "flask_app" لتحريره: (env)user@localhost:$ nano app.py سيعمل هذا الملف على إعداد الاتصال مع قاعدة البيانات وإنشاء وجهة فلاسك وحيدة قادرة على استخدام هذا الاتصال، لذا سنكتب ضمنه الشيفرات التالية: import sqlite3 from flask import Flask, render_template app = Flask(__name__) def get_db_connection(): conn = sqlite3.connect('database.db') conn.row_factory = sqlite3.Row return conn @app.route('/') def index(): conn = get_db_connection() posts = conn.execute('SELECT * FROM posts').fetchall() conn.close() return render_template('index.html', posts=posts) نحفظ الملف ونغلقه. استوردنا بدايةً في الشيفرة السابقة وحدة sqlite3 لاستخدامها في إنشاء الاتصال مع قاعدة البيانات، ومن ثمّ استوردنا كل من الصنف Flask ودالة تصيير القوالب ()render_template من حزمة فلاسك، كما أنشأنا نسخةً فعليةً من التطبيق باسم app، ومن ثمّ عرفنا دالةً باسم ()get_db_connection لإنشاء اتصال مع ملف قاعدة البيانات "database.db" المُنشأ مُسبقًا، وتحدّد بعد ذلك قيمة السمة row_factory لتكون sqlite3.Row لنتمكّن من الوصول إلى الأعمدة باستخدام أسمائها، ما يعني أنّ اتصال قاعدة البيانات سيعيد سجلات يمكننا التعامل معها كما هو الحال مع قواميس بايثون النمطية، ونهايةً تعيد الدالة كائن الاتصال conn، الذي سنستخدمه للوصول إلى قاعدة البيانات. استخدمنا بعد ذلك المُزخرف ()app.route لإنشاء دالة عرض view في فلاسك باسم ()index، وبعدها نفتح اتصالًا مع قاعدة البيانات باستخدام الدالة get_db_connection() التي عرفناها للتو. إذ سننفذ استعلام SQL للحصول على كل المدخلات الموجودة في الجدول "posts"، وسنستدعي التابع fetchall() لجلب كل الأسطر الناتجة عن الاستعلام، وهذا سيعيد بالنتيجة قائمةً بالتدوينات المُدخلة إلى قاعدة البيانات في الخطوة السابقة. يمكنك إغلاق الاتصال بقاعدة البيانات باستخدام التابع close() وإعادة نتيجة تصيير القالب index.html، كما يمكنك تمرير الكائن posts وسيطًا، فهو الكائن الحاوي على النتائج المُستخلصة من قاعدة البيانات، ويساعدنا هذا التمرير على الوصول إلى التدوينات برمجيًا داخل قالب "index.html". الآن وبغية عرض التدوينات المُخزنة ضمن قاعدة البيانات في الصفحة الرئيسية للتطبيق، سننشئ بدايةً ملف قالب ليتضمّن كافة شيفرات HTML الأساسية اللازمة لترثها لاحقًا القوالب الأُخرى ما يجنبنا تكرار الشيفرات، ومن ثمّ سننشئ ملف قالب الصفحة الرئيسية index.html المُصيّر أصلًا باستخدام الدالة ()index. لذا سننشئ مجلدًا للقوالب باسم "templates"، وسننشئ ضمنه ملف قالب باسم "base.html"، والذي سيمثّل القالب الأساسي لبقية القوالب، كما يلي: (env)user@localhost:$ mkdir templates (env)user@localhost:$ nano templates/base.html وسنكتب فيه الشيفرة التالية: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %} {% endblock %}- FlaskApp</title> <style> .post { padding: 10px; margin: 5px; background-color: #f3f3f3 } nav a { color: #d64161; font-size: 3em; margin-left: 50px; text-decoration: none; } </style> </head> <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% block content %} {% endblock %} </div> </body> </html> احفظ الملف واغلقه. يتضمّن القالب الأساسي كافّة الشيفرات المتداولة التي سنحتاجها في القوالب الأُخرى. ستُستبدل لاحقًا كتلة العنوان title بعنوان كل صفحة وكتلة المحتوى content بمحتواها، أمّا عن شريط التصفح فسيتضمّن رابطين، الأوّل ينقل المُستخدم إلى الصفحة الرئيسية للتطبيق باستخدام الدالة المساعدة ()url_for لتحقيق الربط مع دالة العرض ()index، والآخر لصفحة المعلومات حول التطبيق في حال قررت تضمينها في تطبيقك. الآن، سننشئ ملف قالب باسم "index.html" وهو الاسم الذي حددناه في الملف "app.py": (env)user@localhost:$ nano templates/index.html ونضيف ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1>{% block title %} Posts {% endblock %}</h1> {% for post in posts %} <div class='post'> <p>{{ post['created'] }}</p> <h2>{{ post['title'] }}</h2> <p>{{ post['content'] }}</p> </div> {% endfor %} {% endblock %} نحفظ الملف ونغلقه. اعتمدنا في الشيفرة السابقة على الوراثة من ملف القالب "base.html" من خلال تعليمة extends، واستبدلنا محتوى كتلة المحتوى content مُستخدمين تنسيق عنوان من المستوى الأوّل <h1> الذي يفي أيضًا بالغرض ويكون عنوانًا للصفحة. استخدمنا في السطر البرمجي {% for post in posts %} حلقة for من تعليمات محرّك القوالب جينجا jinja، والهدف من استخدام هذه الحلقة هو المرور على كل عنصر في القائمة posts، وبما أنّ كل عنصر (تدوينة post) في القائمة سيكون مشابهًا لما هو عليه في قاموس بايثون، فمن الممكن الوصول إلى تاريخ إنشاء التدوينة باستخدام الأمر {{ post['created'] }}، وإلى عنوانها من خلال post['title'] }} }}، وإلى محتواها من خلال {{ post['content'] }}. الآن ومع وجودنا ضمن المجلد "flask_app" ومع كون البيئة الافتراضية مُفعّلة، سنُعلم فلاسك بالتطبيق المراد تشغيله (وهو في حالتنا الملف app.py) باستخدام متغير البيئة FLASK_APP على النحو التالي: (env)user@localhost:$ export FLASK_APP=app نضبط متغير البيئة FLASK_ENV على القيمة development لتشغيل التطبيق في وضع التطوير مع تشغيل مُنقّح الأخطاء، ولمزيدٍ من المعلومات حول مُنقّح الأخطاء في فلاسك ننصحك بقراءة المقال كيفية التعامل مع الأخطاء في تطبيقات فلاسك، ولتنفيذ ما سبق سنشغّل الأوامر التالية (مع ملاحظة أنّنا نستخدم الأمر set في بيئة ويندوز عوضًا عن الأمر export? (env)user@localhost:$ export FLASK_ENV=development والآن سنشغّل التطبيق باستخدام الأمر flask run: (env)user@localhost:$ flask run وبعد التأكد من كون خادم التطوير ما يزال قيد التشغيل، نذهب إلى الرابط التالي باستخدام المتصفح: http://127.0.0.1:5000/ وعندها ستظهر لك التدوينتان المُضافتان سابقًا إلى قاعدة البيانات بالشّكل التالي: ومع نهاية هذه الخطوة نكون قد تمكنّا من عرض التدوينات المُخزنة ضمن قاعدة البيانات مُسبقًا في الصفحة الرئيسية للتطبيق، أمّا في الخطوة التالية فسنعمل على إضافة وجهة جديدة من شأنها السماح للمُستخدمين بإضافة تدوينات جديدة إلى قاعدة البيانات. الخطوة 3 - إنشاء تدوينات سنضيف في هذه الخطوة وجهة جديدة للسماح للمُستخدمين بإضافة تدوينات جديدة إلى قاعدة البيانات، لتظهر لاحقًا ضمن الصفحة الرئيسية للتطبيق، إذ سنضيف للتطبيق صفحةً جديدةً تتضمّن نموذج ويب لإدخال كل من عنوان ومحتوى التدوينة من قبل المستخدم، وسيجري التحقّق من هذه المُدخلات للتأكد من عدم إرسال المُستخدم نموذجًا فارغًا، وفي حال وجود خطأ ما في البيانات المُدخلة، سيُعلم المُستخدم بالخطأ الحاصل من خلال الرسائل الخاطفة التي تُعرض لمرة واحدة وتختفي بمجرّد إنشاء أي طلب جديد (مثل الانتقال إلى صفحة أُخرى من التطبيق). لذلك، سنفتح نافذة أسطر أوامر جديدة مع بقاء خادم التطوير قيد التشغيل، ثمّ سنفتح الملف "app.py": (env)user@localhost:$ nano app.py للتعامل مع نموذج الويب، لا بُد أولًا من استيراد التالي من حزمة فلاسك: الكائن request العام المسؤول عن الوصول إلى بيانات الطلب والتي ستُرسل من خلال نموذج HTML. الدالة url_for() لتوليد عناوين الروابط. الدالة flash() لعرض رسالةٍ خاطفة في حال ورود طلب غير صحيح. الدالة redirect() لإعادة توجيه المستخدم إلى صفحة التطبيق الرئيسية بعد إضافة التدوينات الجديدة إلى قاعدة البيانات. أضِف هذه الاستيرادات إلى السطر الأوّل من ملف "app.py" كما يلي: from flask import Flask, render_template, request, url_for, flash, redirect # ... تخزّن الدالة flash() الرسائل في جلسة المتصفح لدى المستخدم، وهذا ما يتطلّب إعداد مفتاح أمان Secret Key، إذ سيُستخدم هذا المفتاح لجعل الجلسات آمنة، وبذلك يتمكّن فلاسك من الحفاظ على المعلومات عند الانتقال من طلبٍ إلى آخر، ولا يجب أن تسمح لأي أحدٍ بالوصول إلى مفتاح الأمان الخاص بك. لإعداد مفتاح أمان، سنضيف ضبط SECRET_KEY إلى التطبيق من خلال الكائن app.config، الذي سنضيفه مباشرةً بعد تعريف الكائن app على النحو التالي: # ... app = Flask(__name__) app.config['SECRET_KEY'] = 'your secret key' تذكّر أنّ مفتاح الأمان يجب أن يكون سلسلةً نصيةً عشوائية بطولٍ مناسب، وللمزيد حول نماذج الويب وضبط مفتاح الأمان الخاص به، ننصحك بقراءة المقال كيفية استخدام نماذج الويب في تطبيقات فلاسك. بعد ذلك، سنضيف الوجهة التالية إلى نهاية الملف app.py : # ... @app.route('/create/', methods=('GET', 'POST')) def create(): return render_template('create.html') نحفظ الملف ونغلقه. نمرّر في هذه الوجهة صفًا tuple يحتوي على القيم ('GET', 'POST') إلى المعامل methods بغية السماح بكلا نوعي طلبيات HTTP وهما GET و POST؛ إذ تتخصّص الطلبيات من النوع GET بجلب البيانات من الخادم؛ أمّا الطلبيات من النوع POST فهي مُتخصّصة بإرسال البيانات إلى وجهة مُحدّدة، مع ملاحظة أنّ الطلبيات من النوع GET هي الوحيدة المسموحة افتراضيًا، وحالما يطلب المستخدم الوجهة create/ باستخدام طلبية من النوع GET، سيُصيّر ملف قالب باسم "create.html". سنعدّل هذه الوجهة لاحقًا لتتعامل أيضًا مع الطلبيات من نوع POST اللازمة لدى ملء المُستخدمين للنماذج وإرسالها بغية إنشاء تدوينات جديدة. والآن، افتح ملف "create.html" الجديد داخل مجلد القوالب "templates" على النحو التالي: (env)user@localhost:$ nano templates/create.html واكتب ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1>{% block title %} Add a New Post {% endblock %}</h1> <form method="post"> <label for="title">Title</label> <br> <input type="text" name="title" placeholder="Post title" value="{{ request.form['title'] }}"></input> <br> <label for="content">Post Content</label> <br> <textarea name="content" placeholder="Post content" rows="15" cols="60" >{{ request.form['content'] }}</textarea> <br> <button type="submit">Submit</button> </form> {% endblock %} احفظ الملف واغلقه. وبذلك نكون قد ورثنا خصائص القالب "base.html"، واستخدمنا الوسم <form> وفيه ضبطنا سمة نوع طلبية HTTP لتكون من النوع POST ما يعني أنّ نموذج الويب هذا سيرسل طلب من النوع POST، كما أضفنا صندوق نصي باسم title لنستخدمه في الوصول إلى بيانات عنوان التدوينة في الوجهة create/، وضبطنا القيمة داخل الصندوق النصي هذا الخاص بعنوان التدوينة إلى {{ request.form['title'] }} والتي قد تكون فارغةً، أو نسخةً محفوظةً مؤقتًا من العنوان في حال كون النموذج المرسل خاطئًا، وهذا ما يحافظ على البيانات المدخلة في الأداة عند حدوث خطأ ما. أضفنا بعد الصندوق النصي المُخصّص للعنوان حقلًا نصيًا مُتعدّد الأسطر مُخصّص لمحتوى التدوينة باسم content والقيمة داخله هي {{ request.form['content'] }} للحفاظ على البيانات المدخلة فيه في حال كون النموذج المرسل خاطئًا، لأنّ المعلومات المدخلة سيُحتفظ بها في الكائن العام request، ونهايةً أضفنا إلى نهاية النموذج زرًا لتأكيد إرساله. الآن وأثناء عمل خادم التطوير، استخدم المتصفح للانتقال إلى الوجهة /create: http://127.0.0.1:5000/create فستظهر لك صفحة إنشاء تدوينة جديدة مع صندوق نصي لإدخال العنوان، وحقل نصي مُتعدّد الأسطر لإدخال المحتوى، وزر لتأكيد إرسال النموذج Submit، كما في الشّكل التالي: يرسل نموذج الإدخال هذا طلبًا من النوع POST إلى الخادم، ولكن حتى هذه اللحظة لا يوجد شيفرة مسؤولة عن معالجة هذا الطلب في الوجهة create/، وبالتالي لن يحدث شيء في حال ملء النموذج الآن وإرساله. لذا، فيما يلي ستعالج الدالة create() الطلبات الواردة بطريقة POST عند إرسال محتويات نموذج الإدخال وذلك بعد التحقق من قيمة تابع الطلب request.method؛ فإذا كانت قيمته 'POST'، تستمر بمتابعة قراءة البيانات المرسلة والتحقق منها وإدخالها في قاعدة البيانات. الآن، افتح الملف "app.py" بهدف تعديله ليتعامل مع الطلبات من النوع POST المُرسلة من قبل المستخدم: (env)user@localhost:$ nano app.py ونعدّل الوجهة create/ لتصبح كما يلي: # ... @app.route('/create/', methods=('GET', 'POST')) def create(): if request.method == 'POST': title = request.form['title'] content = request.form['content'] if not title: flash('Title is required!') elif not content: flash('Content is required!') else: conn = get_db_connection() conn.execute('INSERT INTO posts (title, content) VALUES (?, ?)', (title, content)) conn.commit() conn.close() return redirect(url_for('index')) return render_template('create.html') احفظ الملف واغلقه. تَمكَّنا باستخدام العبارة الشرطية if request.method == 'POST' التي تقارن قيمة request.method مع القيمة POST من التحقُّق بأنّ التعليمات التالية لها لن تُنفّذ إلّا إذا كان الطلب الحالي هو فعلًا بطريقة POST، ومن ثمّ قرأنا قيم العنوان والمحتوى المرسلين من الكائن request.form، الذي يمكِّننا من الوصول إلى بيانات نموذج الإدخال المُضمّنة في الطلب. في حال عدم إدخال قيمةٍ للعنوان ستظهر رسالة خاطفة للمستخدم نعلمه من خلالها باستخدام الدالة ()flash بأن العنوان مطلوب !Title is required، وسيحدث الأمر ذاته في حال عدم ملء حقل المحتوى؛ أمّا في حال وجود كل من العنوان والمحتوى، فسيُفتَح اتصال مع قاعدة البيانات باستخدام الدالة get_db_connection()، وسيُنفّذ الأمر INSERT INTO من أوامر SQL باستخدام التابع ()execute وذلك لإضافة التدوينة الجديدة إلى جدول التدوينات "posts" في قاعدة البيانات وفق العنوان والمحتوى المُدخلان أصلًا من قِبل المُستخدم في النموذج. استخدمنا الموضع المؤقت ? بما يضمن إدخال البيانات في الجدول بأمان، ثمّ حفظنا التغييرات باستخدام الدالة ()connection.commit، وأُغلق الاتصال مع قاعدة البيانات باستخدام الدالة ()connection.close، ونهايةً أعدنا توجيه المًستخدم إلى الصفحة الرئيسية من التطبيق ليرى تدوينته الجديدة أسفل التدوينات السابقة الموجودة أصلًا. تنبيه: لا تستخدم عمليات بايثون المحرفية بغية إنشاء سلسلة تعليمات SQL ديناميكيًا، بل استخدم دائمًا الموضع المؤقت ? في تعليمات SQL لتعويض القيم ديناميكيًا، فمن الممكن تمرير صف tuple من القيم مثل وسيط ثانٍ ضمن التابع ()execute لربط هذه القيم مع تعليمة SQL، وهذا ما يمنع هجمات حقن استعلامات SQL (إحدى أكثر طرق الاختراق خطرًا على كل من المواقع والأنظمة، وتتضمن هذه الطريقة إدخال استعلامات SQL في حقول الإدخال). الآن وأثناء عمل خادم التطوير، استخدم المتصفح للانتقال إلى الوجهة /create باستخدام الرابط: http://127.0.0.1:5000/create املأ النموذج بالعنوان والمحتوى الذي تريد، وحال إرسال النموذج سيُعاد توجيهك إلى الصفحة الرئيسية للتطبيق، حيث سترى التدوينة الجديدة فيها؛ أمّا في حال إرسال النموذج مع حقل عنوان أو محتوى فارغ، فلن تُضاف التدوينة إلى قاعدة البيانات، ولن يُعاد التوجيه إلى الصفحة الرئيسية للتطبيق، وكل ذلك دون الحصول على أي معلومات حول ماهية ما حصل وسببه، وذلك لأننا لم نُعدّ الرسائل الخاطفة للعرض حتى الآن. لذلك، سنفتح الآن ملف القالب الأساسي base.html لإضافة رابط في شريط التصفح للوصول إلى صفحة إنشاء تدوينة جديدة، ولإظهار الرسائل الخاطفة أسفله: (env)user@localhost:$ nano templates/base.html ونعدل الملف ليصبح كما يلي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %} {% endblock %} - FlaskApp</title> <style> .post { padding: 10px; margin: 5px; background-color: #f3f3f3 } nav a { color: #d64161; font-size: 3em; margin-left: 50px; text-decoration: none; } .alert { padding: 20px; margin: 5px; color: #970020; background-color: #ffd5de; } </style> </head> <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="{{ url_for('create') }}">Create</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% for message in get_flashed_messages() %} <div class="alert">{{ message }}</div> {% endfor %} {% block content %} {% endblock %} </div> </body> </html> نحفظ الملف ونغلقه. أضفنا في الشيفرة السابقة وسم رابط <a> جديد إلى شريط التصفُّح والذي يشير إلى صفحة إنشاء تدوينة جديدة. استخدمنا حلقة for التكرارية من تعليمات جينجا للمرور على الرسائل الخاطفة، إذ يوفّر فلاسك هذه الرسائل من خلال دالة فلاسك الخاصة get_flashed_messages() لتُعرض ضمن وسم <div> ذو صنف CSS يدعى alert، ونسقنا مظهر الوسم <div> من خلال الوسم <style> المتوضّع في قسم الترويسة <head>. سيظهر الرابط الجديد ضمن شريط التصفُّح عند تحديث الصفحة الرئيسية للتطبيق، وسينقلنا النقر على هذا الرابط الجديد إلى صفحة إنشاء تدوينة جديدة، فلو أرسلنا في هذه المرحلة نموذجًا فارغًا، ستظهر رسالة خاطفة تعلمنا بأنّ حقل العنوان مطلوب "!Title is required". ولو أرسلنا بعد ذلك نموذجًا بعنوان ما وحقل محتوى فارغ، فستظهر رسالة خاطفة لتعلمنا بكون حقل المحتوى مطلوب "!Content is required"، وعندها تختفي الرسالة "!Title is required" السابقة، لأنها رسالة خاطفة وليست دائمة. ومع نهاية هذه الخطوة أصبح لدينا آلية لإضافة التدوينات الجديدة، وسنعمل في الخطوة التالية على إضافة وجهة جديدة لتعديل التدوينات الموجودة أصلًا. الخطوة 4 - تعديل تدوينات سنعمل في هذه الخطوة على إضافة وجهة جديدة للتطبيق لتُمكّن المُستخدمين من تعديل التدوينات المنشورة أصلًا. بدايةً، وبغية تجنّب تكرار الشيفرات وعزلها الأمر الذي يجعل التعامل معها وإصلاحها في حال حدوث أخطاء أسهل، سنضيف دالة جديدة بحيث نمرر لها معرّف التدوينة المراد تعديلها لتعيد بيانات التدوينة الموافقة من قاعدة البيانات، كما سنستخدم هذه الدالة في الخطوة التالية لحذف تدوينة ما. لذا، افتح الملف "app.py": (env)user@localhost:$ nano app.py ستعيد الدالة الجديدة التي سنستخدمها في جلب التدوينات المطلوبة من قاعدة البيانات خطأ HTTP من النوع "404" أي "404 Not Found" في حال كون المعرّف المطلوب لا يوافق أي من معرفات التدوينات الموجودة في قاعدة البيانات، ولإنجاز ذلك سنستعين بالدالة ()abort التي تُلغي الطلب الخاطئ مُستجيبةً برسالة خطأ. لذا سنضيف الدالة ()abort إلى مجموعة الاستيرادات في بداية الملف: from flask import Flask, render_template, request, url_for, flash, redirect, abort سنضيف بعد الدالة ()get_db_connection دالة جديدة باسم ()get_post، كما يلي: # ... def get_db_connection(): conn = sqlite3.connect('database.db') conn.row_factory = sqlite3.Row return conn def get_post(post_id): conn = get_db_connection() post = conn.execute('SELECT * FROM posts WHERE id = ?', (post_id,)).fetchone() conn.close() if post is None: abort(404) return post # ... تمتلك هذه الدالة الجديدة وسيطًا post_id، والذي نحدّد من خلاله معّرف ID التدوينة المراد جلبها مثل قيمة معادة من قاعدة البيانات، لذا تعمل الدالة ()get_post على فتح اتصال مع قاعدة البيانات باستخدام الدالة ()get_db_connection لتنفّذ فيها استعلامًا للوصول إلى التدوينة الموافقة لقيمة المُعرّف المُمرّر أصلًا إلى الوسيط post_id، إذ تُجلب التدوينة باستخدام التابع ()fetchone لتُخزّن في المتغير post، ومن ثمّ يُغلق الاتصال مع قاعدة البيانات. إذا كانت قيمة المتغير post فارغة أي تساوي "None"، فهذا يعني أنّه لم يوجد أي نتيجة موافقة في قاعدة البيانات، وعندها تُستخدم الدالة ()abort التي استوردناها سابقًا للاستجابة برسالة خطأ HTTP من النوع "404"، ثمّ إيقاف التنفيذ؛ أمّا في حال إيجاد معرّف التدوينة المطلوبة ضمن قاعدة البيانات فتُعاد قيمة المتغير post. والآن سنضيف وجهة جديدة مُخصّصة لتعديل التدوينات إلى نهاية الملف "app.py"، كما يلي: # ... @app.route('/<int:id>/edit/', methods=('GET', 'POST')) def edit(id): post = get_post(id) if request.method == 'POST': title = request.form['title'] content = request.form['content'] if not title: flash('Title is required!') elif not content: flash('Content is required!') else: conn = get_db_connection() conn.execute('UPDATE posts SET title = ?, content = ?' ' WHERE id = ?', (title, content, id)) conn.commit() conn.close() return redirect(url_for('index')) return render_template('edit.html', post=post) ثمّ نحفظ الملف ونغلقه. سنستخدم الوجهة الجديدة بالشكل /int:id>/edit>/، إذ يمثّل :int محوّلًا لنمط البيانات، ويقبل قيمًا عددية صحيحة موجبة فقط، أما id فيمثّل الجزء المتغير من الرابط المطلوب، والذي يدل على التدوينة المراد تعديلها، فمثلًا استخدام الوجهة (ضمن الرابط) بالشكل /2/edit/ يعني أنّنا نود تعديل التدوينة ذات معرّف ID المساوي "2"، إذ يُمرّر المعرّف هذا من الرابط إلى دالة العرض ()edit، لتُمرر فيها قيمة الوسيط id إلى الدالة ()get_post لجلب التدوينة الموافقة لهذا المعرّف من قاعدة البيانات، مع ملاحظة أنّه في حال عدم وجود تدوينة موافقة للمعرّف المطلوب، ستكون الاستجابة برسالة خطأ "404 Not Found". أمّا السطر الأخير من الشيفرة السابقة فيصيّر ملف قالب باسم edit.html مُمرّرًا إليه المتغير post المُتضمّن بيانات التدوينة المراد تعديلها، وهذا القالب مسؤول عن عرض عنوان ومحتوى التدوينة الحاليين (قبل التعديل) ضمن صفحة التعديل. وعلى نحوٍ مشابه لحالة إنشاء تدوينة جديدة فإن الجملة الشرطية 'if request.method == 'POST مسؤولة عن التعامل مع البيانات الجديدة التي يرسلها المُستخدم، فبعد تحقّق الشرط نستخلص من النموذج العنوان والمحتوى الجديدين مع إظهار رسالة خاطفة في حال كون أحدهما فارغ. في حال كون النموذج المرسل صحيحًا، نفتح اتصالًا مع قاعدة البيانات ونحدّث الجدول "posts" بالعنوان والمحتوى الجديدين للتدوينة ذات المعرّف المساوي لذلك المحدَّد في الرابط المطلوب باستخدام التعليمة UPDATE من تعليمات SQL، ومن ثمّ نؤكّد التغييرات ونغلق الاتصال مع قاعدة البيانات ونعيد توجيه المستخدم إلى الصفحة الرئيسية للتطبيق. أمّا الآن فعلينا إنشاء الصفحة التي سيعدّل المستخدمون التدوينات ضمنها، لذا سنفتح ملف قالب جديد باسم "edit.html": (env)user@localhost:$ nano templates/edit.html ونكتب ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1>{% block title %} Edit "{{ post['title'] }}" {% endblock %}</h1> <form method="post"> <label for="title">Title</label> <br> <input type="text" name="title" placeholder="Post title" value="{{ request.form['title'] or post['title'] }}"></input> <br> <label for="content">Post Content</label> <br> <textarea name="content" placeholder="Post content" rows="15" cols="60" >{{ request.form['content'] or post['content'] }}</textarea> <br> <button type="submit">Submit</button> </form> {% endblock %} نحفظ الملف ونغلقه. الشيفرة السابقة مُشابهة لتلك في القالب "create.html" ما عدا أنها تعمل على إظهار عنوان التدوينة مثل عنوان للصفحة من خلال التعليمة: {% block title %} Edit "{{ post['title'] }}" {% endblock %} وتجعل القيمة داخل الصندوق النصي بالشّكل: {{ request.form['title'] or post['title'] }} والقيمة داخل الحقل النصي مُتعدّد الأسطر على النحو التالي: {{ request.form['content'] or post['content'] }} وبذلك ستظهر البيانات المًخزّنة في الطلب في حال وجودها، وإلّا ستظهر البيانات الموجودة في المتغير post المُمرّر إلى القالب الذي يحتوي على البيانات الحالية لقاعدة البيانات. الآن وفي حين خادم التطوير يعمل، استخدم المتصفح للانتقال إلى الرابط التالي بغية تعديل التدوينة الأولى`: http://127.0.0.1:5000/1/edit فتظهر صفحة بالشّكل: وبتعديل التدوينة وإرسال النموذج ستظهر التغييرات في الصفحة الرئيسية للتطبيق، وفي حال إرسال النموذج بعنوان أو محتوى فارغ، ستظهر رسالة خاطفة لإعلامنا بالخطأ الحاصل. أمّا الآن فسنعمل على إضافة رابط للوصول إلى صفحة التعديل لكل تدوينة من الصفحة الرئيسية للتطبيق، لذا نفتح ملف القالب index.html: (env)user@localhost:$ nano templates/index.html ونعدله ليصبح تمامًا على النحو التالي: {% extends 'base.html' %} {% block content %} <h1>{% block title %} Posts {% endblock %}</h1> {% for post in posts %} <div class='post'> <p>{{ post['created'] }}</p> <h2>{{ post['title'] }}</h2> <p>{{ post['content'] }}</p> <a href="{{ url_for('edit', id=post['id']) }}">Edit</a> </div> {% endfor %} {% endblock %} نحفظ الملف ونغلقه. أضفنا الوسم <a> للربط مع دالة العرض ()edit، مُمرّرين معّرف التدوينة ID الموجود في post['id']) إلى الدالة ()url_for لتُنشئ رابط تعديل التدوينة الموافقة، وهذا ما سيضيف بالنتيجة رابطًا إلى صفحة التعديل من أجل كل تدوينة. الآن، وبعد تحديث الصفحة الرئيسية للتطبيق يصبح من الممكن تعديل أي تدوينة بالنقر على رابط التعديل Edit الخاص بها. ومع نهاية هذه الخطوة أصبح بإمكان مستخدمي التطبيق إضافة تدوينات جديدة وتعديل التعديلات الموجودة أصلًا، وسنعمل في الخطوة التالية على إضافة زر لتمكين المستخدمين من حذف تدوينة ما. الخطوة 5 - حذف تدوينات سنعمل في هذه الخطوة على إضافة زر حذف إلى صفحة التعديل بحيث يسمح للمستخدمين بحذف تدوينة ما. بدايةً، سنضيف وجهةً جديدةً للحذف، وهي /id/delete تتعامل مع الطلبات من النوع POST بآلية مشابهة لتلك في دالة العرض edit()، إذ ستستقبل دالة الحذف الجديدة delete() رقم معرّف التدوينة ID المُراد حذفها من خلال الرابط URL ليجري جلبها باستخدام الدالة ()get_post ومن ثمّ حذفها من قاعدة البيانات في حال وجودها. الآن، افتح الملف "app.py": (env)user@localhost:$ nano app.py أضِف الوجهة التالية إلى نهاية الملف: # ... @app.route('/<int:id>/delete/', methods=('POST',)) def delete(id): post = get_post(id) conn = get_db_connection() conn.execute('DELETE FROM posts WHERE id = ?', (id,)) conn.commit() conn.close() flash('"{}" was successfully deleted!'.format(post['title'])) return redirect(url_for('index')) احفظ الملف واغلقه. تتعامل هذه الدالة فقط مع الطلبات الواردة بطريقة POST وفق ما جرى تحديده في المعامل methods المسؤول عن تحديد أنواع طلبيات HTTP المسموحة؛ وهذا يعني أنك إذا انتقلت إلى الوجهة /ID/delete في المتصفح، ستحصل على خطأ "405 Method Not Allowed"، لأن المتصفحات تستخدم طريقة GET افتراضيًا للطلبات؛ لذا أضفنا زرًا لحذف التدوينات كونه يُرسل إلى الوجهة طلبًا من النوع POST. تستقبل الدالة قيمة المعرّف ID للتدوينة المراد حذفها وتستخدمها لجلب التدوينة من قاعدة البيانات باستخدام دالة get_post()، لتستجيب بخطأ من النوع 404 بالرسالة "404 Not Found" في حال عدم وجود تدوينة موافقة، وإلّا يُفتح اتصال مع قاعدة البيانات وتُنفّذ تعليمة DELETE FROM من تعليمات SQL لحذف التدوينة، إذ استخدمنا التعبير WHERE id = ? للدلالة على التدوينة المراد حذفها. نهايةً، يُؤكَّد على التعديلات وتُرسل إلى قاعدة البيانات ويُغلق الاتصال لتظهر رسالةٌ خاطفةٌ تُعلم المستخدم بانتهاء عملية حذف التدوينة بنجاح، ثم يُعاد توجيه المستخدم إلى الصفحة الرئيسية. لاحظ أننا هنا لا نصيّر ملف قالب، وإنمّا نضيف فقط زر أوامر "Delete" إلى صفحة تعديل التدوينة. الآن افتح ملف القالب edit.html: (env)user@localhost:$ nano templates/edit.html ثم أضف وسم النموذج <form> بعد وسم إظهار الفاصل الأفقي <hr> تماماً بعد السطر البرمجي {% endblock %} على النحو التالي: <button type="submit">Submit</button> </form> <hr> <form action="{{ url_for('delete', id=post['id']) }}" method="POST"> <input type="submit" value="Delete Post" onclick="return confirm('Are you sure you want to delete this post?')"> </form> {% endblock %} احفظ الملف واغلقه. أنشأنا في الملف السابق نموذج ويب يُرسل طلبًا من النوع POST إلى دالة العرض ()delete ممررين القيمة ['post['id لتحديد التدوينة المراد حذفها، كما استخدمنا التابع confirm() المتوفّر في متصفحات الويب لعرض رسالة تأكيد قبل إرسال الطلب. الآن انتقل إلى صفحة تعديل تدوينة في المتصفح مجددًا وجرّب حذفها: http://127.0.0.1:5000/1/edit بعد تأكيد الحذف سيُعاد توجيهك إلى الصفحة الرئيسية للتطبيق، حيث ستختفي التدوينة المحذوفة، وستظهر رسالة خاطفة أسفل شريط التصفّح لإعلامك بنجاح عملية حذف التدوينة، وبذلك يكون أصبح لدينا آليةً في تطبيق فلاسك هذا لحذف التدوينات غير المرغوب بها. الخاتمة أنشأنا في هذا المقال مدونة ويب مصغّرة تتخاطب مع قاعدة بيانات SQLite، مع كامل الوظائف الأساسية من إضافة بيانات جديدة إلى قاعدة البيانات، وجلب بيانات منها لعرضها في صفحات التطبيق، وحذف وتعديل بيانات موجودة أصلًا. ترجمة -وبتصرف- للمقال How To Use an SQLite Database in a Flask Application لصاحبه Abdelhadi Dyouri. اقرأ أيضًا كيف ومتى نستخدم SQLite التعامل مع قواعد البيانات SQLite في تطبيقات Flask كيفية استخدام علاقة نوع واحد-إلى-متعدد one-to-many مع إطار العمل فلاسك Flask ومحرك قواعد البيانات SQLite تعديل العناصر في علاقات من نوع واحد-إلى-متعدد one-to-many باستخدام إطار العمل فلاسك Flask ومحرك قاعدة البيانات SQLite استخدام علاقة متعدد-إلى-متعدد many-to-many مع إطار العمل فلاسك Flask ومحرك قواعد البيانات SQLite
-
تعرفنا في المقال السابق على كيفية التعامل مع رسائل الأخطاء في بايثون وعلى الطرق المساعدة في فهم فحواها وإيجاد مسببات ظهورها، وذلك من خلال الاستعانة بمتتبع الأخطاء لتحديد مصدر الخطأ أو باستخدام منقح صياغة لتلافي وقوع أخطاء ما أمكن، أو اللجوء إلى الإنترنت بحثًا عن تفسير لمضمون رسالة الخطأ. ولكن ماذا لو فشلت هذه المحاولات، أو إن لم تجد في بحر الإنترنت سؤالًا مشابهًا لسؤالك، في هذه الحالة لابد من طرح سؤالك في أحد المنتديات أو ربما عبر إحدى المجموعات البريدية. في حال اضطررت لطرح سؤال جديد، فيجب عليك الالتزام بالعديد من القواعد والأصول لزيادة احتمالية الرد على سؤالك. سيرشدك هذا المقال نحو كيفية طلب المساعدة البرمجية باحترافية. كيفية طلب المساعدة البرمجية عند فشل كل من عمليات البحث على الإنترنت ومنقحات الصياغة في تحديد مشكلة برنامجك وحلها، ما من سبيل أمامك سوى طلب المساعدة البرمجية عبر الإنترنت، إلا أن لطلب النصيحة آداب وأصول ليكون أكثر فعالية، فعليك الاستفادة بكفاءة من وقت مطوري البرمجيات المحترفين كونهم مستعدون للإجابة عن تساؤلاتك بالمجان. ويجب أن يكون هذا الحل هو الأخير دائمًا بالنسبة لك، فقد تمر ساعات أو أيام قبل أن يرد أحد الأشخاص على سؤالك المنشور هذا إن رد أحدهم عليه أصلًا، لذا يبقى البحث في الإنترنت هو الأسرع عبر البحث عن آخرين قد طرحوا نفس سؤالك وقراءة الإجابات التي تلقوها، ومن هنا جاءت فكرة بناء التوثيقات ومحركات البحث، بغية أتمتة عمليات الإجابة عن الأسئلة التي كان يتعين على البشر الإجابة عنها. وفي حال سدت كل السبل في طريقك واستنفذتَ خياراتك بحيث لم يتبقَ أمامك سوى خيار طرح السؤال على جمهور المبرمجين، فتجنب الأخطاء الشائعة التالية: إضاعة الوقت بالسؤال عما إذا كان من المقبول طرح سؤالك بدلًا من طرحه مباشرةً. أن يكون سؤالك ضمنيًا وغير مباشر. طرح السؤال في الموقع الإلكتروني أو المنتدى غير المناسب. أن يكون عنوان السؤال غير محدد من قبيل "لدي مشكلة" أو "أرجو المساعدة". أن تذكر كون برنامجك لا يعمل دون توضيح الطريقة التي تريدها أن يعمل بها. عدم تضمين رسالة الخطأ كاملةً. عدم مشاركة الشيفرة. مشاركة شيفرة رديئة التنسيق. عدم ذكر الخطوات التي قد جربتَها أصلًا. عدم ذكر معلومات نوع نظام التشغيل وإصداره. طلب كتابة برنامج كامل. إن قائمة "أشياء لا يجب فعلها" السابقة ليست بهدف اللباقة فحسب، وإنما لأن فعلها لن يمكّن من يرغب بمساعدتك من تحقيق ذلك، فالخطوة الأولى لأي شخص قد يساعدك تتمثّل في تنفيذ الشيفرة محاولًا إعادة إظهار المشكلة بنفس الطريقة التي واجهتك، ولإتمام ذلك لا بد من توفر الكثير من المعلومات حول شيفرتك وحاسوبك وتطلعاتك حول نتائج البرنامج. فمن الشائع جدًا تقديم معلومات قليلة جدًا حول السؤال المطروح بدلًا من تقديم الكثير منها، وسنستعرض في الأقسام التالية الممارسات الواجب اتباعها لتجنب هذه الأخطاء الشائعة وفيها سنفترض بأنك تنشر أسئلتك في أحد المنتديات البرمجية، إلا أن القواعد نفسها تنطبق في حال طرحها عبر البريد الإلكتروني لشخص واحد أو لقائمة بريدية. قلل من النقاشات غير المجدية بتقديم كافة المعلومات اللازمة مقدما لو افترضنا أنك تطرح على أحدهم سؤالك وجهًا لوجه، فسيكون من المناسب والمريح سؤاله حول إمكانية طرح سؤالك لمعرفة ما إذا كان متوفرًا، ولكن في المنتديات الأمر مختلف، فمن سيساعدك قد يؤجل رده عليك إلى حين توفر الوقت المناسب له، ونظرًا لاحتمالية وجود ساعات ما بين طرح السؤال والحصول على الردود، فمن الأفضل طرح السؤال مباشرة وتوفير كافة المعلومات التي قد يحتاجها من سيساعدك بدلًا من إضاعة الوقت في السؤال عن إمكانية طرح السؤال، وبذلك وفي حال عدم تلقيك لأي ردود، يمكنك ببساطة نسخ معلومات سؤالك كاملًا لطرحها في منتدى أخر. اطرح سؤالك على هيئة سؤال فعلي قد تفترض مجازًا أن مُساعديك على اطلاع حول ما تتحدث عنه لدى شرح مشكلتك، إلا أن البرمجة مجال واسع جدًا وقد لا يكون لديهم الخبرة في الجزئية التي تواجه مشكلة فيها، لذا من المهم طرح سؤالك على هيئة سؤال فعلي، فعلى الرغم من كون الجمل التي تبدأ بعبارات من قبيل "أريد …. " أو "الشيفرة لا تعمل" قد توضح ماهية سؤالك، إلا أنه من الضروري تضمين أسئلة صريحة ومباشرة، أي جمل تنتهي بإشارات استفهام، وإلا وفي حال عدم استخدامها قد لا يتضح تمامًا ما تطلبه. اطرح سؤالك في المكان المناسب غالبًا سيكون من غير المجدي طرح سؤال حول بايثون في منتدى مخصص لجافاسكربت، أو سؤال حول الخوارزميات في قائمة بريدية مخصصة لأمن الشبكات، فغالبًا ما تتضمن المنتديات والقوائم البريدية على مستندات تتضمن الأسئلة الشائعة FAQ أو صفحات وصف لطبيعتها والتي تبين المواضيع المناسبة للنقاش فيها. فعلى سبيل المثال تتمحور القائمة البريدية python-dev حول الميزات التصميمية للغة بايثون ولا تمثل قائمة بريدية عامة للمساعدة حول بايثون بصورة عامة. وستوجهك صفحة المساعدة إلى المكان المناسب وفق طبيعة سؤالك حول بايثون. لخص سؤالك في العنوان لعل من أحد فوائد نشر سؤالك في منتدى على الإنترنت أن المبرمجين المستقبليين ممن سيواجهون نفس المشكلة سيجدون سؤالك وإجابته لدى البحث على الإنترنت، لذا تأكد من اختيارك لعنوان يلخص سؤالك بالفعل الأمر الذي يساعد على تنظيمه في محركات البحث، فالعناوين العامة من قبيل "أرجو المساعدة" أو "لماذا لا يعمل هذا الأمر؟" مبهمة، وكذلك الأمر في حال إرسال بريد إلكتروني، ففي حال اختيارك لعنوان ذو معنى، فهذا سيساعد المتلقي على معرفة فكرة سؤالك بمجرد تصفحه لصندوقه الوارد. وضح الهدف المرجو من شيفرتك سؤال من قبيل "لماذا لا يعمل برنامجي؟" يتجاهل تفاصيل دقيقة حول ما المتوقع من هذا البرنامج فعله أصلًا، فلن يكون هذا بالأمر الجلي دائمًا لمن سيساعدك كونه لا يعرف على وجه التحديد غايتك المرجوة من البرنامج، وحتى إن كان سؤالك من نوع "لماذا يظهر هذا الخطأ لي؟" فمن المفيد أيضًا تحديد الهدف النهائي للبرنامج، ففي بعض الأحيان قد يرشدك المساعد إلى منهجية مختلفة تمامًا في تحقيق غايتك ما يجعلك تتخطى المشكلة تمامًا بدلًا من إضاعة الوقت في محاولة حلها. ضمّن رسالة الخطأ كاملة تأكد دائمًا من نسخ رسالة الخطأ بأكملها مع ملخص متتبّع الأخطاء، فإن وصف الخطأ وحده كقولك "أتلقى خطأ من نوع Out of range" لا يوفر لمن يساعدك تفاصيلًا كافية لاكتشاف ماهية الخطأ، كما يجب عليك تحديد ما إذا كان خطأً عابرًا أم دائمًا، وفي حال كنت قد اكتشفت الظروف التي يحدث عندها الخطأ، فضمّنها أيضًا في سؤالك. شارك شيفرتك كاملة إلى جانب رسالة الخطأ الكاملة وملخص متتبّع الأخطاء، شارك الشيفرة المصدرية لبرنامجك كاملةً، بحيث يتمكن من سيساعدك من تشغيلها على حاسوبه مستخدمًا منقّح أخطاء لاكتشاف ما يحدث، فيجب عليك توفير نسخة من شيفرتك فيها ما يهم من سيساعدك بالكامل بأقل تعقيد وبحيث تكون قابلة للتعديل وإعادة التنفيذ، وهذا ما أطلق عليه موقع Stack Overflow اختصارًا MCR. إذ يرمز الحرف M إلى Minimal بمعنى أن تكون الشيفرة أقصر ما يمكن مع بقائها تسبب المشكلة التي تواجهك، أما الحرف C فيرمز إلى Complete بمعنى أن تكون الشيفرة كاملة ومتضمنة كل ما يلزم لظهور المشكلة، والحرف R فيرمز إلى Reproducible بمعنى أن الشيفرة ستسبب نفس المشكلة التي تصفها عند كل تنفيذ، أما إن كان برنامجك مضمنًا في ملف واحد، فسيكون من السهل إرفاقه بسؤالك بعد التأكد من تنسيقه بطريقة صحيحة. اجعل شيفرتك مقروءة ومفهومة باستخدام التنسيق الملائم إن الهدف الرئيسي من إرفاق الشيفرة الخاصة ببرنامجك مع سؤالك هو السماح لمن سيساعدك بتشغيل هذه الشيفرة للحصول على نفس الخطأ الظاهر لديك، فلا يحتاج هذا الشخص مجرد الشيفرة فحسب، بل يحتاجها منسقة بالشكل الصحيح، فيجب عليك التأكد من كونه قادرًا على نسخها لتكون جاهزة لتشغيلها مباشرةً، فمن الجدير بالملاحظة مثلًا أن العديد من مضيفي البريد الإلكتروني يزيلون المسافات البادئة تلقائيًا، ما يجعل الشيفرة تبدو بالشكل: def knuts(self, value): if not isinstance(value, int) or value < 0: raise WizCoinException('knuts attr must be a positive int') self._knuts = value فالأمر ليس بمجرد الوقت الذي سيستغرقه من سيساعدك في إعادة إدخال المسافات البادئة لكل سطر في شيفرتك، بل وحيرته بشأن مقدارها لكل سطر. وللتأكد من الحفاظ على صحة تنسيق الشيفرة الخاصة ببرنامجك، انسخها إلى أحد مواقع مشاركة الشيفرات مثل https://pastebin.com/ أو https://gist.github.com/ والتي تخزن شيفرتك على هيئة رابط مختصَر وعام، فمثلًا إن أرفقت الشيفرة مع سؤالك على هيئة رابط مثل https://pastebin.com/XeU3yusC سيكون أيسر وأسهل من إرفاقها كملف مرفق. وإذا كنت في صدد نشر شيفرة برنامجك في أحد المواقع الإلكترونية مثل stackoverflow أو قسم الأسئلة في أكاديمية حسوب أو reddit، فتأكد من استخدامك لأدوات التنسيق التي توفرها الصناديق النصية فيها، فمثلًا باستخدامك لمسافة بادئة مكونة من أربعة مسافات سيضمن بالنتيجة استخدام نمط خط ذو تباعد مفرد للشيفرة بالغالب وهو الأسهل للقراءة، كما يمكنك تضمين الشيفرة ما بين علامات اقتباس مائلة (`) لتنسيقها بنمط خط ذو تباعد مفرد. وعادةً ما تتضمن هذه المواقع رابطًا لشرح قواعد التنسيق، والتي بعدم اتباعها ستظهر شيفرتك بشكل غير واضح، كأن تظهر مكتوبة في سطر واحد بالكامل، كما يلي: def knuts(self, value):if not isinstance(value, int) or value < 0:raise WizCoinException('knuts attr must be a positive int') self._knuts = value ومن المهم أيضًا عدم مشاركتك للشيفرة على هيئة صورة أو لقطة شاشة، الأمر الذي يجعل من المستحيل نسخها، ناهيك عن كونه في هذه الحالة -على الغالب- غير مقروء. اذكر في سؤالك جميع المحاولات التي أجريتها عند نشرك لسؤال ما، لا تنسَ إخبار من سيساعدك بما قد حاولت تجريبه كحلول وما كانت نتائج كل محاولة، لما لهذه المعلومات من دور في توفير الوقت والجهد على من سيساعدك في تجربة حلول غير ناجحة، ناهيك عن الانطباع الذي تعطيه حيال بذلك لجهد في سبيل حل مشكلتك. كما أن تقديمك لهذه المعلومات يوضح أنك تطلب المساعدة فقط، ولست اتكاليًا تنتظر من يكتب لك شيفرة لبرنامجك عوضًا عنك. فالخبر السيئ أن الكثير من طلاب علوم الحاسوب اعتادوا أن يطلبوا من الأشخاص على الإنترنت حل واجباتهم، أو من المستقلين أن يعدّو لهم تطبيقات سريعة مجانًا، متناسين أن منتديات المساعدة البرمجية لم تعد أصلًا لهذا الغرض. وصف الإعدادات التي تستخدمها إن لإعدادات حاسوبك الأثر البالغ في كيفية تنفيذ شيفرة برنامجك وعلى نوعية الأخطاء التي قد تنتج، ولضمان تمكن من سيساعدك من الحصول على نفس المشكلة التي تواجهك على حاسوبه، يجب عليك تقديم المعلومات التالية حول حاسوبك: نوع نظام التشغيل وإصداره، كأن تذكر أنه الإصدار الاحترافي من ويندوز 10 أو إصدار كاتالينا (الخامس عشر) من ماك أو إس. إصدار بايثون المستخدم في تنفيذ برنامجك، كأن تذكر بأن الإصدار 3.7 من بايثون أو الإصدار 3.6.6 منها. أي وحدات خارجية تستخدمها في برنامجك مع ذكر إصدار كل منها، كأن تذكر أنك تستخدم الإصدار 2.1.1 من وحدة جانغو. ولمعرفة إصدارات الوحدات الخارجية المستوردة في برنامجك، يمكنك تشغيل الأمر pip list، كما يمكنك التأكد من رقم إصدار كل منها باستخدام سمة الإصدار __version__، كما في المثال التالي المكتوب ضمن صَدفة بايثون التفاعلية: >>> import django >>> django.__version__ '2.1.1' ورغم كون هذه المعلومات ليست بذلك القدر من الأهمية، إلا أن ذكرها سيقلل من الوقت الضائع في النقاشات (الصد والرد)، لذا من المحبذ عرضها من البداية. أمثلة على أسئلة متكاملة نبين فيما يلي مثالًا على سؤال مطروح بطريقة صحيحة وفقًا للإرشادات والنصائح الواردة في هذا المقال: محرك الويب سيلينيوم: كيف أجد كافّة سمات العنصر؟ لدى استخدامي لوحدة سيلينيوم في بايثون، فبإمكاني الحصول على قيمة أي سمة من سمات كائن عنصر الويب باستخدام التابع ()get_attribute بالشكل: get_attribute(): foo = elem.get_attribute('href') وفي حال عدم وجود اسم السمة href، ستكون القيمة المعادة لا شيء None. سؤالي هو: كيف أحصل على قائمة بكافة سمات كل عنصر؟ فيبدو أنه لا يوجد توابع لجلب السمات من قبيل get_attributes() أو ()get_attribute_names. علمًا أني أستخدم الإصدار 2.44.0 من وحدة سيلينيوم في بايثون. لقد ورد هذا السؤال في قسم الأسئلة في أكاديمية حسوب، وفيه نلاحظ بأن العنوان قد لخص مضمون السؤال كاملًا في جملة واحدة، إذ ذُكرت المشكلة وهي التي تتحدث عن صيغة الخطأ الذي يظهر أثناء التنفيذ، وبذلك إذا قرأ شخص ما هذا العنوان مستقبلًا في نتائج البحث، فسيتضح له مباشرةً فيما إذا كان ذو صلة بسؤاله أم لا. كما أن الشيفرة ضمن السؤال منسقة بطريقة جيدة، وأجزاء النص مفصولة على هيئة فقرات، كما أن وجه السؤال واضح تمامًا، وقد تمت الإشارة له بعبارة "كيف أحل المشكلة؟"، كما يتحدث عن حدوث المشكلة أحيانًا وليس دائمًا، وهذا يدل على أن السائل قد حاول بالفعل إيجاد جواب وجرب حل الموضوع وتجريبه عدة مرات قبل أن يتجه لطرح السؤال. الخلاصة لعل وصولك إلى إجابات عن أسئلتك الخاصة بنفسك هي واحدة من أهم المهارات التي عليك اكتسابها كمبرمج، وهنا يمثّل الإنترنت ثروة من المصادر المتضمنة الإجابات التي تحتاج. إلا أن الإجراء الأول الذي يجب اتخاذه هو محاولة تحليل رسالة الخطأ المبهمة المعروضة من قبل بايثون، وفي حال عدم تمكنك من إيجاد حل لمشكلتك عبر البحث في الإنترنت، فحاول عندها نشر سؤالك في أحد المنتديات البرمجية أو إرسال بريد إلكتروني لأحد المختصين، إذ قدمنا في هذا المقال نصائح وإرشادات من شأنها ضمان كون هذه العملية فعالة ومثمرة من خلال طرح سؤال جيد. وتتمثل مواصفات السؤال الجيد في كونه محددًا ومفصلًا، يوفر الشيفرة المصدرية المسببة للخطأ كاملةً بالإضافة إلى رسالة الخطأ نفسها، شارحًا الإجراءات المجربة أصلًا مع تحديد نوع نظام التشغيل وإصدار بايثون المستخدم. وبحصولك على إجابة لسؤالك، لن تقتصر الفائدة عليك، بل على أي مبرمج مستقبلي قد يواجه نفس مشكلتك. وتذكر أن البرمجة مجال واسع جدًا، ولا يمكن لأحد أن يحيط بجميع تفاصيله، لذا لا تشعر بالإحباط إن اضطررت للبحث باستمرار عن إجابات لأسئلتك، فحتى مطورو البرمجيات المتمرسون يبحثون في الإنترنت عن التوثيقات والحلول يوميًا، وبدلًا من تضييع وقتك بالإحساس بالإحباط ركز على اكتساب مهارة إيجاد الحلول، وهي خطوة مهمة في طريقك لتصبح خبير بايثون. ترجمة -وبتصرف- للفصل الأول "التعامل مع الأخطاء وطلب المساعدة" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: التعامل مع رسائل الأخطاء في بايثون كيفية استخدام سطر أوامر بايثون التفاعلي كيفية التعامل مع الأخطاء البرمجية
-
تمنح نماذج الويب web forms مثل الحقول النصيّة السطرية text fields وصناديق إدخال النصوص text areas المستخدمين القدرة على إرسال البيانات إلى التطبيق، سواءً احتوت هذه النماذج على قوائم منسدلة، أو أزرار انتقاء radio button، فسيستخدمها التطبيق في تنفيذ أمر ما، أو حتّى لإرسال محتوًى نصي كبير، إذ يُمنح المستخدم مثلًا في تطبيقات التواصل الاجتماعي حيزًا حيث يمكنه إضافة محتوى جديد إلى صفحته الشخصية. يُعد فلاسك flask إطار عمل للويب مبني بلغة بايثون، ويتميز بكونه صغير الحجم وسهل المعالجة، ويوفّر أيضًا العديد من الأدوات والميزات التي من شأنها جعل إنشاء تطبيقات الويب في لغة بايثون أسهل، وبغية تصييّر نماذج الويب والتحقق من صحة مدخلاتها بأمانٍ ومرونة، سنستخدم إضافة فلاسك Flask-WTF التي تتيح لنا استخدام المكتبة WTForms المُتضمّنة نماذج الويب ذات واجهة المستخدم التفاعلية لاستخدامها في تطبيقات فلاسك. إذًا، إضافة WTForms هي مكتبة من مكاتب بايثون، توفّر تصييرًا مرنًا لنماذج الويب، فمن الممكن استخدامها في تصيير الحقول النصيّة السطرية وصناديق إدخال النصوص متعددة الأسطر وصناديق كلمات المرور وأزرار الانتقاء وغيرها، كما أنّها توفّر آلية فعّالة للتحقّق من صحة البيانات التي يدخلها المستخدم بالاعتماد على ميّزة المُصادِقين المختلفين الهادفة للتأكّد من كون هذه البيانات تُطابق صيغًا وقواعد ومعايير محدّدة، فمثلًا من الممكن التحقق من كون المُستخدم قد أدخل فعلًا البيانات اللازمة في أحد الحقول المطلوب ملؤها من قبله، أو أنّ المدخلات بطول معيّن. تستخدم مكتبة WTForms -إضافةً إلى مُساعدتنا على تأكيد البيانات وتقييدها بشروطٍ خاصّة- مفتاحًا مساعدًا لتحمينا من هجمات تزوير الطلب عبر المواقع Cross-site request forgery -أو اختصارًا CSRF- وهي نوع من الهجمات التي تُمكّن المخترق من تنفيذ أمور خطيرة غير مرغوبة في تطبيق الويب، متنكّرا على هيئة مُستخدم قد سجّل دخوله في التطبيق فعلًا، فقد تجبر الهجمات الناجحة المُستخدم الضحية على إرسال طلبات تغيير أساسية state-changing بغية تحويل أموال مثلًا إلى حساب المخترِق البنكي في أحد تطبيقات الصيرفة، أو تغيير عنوان البريد الإلكتروني الخاص بالمستخدم وهكذا دواليك، وفي حال كون الحساب الضحية حساب بصلاحيات مدير، فعندها ستكون هجمة تزوير الطلب CSRF قادرةً على اختراق كامل التطبيق. سننشئ في هذا المقال تطبيق ويب مُصغّر بغية توضيح كيفية تصيير نماذج الويب والتحقّق من صحة مدخلاتها باستخدام الإضافة Flask-WTF، إذ سيتضمّن التطبيق صفحةً لعرض الدورات التدريبيّة المُخزّنة أصلًا في قائمة بايثون، أما الصفحة الرئيسية فستتضمّن نموذجًا لإدخال عنوان الدورة التدريبية ووصفها وتكلفتها وكونها متاحة أم لا إضافةً إلى مستواها (مبتدئ، أو متوسط، أو متقدم). مستلزمات العمل قبل المتابعة في هذا المقال لا بُدّ من: توفُّر بيئة برمجة بايثون 3 محلية، مثبّتة على حاسوبك، وسنفترض في مقالنا أن اسم مجلد المشروع هو "flask_app". الفهم الجيد لأساسيات فلاسك، مثل مفهوم الوجهات ودوال العرض، وفي هذا الصدد يمكنك الاطلاع على المقالين كيفية بناء موقعك الإلكتروني الأول باستخدام إطار عمل فلاسك من لغة بايثون وكيفية استخدام القوالب في تطبيقات فلاسك Flask لفهم مبادئ فلاسك. فهم أساسيات لغة HTML. من المحبّذ أيضًا فهم أساسيات استخدام نماذج الويب في فلاسك، وهنا ننصحك بقراءة المقال كيفية استخدام نماذج الويب في تطبيقات فلاسك للاطلاع على هذه النقطة. الخطوة الأولى - تثبيت فلاسك والإضافة Flask-WTF سنعمل في هذه الخطوة على تثبيت كل من فلاسك والإضافة Flask-WTF والتي بدورها ستثبّت المكتبة WTForms تلقائيًا. لذلك، وبعد التأكّد من تفعيل البيئة الافتراضية، سنستخدم أمر تثبيت الحزم pip لتثبيت كل من فلاسك والإضافة Flask-WTF كما يلي: (env)user@localhost:$ pip install Flask Flask-WTF وبمجرّد انتهاء التثبيت بنجاح، سيظهر في السطر الأخير من الخرج ما يشبه التالي: Successfully installed Flask-2.0.2 Flask-WTF-1.0.0 Jinja2-3.0.3 MarkupSafe-2.0.1 WTForms-3.0.0 Werkzeug-2.0.2 click-8.0.3 itsdangerous-2.0.1 ومنه نلاحظ أنّ المكتبة WTForms قد ثُبتّت أيضًا كونها تتبع للحزمة Flask-WTF، أما باقي الحزم المُثبّتة فهي من تلك التابعة لفلاسك نفسه، ومع نهاية هذه الخطوة نكون قد ثبتنا ما يلزم من حزم بايثون، وأصبح من الممكن الانتقال إلى الخطوة التالية المُتمثّلة بإعداد نموذج ويب. الخطوة الثانية - إعداد النماذج سنعدّ في هذه الخطوة نموذج ويب باستخدام مُدقّقين validators وحقول مستوردة من مكتبة WTForms. إذ سنعدّ الحقول التالية: "Title": وهو صندوق إدخال نصي لإدخال عنوان الدورة التدريبية. "Description": وهو حقل نصي مُتعدّد الأسطر لإدخال توصيف الدورة التدريبية. "Price": وهو حقل مُخصّص لإدخال رقم صحيح يُمثّل تكلفة الدورة التدريبية. "Level": وهو حقل انتقاء بثلاث خيارات لتحديد مستوى الدورة التدريبية وهي: مبتدئ، متوسّط، متقدّم. "Available": وهو مربع اختيار يشير لكون الدورة التدريبية متوفرّة ومتاحة حاليًا أم لا. لذا، سننشئ بدايةً ضمن مجلد المشروع "flask_app" سننشئ ملفًا للنماذج باسم "forms.py"، ليتضمّن كافّة النماذج اللازمة للتطبيق: (env)user@localhost:$ nano forms.py إذ سيتضمّن هذا الملف صنفًا يمثّل نموذج الويب الذي نُعدّه، ولذلك سنضيف الاستيرادات التالية import إلى بداية الملف: from flask_wtf import FlaskForm from wtforms import (StringField, TextAreaField, IntegerField, BooleanField, RadioField) from wtforms.validators import InputRequired, Length الآن، ولبناء نموذج ويب، سننشئ صنفًا فرعيًا من الصنف الأساسي FlaskForm الذي استوردناه أصلًا من الحزمة flask_wtf، كما لا بُدّ من تحديد الحقول المراد استخدامها في النموذج والتي سنستوردها من المكتبة wtforms، وهي: StringField: وهو صندوق إدخال نصّي. TextAreaField: وهو حقل إدخال نصّي مُتعدّد الأسطر. IntegerField: وهو حقل مُخصّص لإدخال أعداد صحيحة. BooleanField: وهو مربع اختيار. RadioField: وهو حقل لإظهار أزرار الانتقاء ليتيح للمُستخدم اختيار إحداها. هذه الحقول هي المسؤولة عن تصيّير البيانات وتحويلها إلى النمط المطلوب، إضافةً إلى تفويض المُدققين بغية التحقق من صحة البيانات المُدخلة. استوردنا في السطر التالي من الشيفرة السابقة اثنين من المُدقّقين لنطبّقهما على الحقول الآنفة الذكر بما يضمن صحة مُدخلات المُستخدم: from wtforms.validators import InputRequired, Length إذ يُستخدم المُدقق InputRequired للتأكّد من كون المُستخدم قد أدخل فعلًا قيمًا في الحقول المطلوب ملؤها (الإجبارية)، والمُدقق Length يُستخدم للتأكّد من أن طول السلسلة النصية المُدخلة في حقل ما يُحقّق الحد الأدنى المطلوب من عدد المحارف، أو أنّه لم يتجاوز العدد الأعظمي المُتاح. أمّا الآن، فسنضيف الصنف التالي بعد تعليمة الاستيراد import مُباشرةً على النحو التالي: class CourseForm(FlaskForm): title = StringField('Title', validators=[InputRequired(), Length(min=10, max=100)]) description = TextAreaField('Course Description', validators=[InputRequired(), Length(max=200)]) price = IntegerField('Price', validators=[InputRequired()]) level = RadioField('Level', choices=['Beginner', 'Intermediate', 'Advanced'], validators=[InputRequired()]) available = BooleanField('Available', default='checked') نحفظ الملف ونغلقه. ورثنا في الصنف CourseForm أعلاه الشيفرات من الصنف الأساسي FlaskForm المُستورد أصلًا من حزمة فلاسك، كما عرّفنا مجموعةً من حقول النموذج مثل متغيرات لهذا الصنف باستخدام حقول النموذج نفسها المستوردة سابقًا من المكتبة WTForms، بحيث يكون عنوان الحقل هو الوسيط الأوّل لدى استنساخه. نعرّف المُدققين اللازمين لكل حقل بتمرير قائمة بأسماء المُدققين المُستوردين أصلًا من الوحدة wtforms.validators مثل وسيط لمتغير الصنف، فمثًلا بالنسبة للصندوق النصّي المُخصّص لعنوان الدورة التدريبية "title" في مثالنا، يكون وسيط العنوان فيه هو السلسلة النصية Title، مع تمرير مُدققين، هما: InputRequired: للدلالة على كون هذا الحقل مطلوب ملؤه من قبل المُستخدم ولا يجوز تركه فارغًا. Length: ويُمرّر له وسيطين، الأوّل هو min الذي يدل على الحد الأدنى المطلوب من عدد المحارف للسلسلة وهو في مثالنا "10"، أي لا ينبغي لطول سلسلة العنوان أن يقل عن عشرة محارف، أمّا الوسيط الثاني max فيشير إلى الحد الأعظمي المسموح فيه لعدد محارف السلسلة، وهو في حالتنا "100"، وبالتالي لا يجوز لطول العنوان المُدخل في الصندوق النصي أن يتجاوز مئة محرف. يتضمن الحقل النصي مُتعدّد الأسطر المُخصّص لوصف description الدورة التدريبية مُصادقين أيضًا، هما: InputRequired الذي يعني أنّ هذا الحقل مطلوب، والآخر Length جاعلًا قيمة الوسيط max تساوي "200"، دون وجود قيمة لوسيط الحد الأدنى min، ما يعني أنّ المطلوب فقط عدم تجاوز طول السلسلة المُدخلة لمئتي محرف. نعرّف بنفس الطريقة حقلًا مطلوبًا لإدخال قيمة عددية صحيحة تُمثّل تكلفة الدورة التدريبية باسم price؛ أما الحقل المُخصّص لمستوى الدورة level فهو زر انتقاء ذو عدّة خيارات، بحيث نعرّف هذه الخيارات ضمن قائمة بايثون ومن ثمّ نمررها قيمةً للوسيط choices من زر الانتقاء، ويُعرَّف هذا الحقل أيضًا على أنه مطلوب ملؤه باستخدام المُدقق InputRequired. أمّا عن الحقل available (وهو حقل مربع اختيار check box field) فهو يدل على أن الدورة مُتاحةٌ للتسجيل حاليًا، وقد عيّنا القيمة الافتراضية له ليكون مُفعلًا checked أي جرى اختياره عبر تمرير هذه القيمة إلى معامل الحالة الافتراضية default، ما يعني أنّ مُربّع الاختيار هذا سيكون مُفعّلًا افتراضيًا بمجرّد إضافة أي دورة جديدة للدلالة على أنها مُتاحة، إلّا في حال ألغى المُستخدم تفعيله يدويًا. ومع نهاية هذه الخطوة نكون قد أعددنا نموذج الويب المطلوب ضمن الملف المُسمّى "forms.py"، وسنعمل في الخطوة التالية على إنشاء تطبيق فلاسك، وسنستورد فيه هذا النموذج ونعرض حقوله ضمن صفحة التطبيق الرئيسية، كما سننشئ صفحةً أُخرى لعرض قائمة بالدورات التدريبية. يمكنك الاطلاع على صفحة دورة Crash في توثيق WTForms لمزيدٍ من المعلومات حول كيفية استخدام مكتبة WTForms، ومراجعة صفحة الحقول والمدققين للتأكد من صحة بيانات النموذج. الخطوة الثالثة - عرض نموذج الويب وقائمة الدورات التدريبية سنعمل في هذه الخطوة على إنشاء تطبيق فلاسك، بحيث سنعرض في صفحته الرئيسية نموذج الويب المُعدّ في الخطوة السابقة، كما سننشئ قائمةً لتتضمّن الدورات التدريبية مع صفحة مُخصّصة لعرض هذه القائمة. لذا، وبعد التأكّد من تفعيل البيئة البرمجية وتثبيت فلاسك، سننشئ ملفًا باسم "app.py" ضمن المجلد flask_app لنحرّره: (env)user@localhost:$ nano app.py سنستورد في هذا الملف الصنف والمُساعِدات اللازمة من فلاسك، كما سنستورد النموذج CourseForm من الملف "forms.py"، كما سنبني قائمةً بالدورات التدريبية، ومن ثمّ سنستنسخ instantiate النموذج ونمرّره إلى ملف ليكون قالبًا، ولإنجاز ذلك نكتب الشيفرات التالية ضمن الملف app.py: from flask import Flask, render_template, redirect, url_for from forms import CourseForm app = Flask(__name__) app.config['SECRET_KEY'] = 'your secret key' courses_list = [{ 'title': 'Python 101', 'description': 'Learn Python basics', 'price': 34, 'available': True, 'level': 'Beginner' }] @app.route('/', methods=('GET', 'POST')) def index(): form = CourseForm() return render_template('index.html', form=form) نحفظ الملف ونغلقه. استوردنا في الشيفرة السابقة من فلاسك ما يلي: الصنف Flask اللازم لإنشاء نسخة من تطبيق فلاسك. الدالة ()render_template اللازمة لتصيّير قالب الصفحة الرئيسية. الدالة ()redirect اللازمة لإعادة توجيه المُستخدم إلى الصفحة المُخصّصة بعرض الدورات التدريبية بمجرّد إضافة دورة جديدة. الدالة ()url_for اللازمة لبناء الروابط. استوردنا بدايةً الصنف ()CourseForm من الملف "forms.py"، ثمّ انشأنا النسخة الفعلية من تطبيق فلاسك باسم "app"؛ كما أعددنا الضبط اللازم لمفتاح الأمان الخاص بنماذج WTForms لاستخدامه لدى توليد المفتاح المساعد للحماية من هجمات CSRF، وهذا سيساعد بالنتيجة على تأمين نماذج الويب، مع الأخذ بالحسبان أنّ مفتاح الأمان يجب أن يكون سلسلةً نصيةً عشوائية بطول مناسب. أنشأنا بعد ذلك قائمةً من قواميس بايثون باسم courses_list، ولا تحتوي هذه المرحلة سوى على قاموس واحد يتضمّن دورة تجريبية بعنوان Python 101، بمعنى أنّنا نستخدم قائمة بايثون لتخزين البيانات وذلك كون مقالنا تعليمي ولسنا بصدد شرح طرق التخزين فيه، ولكن في التطبيقات العملية الواقعية نستخدم قاعدة بيانات لتُخزّن هذه البيانات بصورةٍ دائمة سامحةً لنا بتعديلها واسترجاعها بسهولة وفعالية، وللتعرّف على كيفية استخدام قاعدة بيانات لتخزين بيانات الدورات ننصحك بقراءة المقال [How To Use an SQLite Database in a Flask Application](أحد مقالات المجموعة الحالية 531039). أنشأنا الوجهة الرئيسية / باستخدام المزخرف ()app.route ضمن دالة العرض ()index، إذ تتعامل هذه الوجهة مع كلا نوعي طلبات HTTP وهما GET و POST من خلال المعامل methods، إذ تتخصّص الطلبات من النوع GET بجلب البيانات، أما الطلبات من النوع POST فهي مُتخصّصة بإرسال البيانات إلى الخادم عبر نموذج ويب مثلًا. بعد ذلك، استنسخنا الصنف ()CourseForm المُمثّل لنموذج الويب، وحفظنا هذه النسخة ضمن متغير باسم form، لتكون القيمة المعادة هي استدعاء للدالة ()render_template ممرين لها ملف قالب باسم "index.html" مع نسخة النموذج. بغية عرض نموذج الويب ضمن الصفحة الرئيسية للتطبيق، سننشئ بدايةً ملف قالب ليتضمّن كافة شيفرات HTML الأساسية اللازمة لترثها لاحقًا القوالب الأُخرى، وسيجنّبنا هذا تكرار الشيفرات، ومن ثمّ سننشئ ملف قالب الصفحة الرئيسية "index.html" المُصيّر أصلًا باستخدام الدالة ()index. الآن، سننشئ مجلدًا للقوالب باسم "templates"، إذ سيبحث فلاسك عن القوالب ضمن المجلد "flask_app"، وسننشئ ضمن مجلّد القوالب هذا ملف قالب باسم "base.html"، الذي سيمثّل القالب الأساسي لبقية القوالب على النحو التالي: (env)user@localhost:$ mkdir templates (env)user@localhost:$ nano templates/base.html وسنكتب فيه الشيفرات التالية لإنشاء القالب الرئيسي بحيث يتضمّن شريط تصفُّح وكتلة محتوى: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %} {% endblock %} - FlaskApp</title> <style> nav a { color: #d64161; font-size: 3em; margin-left: 50px; text-decoration: none; } </style> </head> <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% block content %} {% endblock %} </div> </body> </html> إذ يتضمّن القالب الأساسي كافّة الشيفرات المتداولة التي ستحتاجها في القوالب الأُخرى. ستُستبدل لاحقًا كتلة العنوان title بعنوان كل صفحة، وكتلة المحتوى content بمحتواها، أمّا عن شريط التصفح فسيتضمّن رابطين، الأوّل ينقل المُستخدم إلى الصفحة الرئيسية للتطبيق باستخدام الدالة المساعدة ()url_for لتحقيق الربط مع دالة العرض ()index، والآخر لصفحة المعلومات حول التطبيق في حال قررت تضمينها في تطبيقك. نحفظ الملف ونغلقه. الآن، سننشئ ملف قالب باسم "index.html" وهو الاسم الذي حددناه في الملف "app.py": (env)user@localhost:$ nano templates/index.html سيتضمّن هذا الملف نموذج الويب المُمرر إلى القالب "index.html" عن طريق المتغير form، وسنضيف ضمنه الشيفرات التالية: {% extends 'base.html' %} {% block content %} <h1>{% block title %} Add a New Course {% endblock %}</h1> <form method="POST" action="/"> {{ form.csrf_token }} <p> {{ form.title.label }} {{ form.title(size=20) }} </p> {% if form.title.errors %} <ul class="errors"> {% for error in form.title.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <p> {{ form.description.label }} </p> {{ form.description(rows=10, cols=50) }} {% if form.description.errors %} <ul class="errors"> {% for error in form.description.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <p> {{ form.price.label }} {{ form.price() }} </p> {% if form.price.errors %} <ul class="errors"> {% for error in form.price.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <p> {{ form.available() }} {{ form.available.label }} </p> {% if form.available.errors %} <ul class="errors"> {% for error in form.available.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <p> {{ form.level.label }} {{ form.level() }} </p> {% if form.level.errors %} <ul class="errors"> {% for error in form.level.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <p> <input type="submit" value="Add"> </p> </form> {% endblock %} نحفظ الملف ونغلقه. امتد القالب الرئيسي في بداية الشيفرة، وعيّنا عنوانًا للصفحة ضمن تنسيق عنوان من المستوى الأوّل باستخدام الوسم <h1>، ثمّ صيّرنا حقول نموذج الويب ضمن الوسم <form>، وضبطنا نوع طلبات HTTP الخاصّة به لتكون من النوع POST، والحدث المرتبط به ليكون الانتقال إلى الوجهة الرئيسية / المُتمثّلة بالصفحة الرئيسية للتطبيق. صيّرنا بدايةً المفتاح المساعد token الذي تستخدمه نماذج WTForms لحماية النموذج من هجمات CSRF باستخدام التعليمة {{ form.csrf_token }}، إذ يُرسل هذا المفتاح إلى الخادم مع بقية بيانات النموذج، ومن المهم جدًا تصيّير المفتاح المساعد لجعل النماذج آمنة. صيّرنا كل حقل من النموذج باستخدام الصيغة ()form.field، كما صيّرنا عنوان كل منها باستخدام الصيغة form.field.label، وهنا من الممكن تمرير وسطاء إلى كل حقل والتي من شأنها التحكّم بطريقة عرضه، فمثلًا عيّنا حجم الصندوق النصي الخاص بالعنوان بالشّكل {{ form.title(size=20) }}، كما عيّنا عدد الأسطر والأعمدة ضمن الحقل النصي مُتعدّد الأسطر والخاص بوصف الدورة بالشّكل من خلال المعاملين rows و cols وبنفس الطريقة التقليدية المُتبعة في لغة HTML، وبذلك من الممكن إضافة ما نشاء من سمات HTML إلى الحقول، مثل سمة class مثلًا بغية تعيّين صنفٍ لشيفرات CSS. استخدمنا الصيغة if form.field.errors للتحقّق من وجود أخطاء في المُصادقة، ففي حال وجود أخطاء في حقلٍ ما، سيجري المرور عليها باستخدام حلقة for تكرارية لتُعرض ضمن قائمة أسفل الحقل. الآن، ومع وجودنا ضمن المجلد "flask_app" ومع كون البيئة الافتراضية مُفعّلة، سنُعلم فلاسك بالتطبيق المراد تشغيله (وهو في حالتنا الملف app.py) باستخدام متغير البيئة FLASK_APP، ثم نضبط متغير البيئة FLASK_ENV لتشغيل التطبيق في وضع التطوير development، مع إمكانية الوصول إلى مُنقّح الأخطاء؛ ولمزيدٍ من المعلومات حول مُنقّح الأخطاء في فلاسك ننصحك بقراءة المقال كيفية التعامل مع الأخطاء في تطبيقات فلاسك، ولتنفيذ ما سبق سنشغّل الأوامر التالية (مع ملاحظة أنّنا نستخدم الأمر set في بيئة ويندوز عوضًا عن الأمر export? (env)user@localhost:$ export FLASK_APP=app (env)user@localhost:$ export FLASK_ENV=development والآن سنشغّل التطبيق باستخدام الأمر التالي: (env)user@localhost:$ flask run وبعد التأكد من كون خادم التطوير ما يزال قيد التشغيل، نذهب إلى الرابط التالي باستخدام المتصفح: http://127.0.0.1:5000/ فيظهر نموذج الويب ضمن الصفحة الرئيسية للتطبيق كما هو موضح في الشكل التالي: ولو جرّبنا إرسال النموذج مع ترك حقل العنوان فارغًا، ستظهر رسالة خطأ تعلمنا بأن العنوان مطلوب، وهنا من الجيد تجربة إرسال نماذج ببيانات لا تحقق الشروط بغية التعرّف على رسائل الخطأ المُختلفة، مثل إدخال عنوان بأقل من 10 محارف، أو وصف يتجاوز 200 محرف، أمّا إذا ملأنا النموذج ببيانات صحيحة وأرسلناه فلن يحدث شيء حاليًا، والسبب أنّنا لم نضف حتى الآن الشيفرة اللازمة للتعامل مع النماذج المُرسلة، الأمر الذي سنفعله لاحقًا. سنعمل الآن على إضافة صفحة لعرض الدورات الموجودة في القائمة، وسنضيف فيما بعد ما يلزم للتعامل مع البيانات الآتية من نموذج الويب، والتي ستضيف بالنتيجة دورة جديدة إلى القائمة، ليُعاد بعدها توجيه المُستخدم إلى صفحة عرض الدورات ليرى أنّ الدورة الجديدة قد أُضيفت فعلًا. لذلك، سنفتح نافذة أسطر أوامر جديدة مع بقاء خادم التطوير قيد التشغيل، ثمّ سنفتح الملف app.py لإضافة الوجهة الخاصّة بصفحة عرض الصفحات إليه: (env)user@localhost:$ nano app.py وسنضيف الوجهة التالية إلى نهاية الملف: # ... @app.route('/courses/') def courses(): return render_template('courses.html', courses_list=courses_list) نحفظ الملف ونغلقه. تُخرج الوجهة السابقة قالبًا باسم "courses.html" ممرّرة له قائمة الدورات courses_list. أمّا الآن فسننشئ القالب "courses.html" المسؤول عن عرض الدورات: (env)user@localhost:$ nano templates/courses.html ونكتب ضمنه الشيفرة التالية: {% extends 'base.html' %} {% block content %} <h1>{% block title %} Courses {% endblock %}</h1> <hr> {% for course in courses_list %} <h2> {{ course['title'] }} </h2> <h4> {{ course['description'] }} </h4> <p> {{ course['price'] }}$ </p> <p><i>({{ course['level'] }})</i></p> <p>Availability: {% if course['available'] %} Available {% else %} Not Available {% endif %}</p> <hr> {% endfor %} {% endblock %} نحفظ الملف ونغلقه. عيّنا في الشيفرة السابقة عنوانًا، ثمّ مررنا على كافّة عناصر القائمة courses_list، إذ سيُعرض العنوان ضمن تنسيق عنوان من المستوى الثاني باستخدام الوسم <h2>، والوصف ضمن تنسيق عنوان من المستوى الرابع <h4>، أمّا كل من السعر والمستوى فسيُعرضان ضمن تنسيق فقرة باستخدام الوسم <p>، ويجري التحقّق من كون الدورة مُتاحة باستخدام العبارة الشرطية ['if course['available، لتُعرض العبارة "Available"، والتي تعني أن الدورة مُتاحة في حال توفرها، والعبارة "Not Available" في حال عدم توفرها. وبالانتقال إلى صفحة الدورات باستخدام الرابط التالي في المتصفح: http://127.0.0.1:5000/courses/ ستظهر الصفحة مُتضمّنةً دورةً تدريبيةً واحدةً وهي تلك التي أضفناها يدويًا إلى قائمة بايثون سابقًا، كما هو موضح في الشّكل التالي: أمّا الآن، سنفتح ملف القالب الأساسي "base.html" لتعديله بغية إضافة رابط مباشر ينقلنا إلى صفحة عرض الدورات وذلك ضمن شريط التصفّح: (env)user@localhost:$ nano templates/base.html ونعدله ليبدو على النحو التالي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %} {% endblock %} - FlaskApp</title> <style> nav a { color: #d64161; font-size: 3em; margin-left: 50px; text-decoration: none; } </style> </head> <body> <nav> <a href="{{ url_for('index') }}">FlaskApp</a> <a href="{{ url_for('courses') }}">Courses</a> <a href="#">About</a> </nav> <hr> <div class="content"> {% block content %} {% endblock %} </div> </body> </html> نحفظ الملف ونغلقه. الآن سيظهر رابط الانتقال إلى صفحة عرض الدورات ضمن شريط التصفّح عند تحديث الصفحة الرئيسية للتطبيق في المتصفّح. ومع نهاية هذه الخطوة نكون قد أنشأنا الصفحات اللازمة للتطبيق، وهي: الصفحة الرئيسية "index.html" المُتضمّنة نموذج ويب لإضافة دورات جديدة، وصفحة لعرض الدورات المُخزّنة ضمن قائمة الدورات "courses.html". لجعل التطبيق يعمل فعليًا، سنعالج في الخطوة التالية البيانات التي يرسلها المُستخدم عبر نموذج الويب من حيث التحقّق من صحتها وإضافتها إلى قائمة الدورات. الخطوة الرابعة - الوصول إلى بيانات النموذج سنعمل في هذه الخطوة على الوصول إلى البيانات التي أرسلها المُستخدم عبر النموذج للتأكّد من صحتها ومن ثمّ إضافتها إلى قائمة الدورات. لذلك، سنفتح الملف "app.py" لتعديله بإضافة الشيفرات اللازمة للتعامل مع بيانات نموذج الويب وذلك ضمن الدالة ()index: (env)user@localhost:$ nano app.py وسنعدّل الدالة ()index لتصبح كما يلي: # ... @app.route('/', methods=('GET', 'POST')) def index(): form = CourseForm() if form.validate_on_submit(): courses_list.append({'title': form.title.data, 'description': form.description.data, 'price': form.price.data, 'available': form.available.data, 'level': form.level.data }) return redirect(url_for('courses')) return render_template('index.html', form=form) نحفظ الملف ونغلقه. استدعينا في الشيفرة السابقة التابع ()validate_on_submit من الكائن form، إذ يتحقّق هذا التابع من كون طلب HTTP من النوع POST، ثمّ شغلنا المدققات التي أعددناها سابقًا لكل حقل من الحقول، فإذا أعاد أي منها خطأ ما لن يتحقق الشرط أي ستكون قيمته False، وسيظهر كل خطأ أسفل الحقل الذي تسبب بحدوثه. أمّا في حال كانت كافّة البيانات المُدخلة صحيحة، سيتحقق الشرط أي ستكون قيمته True وبالتالي سيُنفّذ جزء الشيفرة التالي لتعليمة if، وفيها نبني قاموس بايثون للدورة الجديدة ومن ثمّ نستخدم التابع append لإضافة هذه الدورة إلى قائمة الدورات courses_list، إذ استخدمنا الصيغة form.field.data للوصول إلى قيمة كل حقل في النموذج، ونهايةً وبعد إضافة الدورة الجديدة إلى قائمة الدورات، نعيد توجيه المُستخدم إلى صفحة عرض الدورات. الآن، سننتقل إلى الصفحة الرئيسية للتطبيق مع كون خادم التطوير قيد التشغيل، باستخدام الرابط: http://127.0.0.1:5000/ وبملء النموذج ببيانات صحيحة وإرساله، نلاحظ إضافة الدورة الجديدة فعلًا، وسنُوجّه تلقائيًا إلى صفحة عرض الدورات من التطبيق والتي ستظهر فيها الدورة الجديدة المُضافة. الخلاصة أنشأنا في هذه المقال تطبيق فلاسك ذا نموذج ويب مبني باستخدام الإضافة Flask-WTF والمكتبة WTForms، إذ ضمّنا في نموذج الويب هذا العديد من أنواع الحقول التي من شأنها استقبال البيانات من المُستخدم وتحقّقنا من صحتها بالاعتماد على المُدققين الخاصين بمكتبة WTForms، لنضيف هذه البيانات بالنتيجة إلى مخزن البيانات وهو قائمة بايثون في مثالنا. ترجمة -وبتصرف- للمقال How To Use and Validate Web Forms with Flask-WTF لصاحبه Abdelhadi Dyouri. اقرأ أيضًا كيفية استخدام نماذج الويب في تطبيقات فلاسك Flask مدخل إلى استخدام مكتبة WTForms للتعامل مع نماذج HTML في تطبيقات Flask إنشاء تطبيق ويب باستخدام إطار عمل فلاسك Flask من لغة بايثون
-
تعد بايثون Python إحدى أشهر لغات البرمجة وأكثرها استخدامًا، وهي خيار ممتاز ليبدأ به المبرمجون المبتدئون، إذ يمكن استخدامها في معظم المجالات، بدءًا من ألعاب الفيديو، وحتى تحليل البيانات والتعلم الآلي. بايثون هي لغة برمجة عالية المستوى، وتفاعلية وكائنية. وتتمتع بمقروئية عالية، إذ تستخدم كلمات إنجليزية بسيطة، على خلاف اللغات الأخرى التي تستخدم الرموز، كما أنّ قواعدها الإملائية والصياغية بسيطة، ما يجعل تعلمها سهلًا موازنةً بلغات برمجة أخرى. وفي بدايات تعلّمك لغة بايثون ، وكما هو الحال في رحلة تعلّم أي لغة برمجة، قد تواجهك بعض العقبات ورسائل الأخطاء غير المفهومة، وفي هذه المرحلة من الشائع الشعور بالفشل لدى اضطرارك لاستشارة مواقع الإنترنت عدة مرات في اليوم. لكن حتى أمهر مطوري البرمجيات يبحثون في الإنترنت ويراجعون التوثيقات لإيجاد إجابات حول أسئلتهم البرمجية. كن على يقين بأنه طالما أنك لا تمتلك الموارد الاجتماعية أو المادية اللازمة للحصول على مدرس خصوصي للإجابة على أسئلتك البرمجية، فما من سبل أمامك سوى حاسوبك والإبحار في محركات البحث وصبرك، والخبر الجيد أنه من شبه المؤكد كون جميع تساؤلاتك قد طُرحت من قبل. فحتى تكون مبرمجًا لا بد من امتلاكك مهارة إيجاد إجابات على أسئلتك بنفسك، الأمر الذي يفوق بأهميته معرفتك لأي خوارزمية برمجية أو بنية معطيات ما. سيعمل هذا المقال على إرشادك نحو تطوير هذه المهارة البالغة الأهمية. كيفية فهم رسائل الأخطاء في بايثون عندما يواجه العديد من المبرمجين رسائل الأخطاء المليئة بالثرثرة التقنية، فإن أول ما يفكرون بفعله هو تجاهلها تمامًا، إلا أن رسالة الخطأ هذه تتضمن الإجابة حول الخطأ الحاصل في البرنامج، فإيجاد هذه الإجابة عبارة عن عملية ذات خطوتين: الأولى هي فحص متتبع الأخطاء، والثانية هي البحث في الإنترنت حول رسالة الخطأ هذه. فحص متتبع الأخطاء تتوقف برامج بايثون عن العمل لدى مصادفتها استثناءً دون وجود عبارة استثناء except للتعامل معه، وفي حال حدوث ذلك، تُعرض رسالة بهذا الاستثناء مع تتبّع له وهو ما يدعى أيضًا مكدّس الاقتفاء Stack trace، والذي يُظهر مكان حدوث الاستثناء في برنامجك ومسارات استدعاءات الدوال المسببة له. وبغية التدرب على كيفية قراءة تتبعات الأخطاء، سنكتب البرنامج البسيط التالي ونحفظه باسم abcTraceback.py، إذ تلعب أرقام الأسطر دور المرجع والدليل للسطر وليست جزءًا من الشيفرة. 1. def a(): 2. print('Start of a()') 1 3. b() # Call b(). 4. 5. def b(): 6. print('Start of b()') 2 7. c() # Call c(). 8. 9. def c(): 10. print('Start of c()') 3 11. 42 / 0 # This will cause a zero divide error. 12. 13. a() # Call a(). في البرنامج السابق، تستدعي الدالة ()a الدالة ()b، التي تستدعي بدورها الدالة ()c المتضمنة للتعبير الرياضي 42/0 المسبب لخطأ القسمة على صفر، فعند تشغيل هذا البرنامج، سيبدو الخرج بالشكل: Start of a() Start of b() Start of c() Traceback (most recent call last): File "abcTraceback.py", line 13, in <module> a() # Call a(). File "abcTraceback.py", line 3, in a b() # Call b(). File "abcTraceback.py", line 7, in b c() # Call c(). File "abcTraceback.py", line 11, in c 42 / 0 # This will cause a zero divide error. ZeroDivisionError: division by zero لنتفحص الآن تقرير متتبع الأخطاء سطرًا بسطر، والذي يبدأ من السطر: Traceback (most recent call last): وتهدف هذه الرسالة إلى إبلاغك بأن ما يتلوها هو تقرير متتبّع الأخطاء، وتشير العبارة most recent call last والتي تعني أن الاستدعاء الأخير سيظهر في نهاية التقرير إلى كون استدعاءات التوابع ستظهر في التقرير بنفس ترتيب ورودها في الشيفرة، بدءًا من استدعاء أول دالة وانتهاءً بآخرها. أما السطر التالي فيظهر تقرير تتبّع استدعاء أول دالة: File "abcTraceback.py", line 13, in <module> a() # Call a(). وندعو هذين السطرين بملخص كائن الإطار، كما أنهما يُظهران معلوماتهما ضمن كائن إطار. فعند استدعاء دالة ما فإن كل من بيانات المتغير المحلي والمكان المخصص في الشيفرة لإسناد القيمة المعادة من الدالة بعد استدعائها يُخزنان ضمن كائن إطار، إذ تتضمن كائنات الأطر كل من المتغيرات المحلية وغيرها من البيانات المتعلقة باستدعاء الدوال، إذ يتم إنشاء هذه الكائنات لحظة استدعاء الدوال وينتهي بإعادة الدالة لقيمتها. ويُظهر متتبّع الأخطاء ملخصًا على هيئة إطار لكل إطار قد انتهى دوره بإعادة قيمة دالته، ونلاحظ من التقرير أعلاه أن استدعاء هذه الدالة قد تم في السطر ذو الرقم 13 من الملف abcTraceback.py، ويشير الوسم <module> إلى كون هذا السطر مصرح عنه ضمن المجال العام، ليظهر بعدها السطر 13 نفسه مسبوقًا بمسافتين بادئتين. وتظهر الأسطر الأربعة التالية ملخصات الأطر التالية: File "abcTraceback.py", line 3, in a b() # Call b(). File "abcTraceback.py", line 7, in b c() # Call c(). ومنه نستنج أنه قد تم استدعاء الدالة ()b في السطر الثالث ضمن الدالة ()a، ما أدى بدوره إلى استدعاء الدالة ()c في السطر 7 ضمن الدالة ()b. ومن الجدير بالملاحظة أن تقرير التتبع لا يُظهر استدعاء الدالة ()print الذي تم في كل من الأسطر 2 و 6 و 10 من الشيفرة رغم تشغيلها حتى قبل استدعاء الدوال آنفة الذكر، والسبب في ذلك هو أن التقرير يشمل فقط الأسطر الحاوية على استدعاءات التوابع المسببة للاستثناء. وآخر إطار من ملخص تقرير تتبع الأخطاء يظهر رقم السطر المتضمن للاستثناء الذي لم يتم التعامل معه متبوعًا باسمه ورسالة الخطأ الخاصة به: File "abcTraceback.py", line 11, in c 42 / 0 # This will cause a zero divide error. ZeroDivisionError: division by zero ومن الجدير بالملاحظة أن رقم السطر المعطى في متتبع الأخطاء هو مكان عثور بايثون على آخر خطأ، وبالتالي من الممكن كون مصدر الخطأ الرئيسي في سطر ما قبله. وتُعرف رسائل الخطأ بكونها مختصرة ومبهمة، فالكلمات الثلاث "قسمة على صفر" لن تعني شيئًا ما لم تكن تعلم أنه من المستحيل قسمة أي عدد على الصفر رياضيًا وبأنه يمثل خطأً برمجيًا شائعًا، وفي برنامجنا السابق ليس من الصعب تحديد موطن الخطأ، فبمجرد النظر إلى رقم سطر الشيفرة في إطار ملخص تقرير متتبع الأخطاء يتضح أن خطأ القسمة على صفر يحدث عند التعبير 42/0 من الشيفرة. والآن دعونا ننتقل إلى حالة أصعب قليلًا. لذا، اكتب الشيفرة التالية ضمن محرر النصوص واحفظ الملف باسم zeroDivideTraceback.py: def spam(number1, number2): return number1 / (number2 - 42) spam(101, 42) ولدى تشغيل هذا البرنامج، سيبدو الخرج كالتالي: Traceback (most recent call last): File "zeroDivideTraceback.py", line 4, in <module> spam(101, 42) File "zeroDivideTraceback.py", line 2, in spam return number1 / (number2 - 42) ZeroDivisionError: division by zero إن رسالة الخطأ هي نفسها كما في المثال السابق، إلا أن خطأ القسمة على صفر في السطر البرمجي return number1 / (number2 - 42) غير واضح تمامًا، ولكن من الممكن استنتاج وجود عملية قسمة من وجود المعامل /، وبأن التعبير (number2-42) يساوي الصفر، وهذا ما يمكننا من استنتاج أن الدالة spam() ستعطي خطأ في حال كان الوسيط الثاني number2 المُمرر لها مساويًا للعدد 42. وقد يشير متتبّع الأخطاء أحيانًا إلى كون الخطأ في السطر التالي لمسبب الخطأ الحقيقي. فعلى سبيل المثال، في البرنامج التالي ينقص السطر الأول منه قوس إغلاق دالة الطباعة: print('Hello.' print('How are you?') إلا أن رسالة الخطأ لهذا البرنامج تشير لكون المشكلة في السطر الثاني منه: File "example.py", line 2 print('How are you?') ^ SyntaxError: invalid syntax والسبب في ذلك هو أن مفسر بايثون لن يلاحظ هذا الخطأ إلا عند قراءته للسطر الثاني، فقد يشير متتبّع الأخطاء إلى مكان حدوث الخطأ والذي لن يكون بالضرورة هو نفس موضع مسبب حصول الخطأ الفعلي. وفي حال فشل إطار الملخص في تقديم ما يكفي من معلومات لاكتشاف الخطأ، أو في حال كون المسبب الحقيقي للخطأ في سطرٍ سابق غير موضح في متتبّع الأخطاء، عندها عليك المرور على برنامجك مستخدمًا منقّح الأخطاء، كما يمكنك التحقق من رسائل السجل بحثًا عن السبب. الأمر الذي قد يستغرق وقتًا ليس بالقليل، لذا فإن البحث في الإنترنت حول المشكلة قد يعطيك دلائل مهمة حول كيفية حلها بسرعة أكبر. البحث حول رسائل الخطأ تكون رسائل الأخطاء في الغالب قصيرة جدًا ولا تمثل جملًا كاملة، والسبب في ذلك هو كونها رسائل تذكيرية وليست تفسيرية، إذ يصادفها المبرمجون بانتظام. وإذا كنت تواجه رسالة خطأ للمرة الأولى، فإن نسخها ولصقها في أحد محركات البحث على الإنترنت سيوصلك في الغالب إلى شرح تفصيلي حول الخطأ وأسباب حدوثه المحتملة. ويبين الشكل التالي نتائج البحث عن الجملة python "ZeroDivisionError: division by zero" أي "خطأ القسمة الصفرية: قسمة على صفر" بايثون، إذ من المفيد تضمين رسالة الخطأ ما بين إشارتي تنصيص في إيجاد عبارة موافقة بدقة، كما أن إضافة كلمة Python يؤدي إلى تضييق نطاق البحث أكثر. نسخ رسالة خطأ ولصقها في إحدى أدوات البحث على الإنترنت يمكن أن يؤمن تفسيراتٍ وحلولًا سريعة. ولا يمكن عد البحث حول رسائل الخطأ شكلًا من أشكال الغش أو الاتكالية، فمن شبه المستحيل أن يحفظ الشخص كافة رسائل الخطأ المحتملة لأي لغة برمجة، إذ يبحث مطورو البرمجيات المحترفون عن إجابات لأسئلتهم البرمجية في الإنترنت يوميًا. وفي حال كون أحد أجزاء رسالة الخطأ التي تواجهك هو خاص بشيفرتك أنت، فمن الأفضل استبعاده أثناء البحث، ولتوضيح هذه النقطة لنأخذ رسائل الخطأ التالية: >>> print(employeRecord) Traceback (most recent call last): File "<stdin>", line 1, in <module> 1 NameError: name 'employeRecord' is not defined >>> 42 - 'hello' Traceback (most recent call last): File "<stdin>", line 1, in <module> 2 TypeError: unsupported operand type(s) for -: 'int' and 'str' يوجد في هذا المثال خطأ كتابي في اسم المتغير employeRecord ما سبب حدوث الخطأ 1، وبما أن المعرف employeRecord خاص بشيفرتك، وذلك في رسالة الخطأ NameError: name 'employeRecord' is not defined والتي تعني حدوث خطأ في الاسم بسبب كون اسم المتغير employeRecord غير معرف. أما في السطر الأخير فمن الواضح أن الأجزاء int و str من رسالة الخطأ 2 تعود للقيم 42 و hello على التوالي، ففي هذه الحالة من الأفضل اقتصار البحث على العبارة python "TypeError: unsupported operand type(s)" for والتي تعني خطأ في نمط البيانات ناتج عن كون أحد المعاملات ذو نمط غير مدعوم، وذلك بدلًا من تضمين البحث بأجزاء من الرسالة تخص شيفرتك على وجه التحديد، وإذا فشل هذا البحث المختصر في الوصول إلى نتائج مفيدة، فجرب كتابة رسالة الخطأ كاملةً. تجنب حدوث الأخطاء باستخدام منقحات الصياغة لعل الطريقة الأفضل في إصلاح الأخطاء هي عدم الوقوع بها من الأصل، وهنا يأتي دور منقّحات الأخطاء Linters، وهي عبارة عن تطبيقات تعمل على تحليل الشيفرة المصدرية لتنبهك حول أي خطأ محتمل فيها، أما اسمها "Linters" فقد أتى تيمنًا بأداة جمع الألياف الصغيرة والأوبار في مجفف الملابس. ورغم كون منقّح الأخطاء لن يكتشف أخطاء الشيفرة بالكامل، إلا أن التحليل الساكن الذي يجريه (أي فحص الشيفرة المصدرية دون تشغيلها) قادر على تحديد العديد من الأخطاء الشائعة الناتجة عن الأخطاء الإملائية وسنستعرض في مقالات لاحقة كيفية استخدام تلميحات الكتابة التي يقدمها التحليل الساكن. وتتضمن العديد من محررات النصوص وبيئات التطوير البرمجية المتكاملة IDEs على منقّحات صياغة تعمل في الخلفية، والقادرة على التنبيه إلى الأخطاء المكتشفة بالزمن الحقيقي كما هو موضح في الشكل التالي. منقّح صياغة يشير إلى متغير غير مصرح عنه وذلك في محرر النصوص MU (الشكل الأعلى) وفي المحرر PyCharm (الشكل الأوسط) وفي المحرر Sublime (الشكل الأسفل). وبذلك تعمل إشعارات الأخطاء شبه الفورية التي يوفرها منقّح الأخطاء على تحسين إنتاجية العمل البرمجي بشكل ملموس، فبدون وجوده ستضطر إلى تشغيل برنامجك لتكتشف وجود خطأ، منتقلًا إلى ملخص متتبّع الأخطاء بحثًا عن السطر في الشيفرة المصدرية المتسبب في الخطأ لإصلاح الخطأ الإملائي المسبب له، ولكن ماذا لو ارتكبت عدة أخطاء إملائية؟ عندها لن تجدها إلا بإعادة تنفيذ البرنامج عدة مرات فكل دورة تشغيل ستجد خطأ واحد تتوقف عنده، في حين أن منقّحات الصياغة قادرة على الإشارة لوجود عدة أخطاء معًا ويتم ذلك مباشرةً في محرر النصوص أثناء كتابة الشيفرة، ما يمكنك من تحديد السطر الذي حدثت فيه المشكلة مباشرةً. وقد لا يأتي محرر النصوص أو البيئة البرمجية المتكاملة التي تستخدمها مع منقّح صياغة أصلًا، ولكن إن كانت تدعم الإضافات، فمن شبه المؤكد وجود منقّح صياغة كإضافة، وغالبًا ما تستخدم هذه الإضافات وحدة تنقيح صياغة تدعى Pyflakes أو وحدات أخرى تؤدي هذا الغرض. وبإمكانك تثبيت الوحدة Pyflakes من الرابط https://pypi.org/project/pyflakes/ أو باستخدام أمر تثبيت الحزم pip بكتابة الأمر pip install --user pyflakes، الأمر الذي يستحق العناء لإنجازه. ملاحظة: يمكنك في نظام ويندوز تشغيل كل من الأمرين pip و python، ولكن في حال استخدامك لأنظمة ماك أو إس أو لينكس فتكون أسماء هذين الأمرين بهذا الشكل فقط من أجل الإصدار الثاني من بايثون، أما للإصدار الثالث فيصبحان بالشكل pip3 و python3، انتبه لهذا الموضوع كلما صادفت أحد الأمرين pip أو python في سياق المقال. تأتي بيئة التطوير المتكاملة الخاصة ببايثون IDLE بدون منقّح صياغة وبدون القابلية لتثبيته فيها. الخلاصة لعل وصولك إلى إجابات عن أسئلتك الخاصة بنفسك هي واحدة من أهم المهارات التي عليك اكتسابها كمبرمج، وهنا يمثل الإنترنت ثروة من المصادر المتضمنة الإجابات التي تحتاج. إلا أن الإجراء الأول الذي يجب اتخاذه هو محاولة تحليل رسالة الخطأ المبهمة المعروضة من قبل بايثون، ولا تقلق إن لم تفهم محتواها، فبإمكانك نسخ نص الرسالة إلى أحد محركات البحث وصولًا إلى تفسير حولها باللغة الإنجليزية العادية مع بيان سببها المحتمل، كما أن متتبع الأخطاء سيشير إلى مكان وقوع الخطأ في شيفرة برنامجك. وهنا يلعب منقح الصياغة دورًا مهمًا في الإشارة إلى وقوع الأخطاء المطبعية والمشاكل المحتملة لحظة وقوعها، فهذه المنقحات مفيدة جدًا ولا غنى عنها في التطوير الفعال للبرمجيات، وفي حال كون محرر النصوص أو بيئة التطوير التي تستخدمها لا تملك منقحًا وغير قابلة لإضافته، فمن المفضل أن تنتقل إلى أحد المحررات التي تدعم وجود منقحات الصياغة. وتذكر أن البرمجة مجال واسع جدًا، ولا يمكن لأحد أن يحيط بجميع تفاصيله، لذا لا تشعر بالإحباط إن اضطررت للبحث باستمرار عن إجابات لأسئلتك، فحتى مطورو البرمجيات المتمرسون يبحثون في الإنترنت عن التوثيقات والحلول يوميًا، وبدلًا من تضييع وقتك بالإحساس بالإحباط ركز على اكتساب مهارة إيجاد الحلول، وهي خطوة مهمة في طريقك لتصبح خبير بايثون. ترجمة -وبتصرف- للفصل الأول "التعامل مع الأخطاء وطلب المساعدة" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا كيفية التعامل مع الأخطاء البرمجية أساسيات البرمجة بلغة بايثون إعداد بيئة العمل للمشاريع مع بايثون النسخة العربية الكاملة لكتاب البرمجة بلغة بايثون
-
تُعد SQLite قاعدة بيانات SQL قائمة بحد ذاتها self-contained، ومعتمدة على الملفات file-based، وهي مُضمّنة في بايثون افتراضيًا، إذ من الممكن استخدامها في أي من تطبيقات بايثون دون الحاجة لتثبيت أي برمجيات إضافية. سنتعرف في هذا المقال على الوحدة sqlite3 في بايثون 3، إذ سننشئ اتصالاً مع قاعدة بيانات SQLite، كما سنضيف جدولاً إليها، وسندخل بعض البيانات إلى هذا الجدول ونقرأها ونعدّلها. سنتعامل في هذا المقال مع مثال لتطبيق جرد مخزون أسماك في حوض مُفترض، وبالتالي لا بُدّ من التعديل في حال إضافة أو إزالة سمكة من الحوض. مستلزمات العمل لتحقيق أقصى فائدة ممكنة من هذا المقال، يُفضّل أن تكون مُطّلعًا على البرمجة بلغة بايثون وعلى أساسيات لغة SQL، وفي هذا الصدد ننصحك بقراءة المقالات التالية قبل إكمال هذا المقال: مدخل إلى لغة بايثون البرمجية. تعلم أساسيات MySQL. الخطوة الأولى – إنشاء اتصال مع قاعدة بيانات SQLite عند إنشاء اتصال مع قاعدة بيانات SQLite، فإنّنا نصل بالنتيجة إلى بيانات موجودة في ملف ما على جهاز الحاسوب، حيث تُعد محركات قواعد بيانات SQL كاملة الميزات ويمكن استخدامها لأغراض متعددة، وسنتعامل في مقالنا الحالي مع قاعدة بيانات تُعنى بتعقّب مخزون الأسماك في حوض أسماك مُفترض. يمكن الاتصال بقاعدة البيانات SQLite باستخدام الوحدة sqlite3 في بايثون على النحو التالي: import sqlite3 connection = sqlite3.connect("aquarium.db") حيث تستورد التعليمة import sqlite3 الوحدة sqlite3، مما يمنح برنامج بايثون وصولًا إلى هذه الوحدة، بينمّا تعيد الدالة ()sqlite3.connect كائن اتصال Connection والذي سنستخدمه في التخاطب مع قاعدة البيانات SQLite الموجودة في الملف aquarium.db الذي يُنشأ تلقائيًا من قبل الدالة ()sqlite3.connect في حال عدم وجود ملف بنفس الاسم أصلًا في الحاسوب. يمكن التأكد من أنّ الكائن connection قد أُنشئ بنجاح عبر تشغيل الأمر التالي: print(connection.total_changes) وبتشغيل شيفرة بايثون السابقة، سيظهر لنا الخرج التالي: 0 إذ تمثّل سمة الكائن connection.total_changes إجمالي عدد السجلات (الأسطر) التي جرى تغييرها من قبل الكائن connection، ولكننا لم ننفّذ أي تعليمات SQL حتى الآن، ما يعني أنّ العدد 0 صحيح لعدد التغييرات الإجمالي total_changes حاليًا. وفي حال رغبتك بإعادة خطوات هذا المقال من البداية في أي وقت، يمكنك حذف الملف "aquarium.db" من حاسوبك. ملاحظة: من الممكن أيضًا الاتصال بقاعدة بيانات SQLite مُتصلة بالذاكرة مُباشرةً (وليس بملف) عبر تمرير السلسة النصية الخاصّة ":memory:" إلى الدالة ()sqlite3.connect كما يلي: sqlite3.connect(":memory:") من الجدير بالملاحظة أنّ هذا النوع من قواعد البيانات سيختفي بمجرّد إنهاء برنامج بايثون، ما يجعلها مناسبة للحالات التي نحتاج فيها إلى استخدام وضع الحماية مؤقتًا (البيئة التي يمكن للمطور من خلالها العمل على مشروع قاعدة بيانات دون تعريض البيانات للخطر في حالة حدوث خطأ ما) بغية اختبار أمر ما في SQLite دون الحاجة إلى البيانات بعد إنهاء البرنامج. الخطوة الثانية – إضافة بيانات إلى قاعدة البيانات SQLite الآن وبعد أن أنشأنا الاتصال مع قاعدة بيانات SQLite المُتمثلّة بالملف "aquarium.db"، أصبح من الممكن البدء بإدخال البيانات إلى قاعدة البيانات وقراءتها منها؛ إذ تُخزّن البيانات في قواعد بيانات SQLite ضمن جداول، التي تعرّف مجموعةً من الأعمدة قد تكون خالية تمامًا، أو محتوية على سجل واحد أو أكثر، بحيث يتضمّن كل سجل بيانات موافقة للأعمدة المُعرفّة في الجدول. سننشئ جدولًا باسم "fish" يتتبع البيانات التالية: ------------------------------------------------------ | tank_number | species | name | ------------------------------------------------------ | 1 | shark | Sammy | ------------------------------------------------------ | 7 | cuttlefish | Jamie | ------------------------------------------------------ سيتتبع الجدول "fish" قيم الاسم name والنوع species ورقم الخزّان tank_number لكل سمكة في الحوض، وقد ضمّنا مثالين لسجلات الأسماك، الأول لسمكة من نوع قرش shark باسم Sammy، والآخر لسمكة من النوع حبّار cuttlefish باسم Jamie. ومن الممكن إنشاء الجدول fish في SQLite اعتمادًا على الكائن connection المُنشأ في الخطوة الأولى من هذا المقال، على النحو التالي: cursor = connection.cursor() cursor.execute("CREATE TABLE fish (name TEXT, species TEXT, tank_number INTEGER)") إذ تعيد الدالة ()connection.cursor في الشيفرة السابقة كائن مؤشّر Cursor، والذي يمكنّنا من تنفيذ تعليمات SQL على قاعدة البيانات SQLite باستخدام الدالة ()cursor.execute، أمّا السلسة النصية "... CREATE TABLE fish" فهي تعليمة SQL تُنشئ جدولًا باسم fish مُتضمنًا الأعمدة الثلاث التي أشرنا إليها سابقًا وهي الاسم name، الذي يحتوي بيانات من النوع النصي TEXT، والنوع species الذي يحتوي أيضًا بيانات من النوع النصي TEXT، ورقم الخزان tank_number، الذي يحتوي بيانات من نوع عدد صحيح INTEGER. الآن وبعدما أنشأنا الجدول، أصبح من الممكن إدخال سجلات البيانات إليه: cursor.execute("INSERT INTO fish VALUES ('Sammy', 'shark', 1)") cursor.execute("INSERT INTO fish VALUES ('Jamie', 'cuttlefish', 7)") استدعينا في الشيفرة السابقة الدالة ()cursor.execute مرتين، المرة الأولى بغية إدخال السجل الخاص بالسمكة القرش المسماة Sammy إلى الخزان رقم "1"، والثانية لإدخال سمكة الحبار المسمّاة Jamie إلى الخزان رقم "7"، أمّا السلسة النصية "... INSERT INTO fish VALUES" فهي تعليمة SQL مسؤولة عن إدخال السجلات إلى الجدول. أمّا في الخطوة التالية فسنستخدم تعليمة SELECT من تعليمات SQL بغية التحقّق من السجلات المُدخلة إلى جدول الأسماك "fish". الخطوة الثالثة – قراءة بيانات من قاعدة البيانات SQLite أضفنا في الخطوة السابقة سجلين إلى جدول الأسماك "fish" في قاعدة البيانات SQLite، ومن الممكن جلب هذه السجلات باستخدام التعليمة SELECT من تعليمات SQL على النحو التالي: rows = cursor.execute("SELECT name, species, tank_number FROM fish").fetchall() print(rows) وحال تشغيل هذه الشيفرة، سيظهر الخرج التالي: [('Sammy', 'shark', 1), ('Jamie', 'cuttlefish', 7)] شغّلت الدالة ()cursor.execute -في الشيفرة السابقة- تعليمة SELECT بغية جلب قيم كل من أعمدة الاسم والنوع ورقم الخزان من جدول الأسماك "fish"، لتجلب الدالة ()fetchall كافّة نتائج التعليمة SELECT، ولدى تنفيذ التعليمة (print(rows ستظهر قائمةٌ مكونةٌ من سجلين، ولكل سجل ثلاثة مُدخلات يمثّل كل منها عمود من الأعمدة التي اخترناها من جدول الأسماك، إذ يتضمّن هذان السجلان البيانات التي أدخلناها في الخطوة الثانية، بمعنى أنّنا سنحصل على سجل لسمكة القرش "Sammy"، وسجل لسمكة الحبّار "Jamie"، وفي حال كان المطلوب جلب السجلات من جدول الأسماك التي تحقّق مجموعة من المعايير المُحدّدة، فمن الممكن استخدام العبارة WHERE على النحو التالي: target_fish_name = "Jamie" rows = cursor.execute( "SELECT name, species, tank_number FROM fish WHERE name = ?", (target_fish_name,), ).fetchall() print(rows) وعند تشغيل الشيفرة، سنحصل على الخرج التالي: [('Jamie', 'cuttlefish', 7)] تعمل التعليمة ()cursor.execute(<SQL statement>).fetchall في المثال السابق على جلب كافّة النتائج من التعليمة SELECT، بينما تعمل العبارة WHERE في التعليمة SELECT على ترشيح السجلات لتكون فقط تلك التي تكون فيها قيمة العمود name هي target_fish_name، ومن الجدير بالملاحظة أنّه من الممكن استخدام الموضع المؤقت ? بديلًا عن المتغير target_fish_name ضمن التعليمة SELECT، ومن المتوقّع في حالتنا أن يوافق سجلًا واحدًا هذا المعيار، إذ ستكون القيمة المعادة بعد الترشيح هي سجل سمكة الحبّار "Jamie". تنبيه: لا تستخدم عمليات بايثون على السلاسل النصية أبدًا في إنشاء تعليمات SQL ديناميكيًا، إذ يعرّضك استخدام هذه العمليات في تجميع السلاسل النصية لتعليمات SQL إلى خطر هجمات حقن استعلامات SQL المُستخدمة بغية سرقة أو تحريف أو تعديل البيانات المُخزّنة في قاعدة البيانات، وعوضًا عن ذلك استخدم الموضع المؤقت ? في تعليمات SQL عند رغبتك بتعويض القيم تلقائيًا من قبل برنامج بايثون، إذ نمرر مجموعةً من القيم المُجمّعة مثل وسيط ثاني في الدالة ()Cursor.execute لربط هذه القيم بتعليمات SQL، وهذا النمط من التعويض مشروح في مقالنا. الخطوة 4 – تعديل البيانات في قاعدة بيانات SQLite من الممكن تعديل السجلات في قاعدة البيانات SQLite باستخدام تعليمتي UPDATE و DELETE من تعليمات SQL. لنفترض على سبيل المثال أنّه قد نُقل القرش "Sammy" إلى الخزان رقم 2، فعندها من الممكن تعديل سجل هذه السمكة في الجدول "fish" للتعبير عن هذا التغيير على النحو التالي: new_tank_number = 2 moved_fish_name = "Sammy" cursor.execute( "UPDATE fish SET tank_number = ? WHERE name = ?", (new_tank_number, moved_fish_name) ) استخدمنا في الشيفرة السابقة تعليمة UPDATE من تعليمات SQL لتغيير رقم الخزان tank_number للسمكة Sammy إلى القيمة الجديدة "2"، إذ تضمن الجملة WHERE في التعليمة UPDATE أنّه لن تتغير قيمة رقم الخزان إلا عند تحقق شرط وهو أن يكون اسم السمكة هو Sammy أي "name = "Sammy، ومن الممكن التأكّد من تنفيذ التعديل بالشكل الصحيح من خلال تشغيل تعليمة SELECT التالية: rows = cursor.execute("SELECT name, species, tank_number FROM fish").fetchall() print(rows) وبتشغيل الشيفرة السابقة سنحصل على الخرج التالي: [('Sammy', 'shark', 2), ('Jamie', 'cuttlefish', 7)] فنلاحظ أنّ السجل الخاص بالسمكة "Sammy" يملك القيمة "2" مثل رقم للخزان ضمن العمود tank_number. الآن لنفترض أنّنا حرّرنا القرش Sammy إلى الطبيعة، وبالتالي لم يعد موجودًا في الحوض، عندها يتوجّب حذف السجل الخاص به من الجدول "fish"، لذا سنستخدم التعليمة DELETE من تعليمات SQL لحذف السجل المطلوب كما يلي: released_fish_name = "Sammy" cursor.execute( "DELETE FROM fish WHERE name = ?", (released_fish_name,) ) استخدمنا في الشيفرة السابقة التعليمة DELETE من تعليمات SQL لحذف سجل السمكة Sammy من النوع shark، إذ ضمنت الجملة WHERE في التعليمة DELETE أنّه لن يُحذف السجل إلّا عند تحقق شرط كون اسم السمكة هو Sammy أي "name = "Sammy، ومن الممكن التأكّد من تنفيذ الحذف بالشكل الصحيح من خلال تشغيل تعليمة SELECT التالية: rows = cursor.execute("SELECT name, species, tank_number FROM fish").fetchall() print(rows) وبتشغيل الشيفرة السابقة سنحصل على الخرج التالي: [('Jamie', 'cuttlefish', 7)] فنلاحظ أنّه قد حُذف فعلًا السجل الخاص بالسمكة Sammy من النوع shark، ولم يتبقَ سوى سجل سمكة Jamie من نوع الحبّار cuttlefish. الخطوة 5 – استخدام تعليمة with للإغلاق الآلي استخدمنا في هذا المقال كائنين رئيسين للتعامل مع قاعدة البيانات "aquarium.db" من النوع SQLite وهما: كائن اتصال باسم connection وكائن مؤشّر باسم cursor. يتوجّب إغلاق ملفات بايثون بعد الانتهاء من العمل عليها، وكذلك الأمر بالنسبة للكائنات مثل Connection و Cursor، إذ يجب إغلاقها عند الانتهاء من استخدامها، ومن الممكن استخدام العبارة with لمساعدتنا على إغلاق الكائنات Connection و Cursor تلقائيًا على النحو التالي: from contextlib import closing with closing(sqlite3.connect("aquarium.db")) as connection: with closing(connection.cursor()) as cursor: rows = cursor.execute("SELECT 1").fetchall() print(rows) تُعد الدالة closing من الدوال سهلة الاستخدام التي توفّرها الوحدة contextlib، فعند إنهاء التعليمة with، تضمن closing استدعاء الدالة ()close بغض النظر عن الكائن المُمرّر إليها، وفي مثالنا استخدمنا الدالة closing مرتين، الأولى لضمان الإغلاق التلقائي للكائن Connection المُعاد من الدالة ()sqlite3.connect، والثانية لضمان الإغلاق التلقائي للكائن Cursor المُعاد من الدالة ()connection.cursor، وبتشغيل الشيفرة السابقة سنحصل على الخرج التالي: [(1,)] وبما أنّ التعليمة "SELECT 1" هي تعليمة SQL تعيد دومًا سجلًا وحيدًا بعمود وحيد قيمتة "1"، فمن المنطقي في هذه الحالة الحصول على سجل يحتوي فقط القيمة "1" بمثابة قيمة معادة من الشيفرة. الخاتمة تمثّل الوحدة sqlite3 جزءًا فعّالًا من مكتبة بايثون المعيارية، إذ تمكنّنا من العمل مع قاعدة بيانات SQL محلية كاملة الميزات دون الحاجة لتثبيت أي برمجيات إضافية. استعرضنا في هذا المقال كيفية استخدام الوحدة sqlite3 للاتصال مع قاعدة بيانات SQLite، وكيفية إضافة البيانات إليها وقراءتها منها وتعديلها، كما نوهّنا لأخطار هجمات حقن استعلامات SQL، وبيّنا كيفية استخدام contextlib.closing لاستدعاء الدالة ()close تلقائيًا وتطبيقها على كائنات بايثون الموجودة ضمن عبارات with. ترجمة -وبتصرف- للمقال How To Use the sqlite3 Module in Python 3 لصاحبه DavidMuller. اقرأ أيضًا إضافة PHP MySQLi ونظام إدارة قواعد البيانات SQLite3 التعامل مع قواعد البيانات SQLite في تطبيقات Flask النسخةالعربية الكاملة لكتاب البرمجة بلغة بايثون