اكتشاف دلالات الأخطاء في شيفرات لغة بايثون – بايثون

202

[ad_1]

مما لا شك فيه أن الشيفرة التي تسبب توقف عمل البرنامج هي خاطئة حُكمًا، والسؤال: هل هذا النوع من الأعطال هو الدليل الوحيد على وجود مشاكل في البرنامج؟ بالطبع لا، فبعض الإشارات قد تدل على وجود أخطاء خفية أو على كون مقروئية الشيفرة ضعيفة. وكما هو الحال مع رائحة الغاز التي قد تشير إلى وجود تسرّب في الغاز أو رائحة الدخان التي قد تشير لنشوب حريق، “فرائحة” الشيفرة 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 أيضًا.

يعد تكرار الشيفرات من دلالات وقوع الأخطاء لأنه يجعل من شيفرتك أصعب للتعديل، ففي حال وجود العديد من التكرارات في برنامجك، يكون الحل بحصر الجزء المكرر ضمن حلقة أو دالة ليظهر لمرة واحدة فقط.

الأرقام السحرية

ليس عجبًا إن قلنا أنّ البرمجة تتضمن استخدام الأرقام، إلا أن بعض الأرقام التي تظهر ضمن شيفرتك المصدرية قد تكون مصدر إرباك لمبرمج آخر يقرؤها (أو حتى لك أنت بعد مرور عدة أسابيع على كتابتك لها) وهي ما ندعوه بالأرقام السحرية 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 مؤقتًا ضمن البرنامج بغية عرض قيم المتغيرات قبل تشغيل البرنامج، وعادةً ما تمر هذه العملية بالخطوات التالية:

  1. ملاحظة وجود خطأ في البرنامج.
  2. إضافة بعض الاستدعاءات للدالة ()print لبعض المتغيرات في محاولة معرفة ما تحتويه.
  3. إعادة تشغيل البرنامج.
  4. إضافة المزيد من الاستدعاءات للدالة ()print لأن الاستدعاءات السابقة لم تعرض ما يكفي من معلومات.
  5. إعادة تشغيل البرنامج.
  6. تكرار الخطوتين السابقتين عدة مرات إلى حين اكتشاف موضع الخطأ.
  7. إعادة تشغيل البرنامج.
  8. إدراك أنك قد نسيت حذف بعضًا من استدعاءات الدالة ()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.

اقرأ أيضًا

[ad_2]

المصدر