توثيق الدوال

البرمجة الصحيحة تعتمد على "عقد" بين طرفين:

  • المبرمج المُنَفِّذ: الذي يكتب التفاصيل الوحدة البرمجية.
  • المبرمج المُستفيد: الذي سيستخدم هذه الدالة في برنامجه.

وهذا العقد يسمى الواجهة البرمجية للتطبيق (API)؛ ويتم الاتفاق عليه بين الأعضاء المبرمجين في فِرَق البرمجة، ويتم اعتماده من مهندس أعلى قبل البدء بتفاصيله.

لابد من توثيق الدالة لضبط هذا العقد خصوصًا إذا كانت:

  1. جزءًا من مكتبة يستفيد منها غيرنا.
  2. طويلة لدرجة أنها تحتاج إلى شرح.
  3. ذات منطق معقد لا يتضح من اسمها ومعطياتها.

ضع في ذهنك أن من يستعمل الدالة ليس عنده نفس السياق الذي هو حاضر في ذهننا؛ بل قد نعود نحن إلى الدالة بعد مدة، فنحتاج إلى هذا الشرح الذي كتبناه فنجده.

والتوثيق الجيد يكفي لاستعمال الدالة دون قراءة تفاصيلها (الكود الذي فيها). فهو يبيّن كيف نستدعيها، وما المعطيات التي تقبلها، وما النتيجة التي تعيدها، وما الشروط التي ينبغي مراعاتها.

الفرق بين التوثيق الخارجي والتعلقيات

ثم يجب التفريق بين أمرين في التوثيق التقني للوحدات البرمجية:

  • نص التوثيق الخارجي (docstring): وهو النص الظاهر للمطور المستفيد.
    • غرضه: بيان أمثل طريقة للاستفادة من الدالة.
    • يظهر مثلاً عند وضع المؤشر على اسم الدالة أو في صفحات التوثيق.
    • يشرح الماهيَّة الوظيفية لهذه الوحدة البرمجية (الدالة مثلاً) في سياق المكتبة.
  • التعليقات (code comments): وهي ملاحظات في ثنايا تفاصيل الدالة يعنى بها المطور المنفذ.
    • غرضها: الصيانة والتحسين والتطوير.
    • لا يظهر إلا بفتح النص البرمجي نفسه.
    • يبين فيها سبب سلوك طريقة معيَّنة في التفاصيل (مثلاً: اختيار خوارزميَّة معيَّنة) وبعض المحاذير

وموضوعنا في هذا الدرس هو الأول: نص التوثيق الخارجي (docstring).

أركان نص التوثيق

لكي ينجح هذا العقد بين المستفيد والمفيد، يجب أن يُكتب على شقين:

  1. الوصف البرمجي (يفهمه الحاسب): كتابة اسم الدالة، ونوع المدخلات التي تقبلها، ونوع المخرجات (مثلاً: دالة تقبل "نص" وتُخرج "قيمة منطقية صح/خطأ").
  2. الوصف الخطابي (يفهمه البشر): تعليقات تُكتب في رأس الدالة تشرح الشروط غير البرمجية، مثل: "يجب ألا يحتوي النص المدخل على فراغات"، أو "هذه الدالة تعدل على الملف الفلاني".

نسير في هذا الدرس على طريقة جوجل في توثيق دوال بايثون، حيث رتب النص في أقسام:

  • Args:: معنى كل معطى وشروطه.
  • Returns:: معنى النتيجة التي تعيدها الدالة.
  • Raises:: الأخطاء التي قد تقع، ومتى تقع.

رأس الدالة: الاسم والمعطيات

نريد دالة تقسم عددًا على عدد آخر. مثلًا، قسمة 12.0 على 4.0 تعطي 3.0:

لا يلزم أن يفهم المستفيد كل هذه التفاصيل كي يستعمل الدالة. بل إن ما يهتم له هو السطر الأول: ويسمى رأس الدالة (Function Signature)؛ وهو أمران:

  1. اسمها divide
  2. والمعطيات التي تقبلها numerator وdenominator

وعند كتابة اسمها ووضع المؤشر عليها يظهر له ذلك الرأس؛ فيمرر لها معطياتها:

تكون النتيجة 3.0. رأس الدالة قصير، لكنَّه لا يوضح وحده نوع المعطيين، ولا أنَّ المقام يجب أن يكون غير صفر.

إضافة الأنواع

نضيف نوع كل معطى بعد :، ونوع النتيجة بعد ->. (تبرز الكتلة التالية ما تغير عن الكتلة السابقة):

  • numerator: float: نتوقع عددًا يمثل البسط.
  • denominator: float: نتوقع عددًا يمثل المقام.
  • -> float: تعيد الدالة نتيجة عددية عشرية.

هذه تعليقات نوعية تساعد القارئ والمحرر وأدوات فحص الأنواع. لا تحول القيم تلقائيًا، ولا تفرض بايثون هذه الأنواع وقت التشغيل. مثلًا، كتابة float لا تحوّل النص "12" إلى العدد 12.0.

سطر التلخيص

نضع نص التوثيق docstring بين ثلاث علامات اقتباس مزدوجة، بوصفه أول عبارة داخل جسم الدالة:

وصف المعطيات: Args

نترك سطرًا فارغًا بعد التلخيص، ثم نصف معنى كل معطى وشروطه. نستخدم أسماء المعطيات نفسها الموجودة في رأس الدالة:

النوع موجود في رأس الدالة، فلا نكرره في الوصف. نوضح هنا معنى البسط والمقام، وأنَّ المقام يجب أن يكون غير صفر. كتابة هذا الشرط في التوثيق لا تضيف فحصًا له في الكود.

وصف النتيجة: Returns

نضيف وصف ما تعيده الدالة، مع التفاصيل التي يحتاجها المستدعي:

يشرح Returns معنى الناتج. نحذف القسم إذا كانت الدالة لا تعيد نتيجة مفيدة.

وصف الاستثناءات: Raises

نختار أن ترفع دالتنا ValueError إذا كان المقام صفرًا. نضيف فحصًا قبل القسمة، ونوثق هذا السلوك في قسم Raises:

في هذه الدالة، نفحص المقام بـ if، ثم نرفع الخطأ بـ raise إذا كان صفرًا. من دون هذا الفحص، سترفع عملية القسمة نفسها ZeroDivisionError؛ اخترنا هنا ValueError ضمن سلوك دالتنا الموثّق. لا نضيف هذا القسم إذا لم تكن هناك استثناءات نريد توثيقها ضمن سلوك الدالة.

أمثلة للاستعمال

نضيف أخيرًا استدعاءات قصيرة مع نتائجها المتوقعة:

  • بعد >>> نكتب كود بايثون الذي سينفّذه المفسّر.
  • في السطر التالي نكتب النتيجة المتوقعة، دون >>>.

الأمثلة السابقة توضح قسمة عدد موجب، وقسمة البسط صفرًا التي تعيد 0.0، ونتيجة سالبة غير صحيحة، والاستثناء الموثّق عند المقام صفرًا. نكتب ... بدل التفاصيل المتغيرة في كومة الاستدعاءات؛ يفحص doctest نوع الاستثناء ورسالته. تظل داخل نص التوثيق: لا تُنفذ تلقائيًا عند تعريف الدالة أو استدعائها.

تشغيل الأمثلة باستخدام doctest

الوحدة doctest تأتي مع بايثون. تقرأ الأوامر بعد >>>، وتشغلها، ثم تقارن النتيجة الفعلية بالنص المتوقع. نص التوثيق يصبح بذلك أمثلة يمكن فحصها.

هذه نسخة مكتملة يمكن تشغيلها وحدها. أضفنا استيراد doctest وطلب فحص أمثلة نصوص التوثيق في البرنامج:

  • if __name__ == "__main__":: يحمي أسطر الاختبار التي تحته من التنفيذ عند استيراد الملف بـ import. مثلًا، لو احتوت مكتبة math على هذا الشرط، فلن تعمل الأسطر التي تحته عندما نكتب import math؛ نستفيد من دوالها دون تشغيل اختباراتها. أمَّا حين يشغّل المختبِرون ملف المكتبة مباشرة، فيتحقق الشرط وتعمل تلك الأسطر لفحص أمثلة نصوص التوثيق.
  • doctest.testmod(): يبحث عن الأمثلة في نصوص توثيق البرنامج ودواله، ثم يشغّلها ويقارن النتائج بما كتبناه.
  • verbose=True: يعرض كل محاولة وملخص النتائج في النهاية.
  • exclude_empty=True: يستبعد من الملخص العناصر التي لا تحتوي على أمثلة.

خلاصة

العقد بين المنفِّذ والمستفيد هو رأس الدالة مع نص توثيقها. الرأس يحدّد اسم الدالة ومعطياتها وأنواعها ونوع النتيجة، ونص التوثيق يبيّن عملها وشروط استعمالها والأخطاء المتوقعة وأمثلة الاستعمال.