خرافات حول ممارسات تجنب أخطاء محتملة في شيفرات لغة بايثون – بايثون

196

[ad_1]

تعرفنا في المقال السابق على مفهوم “روائح” الشيفرات 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 ودون أن تحصر إحداها كامل شيفرة الدالة.

خرافة أن الوسيط المنطقي خيار سيئ

يطلق على الوسطاء من النوع قيم منطقية 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.

اقرأ أيضًا

[ad_2]

المصدر