الفصل 19

الدوال — قرار واحد باسم واحد

كتابة الدوال باستخدام def، الفرق الجوهري بين return و print، معالجة الحالات الخاصة والاستثنائية بالحراس (guards) في المقدمة، وسلاسل التوثيق (docstrings).

35 دقيقةPython 3.12
  1. 1المشكلة
  2. 2الفهم
  3. 3أمثلة محلولة
  4. 4التوقع
  5. 5التطبيق
  6. 6التحدي

المشكلة التي نقوم بحلها

لدينا مجموعتان من الطلاب ونريد حساب متوسط درجات كل منهما:

python
marks_a = [72, 45]
marks_b = [90, 33, 61]

print(round(sum(marks_a) / len(marks_a), 2))
print(round(sum(marks_b) / len(marks_b), 2))
text
58.5
61.33

السطران متطابقان تقريباً؛ والفارق الوحيد هو اسم المتغير. وإذا ظهرت مجموعة ثالثة فسنضيف سطراً ثالثاً، والمجموعة الرابعة تعني سطراً رابعاً.

المشكلة لا تكمن في الجهد المبذول في الكتابة — فالنسخ واللصق أمر سهل ورخيص. بل المشكلة الحقيقية هي أن قراراً حسابياً واحداً أصبح مكتوباً ومكرراً في عدة أماكن مختلفة. فإذا طُلب منك تقريب المتوسط إلى منزلة عشرية واحدة بدلاً من اثنتين، سيكون عليك البحث عن كل سطر وتعديله يدوياً؛ ونسيان سطر واحد فقط سيجعل البرنامج يتصرف بطريقتين مختلفتين في آن واحد — ودون أن يُطلق بايثون أي رسالة خطأ تنبهك!

وماذا لو كانت إحدى القوائم فارغة؟ التعبير sum([]) / len([]) سيقسم على الصفر وسيتوقف البرنامج فوراً. وسيكون عليك إضافة هذا الفحص الوقائي لكل سطر على حدة.

الدالة (Function) هي الطريقة المثلى لكتابة قرار برمجي واحد مرة واحدة فقط وإعطائه اسماً دالاً. بعد ذلك، يتولى استدعاء ذلك الاسم إنجاز العمل بالكامل، ويصبح إجراء أي تعديل مستقبلي محصوراً في مكان واحد فقط.

في نهاية هذا الدرس ستكون قادراً على

  • كتابة الدوال واستدعاؤها باستخدام كلمة def
  • شرح الفارق الجوهري بين return و print — وهو المفهوم الأهم على الإطلاق هنا
  • الحفاظ على حجم الدوال صغيراً ومقصوراً على أداء مهمة واحدة محددة
  • معالجة الحالات الحدية والخاصة باستخدام جمل الحراسة (guard clauses) في البداية
  • كتابة نصوص التوثيق (docstrings) للدوال

المتطلبات السابقة: اشتقاق المجموعات والقوائم — Comprehensions.


دالتك الأولى

python
def average(values):
    return round(sum(values) / len(values), 2)


print(average([72, 45]))
print(average([90, 33, 61]))
text
58.5
61.33

الشكل العام للدالة:

  • def — كلمة مفتاحية تعني "أنا أعرّف دالة الآن"
  • average — اسم الدالة، ويتبع نفس قواعد تسمية المتغيرات
  • (values) — المعامل (parameter)، وهو الباب الذي تدخل منه البيانات إلى الدالة
  • : مع كتلة برمجية مُزاحة بمسافة بادئة — جسم الدالة، تماماً كما في if أو for
  • return — إرجاع النتيجة المحسوبة إلى من استدعى الدالة

كتابة average([72, 45]) تُسمى استدعاءً (calling) للدالة. القائمة [72, 45] هي الوسيط (argument)، أي القيمة الفعلية الممررة للدالة؛ بينما values هو المعامل (parameter)، أي اسم المتغير الداخلي الذي يستقبل تلك القيمة داخل الدالة. هذان المصطلحان يظهران في رسائل الخطأ، لذا يجدر الانتباه للفرق بينهما.

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

الأمر return ليس مثل الدالة print!

هذا هو القسم الأكثر أهمية في هذا الدرس، ومصدر الارتباك الأكبر لدى المبتدئين.

python
def with_return(x):
    return x * 2


def with_print(x):
    print(x * 2)


a = with_return(5)
b = with_print(5)

print(a)
print(b)
print(a + 1)
text
10
10
None
11

أول رقمين 10 ظهرا على الشاشة يجعلان الدالتين تبدوان متطابقتين للوهلة الأولى. لكنهما ليستا كذلك على الإطلاق!

الدالة with_return أعادت القيمة وسلمتها للبرنامج، لذا احتفظ المتغير a بالرقم 10 وأصبح بالإمكان إجراء عمليات رياضية عليه — ولذا ظهر 11 في السطر الأخير.

أما الدالة with_print فقد عرضت القيمة على الشاشة فقط ولم تُرجع أي شيء للبرنامج، ولذا أصبحت قيمة المتغير b هي None. الرقم ظهر للمستخدم على الشاشة، لكن البرنامج نفسه لم يستلم شيئاً على الإطلاق! ولو حاولت كتابة b + 1 لواجهت خطأ TypeError.

احفظ هذه القاعدة الذهبية دائماً: print موجهة للإنسان، بينما return موجهة للبرنامج. فالدالة التي تقوم بالحسابات يجب أن تستخدم return؛ أما الطباعة والعرض فيحدثان في المكان الذي تم فيه استدعاء الدالة. وبهذه الطريقة، يمكن لنفس الدالة أن تُرسل نتيجتها إلى الشاشة اليوم، وتكتبها في ملف غداً دون أي تعديل.

الدالة التي لا تحتوي على أمر return ترجع تلقائياً وبشكل صامت القيمة None — وهي نفس القيمة None التي كانت تعيدها الدوال append و .sort() في الدرسين الثاني عشر والثالث عشر. والآن يمكنك أن تدرك سبب ذلك بوضوح: فمهمتها كانت تعديل شيء ما في المكان، وليس إرجاع أي قيمة جديدة.

الأمر return يوقف تنفيذ الدالة فوراً

python
def check(mark):
    if mark >= 40:
        return "pass"
    return "fail"


print(check(72))
print(check(20))
text
pass
fail

في اللحظة التي يتم فيها تنفيذ return، ينتهي عمل الدالة فوراً — والأسطر التي تليه لا تُنفذ أبداً. ولهذا السبب لم نكن بحاجة إلى كتابة else هنا: فإذا تحقق الشرط ونُفذ أمر return الأول، يصبح السطر الثاني غير قابل للوصول إليه أصلاً.

هذا النمط — الخروج الفوري بمجرد تحقق الشرط — يختصر التفرعات العميقة لجمل if/else من الدرس التاسع، ويجعل الكود مسطحاً ومريحاً جداً في القراءة.

التعامل مع الحالات الحدية — جمل الحراسة (Guards)

تذكر المشكلة التي بدأنا بها هذا الفصل؟ القائمة الفارغة:

python
def average(values):
    return sum(values) / len(values)


print(average([]))
text
ZeroDivisionError: division by zero

الحل يوضع في قمة الدالة مباشرة وقبل أي عمل آخر:

python
def average(values):
    if not values:
        return 0.0
    return round(sum(values) / len(values), 2)


print(average([72, 45]))
print(average([]))
text
58.5
0.0

الشرط if not values هو تطبيق لقاعدة التحقق المنطقي من الدرس الثامن: القائمة الفارغة تُعامل كقيمة خاطئة (False)، لذا فإن not تجعلها صحيحة (True). لا حاجة لكتابة len(values) == 0.

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

هل 0.0 هي الإجابة الصحيحة للقائمة الفارغة؟ هذا محل نقاش منطقي. فهو يخلط بين "لا أحد يملك درجات" وبين "الجميع حصلوا على صفر". في البرامج الواقعية، غالباً ما تُرجع الدالة None، أو تطلق استثناءً صريحاً باستخدام raise التي سنتعلمها في الدرس الرابع والعشرين. استخدمنا 0.0 هنا للتبسيط — ولكن اعلم أن هذا قرار تصميمي وليس بالضرورة الإجابة البديهية الوحيدة.

نصوص التوثيق (Docstrings)

أي نص مكتوب بين علامات اقتباس ثلاثية في أول سطر من جسم الدالة يُعتبر توثيقاً رسمياً لها:

python
def average(values):
    """Return the mean of values, or 0.0 when there are none."""
    if not values:
        return 0.0
    return round(sum(values) / len(values), 2)


print(average([1, 2, 3]))
print(average.__doc__)
text
2.0
Return the mean of values, or 0.0 when there are none.

ما يميز نص التوثيق عن التعليقات العادية هو أنه يبقى حياً ومحفوظاً داخل البرنامج أثناء تشغيله — فمحررات الأكواد تعرضه تلقائياً كإرشاد للمطور، وتطبعه دالة help(average).

يوضح نص التوثيق الجيد ما الذي تُعيده الدالة وماذا يحدث في الحالات الحدية الخاصة، وليس تفاصيل كيفية عمل الكود البرمجي خطوة بخطوة؛ فالكود موجود أمامك في الأسفل بالفعل.


مثال متكامل

report.py:

python
# Three small functions, each doing one thing
def average(values):
    """Return the mean of values, or 0.0 when there are none."""
    if not values:
        return 0.0
    return round(sum(values) / len(values), 2)


def grade(mark):
    """Turn a mark into a letter."""
    if mark >= 80:
        return "A"
    if mark >= 70:
        return "B"
    if mark >= 40:
        return "C"
    return "F"


def summarise(name, marks):
    """Build one report line for a person."""
    mean = average(marks)
    return f"{name:<8} {len(marks):>2} papers  avg {mean:>6}  grade {grade(mean)}"


people = {
    "rafi": [72, 88, 61],
    "ahmed": [45, 38],
    "dia": [],
}

for name, marks in people.items():
    print(summarise(name, marks))
text
rafi      3 papers  avg  73.67  grade B
ahmed     2 papers  avg   41.5  grade C
dia       0 papers  avg    0.0  grade F

أربع نقاط تستحق التأمل:

كل دالة تؤدي مهمة واحدة فقط. الدالة average تحسب المتوسط، و grade تعطي التقدير، و summarise تبني سطراً في التقرير. لا توجد أي دالة منها تقوم بالطباعة — فالطباعة تحدث في مكان واحد محدد داخل الحلقة في النهاية. وبالتالي فإن تغيير هذا التقرير ليُكتب في ملف بدلاً من الشاشة يتطلب تعديل سطر واحد فقط!

الدوال تستدعي دوالاً أخرى. الدالة summarise تستدعي كلاً من average و grade. هذه هي الفائدة العظمى للبرمجة التركيبية: بناء مهام وأنظمة برمجية ضخمة من خلال تجميع قطع صغيرة وموثوقة.

لا تحتوي الدالة grade على أي else. كل أمر return ينهي عمل الدالة فوراً، لذا بقيت الشروط مسطحة وأنيقة. ولا تزال قاعدة الدرس التاسع سارية: الشرط الأكثر صرامة يوضع أولاً — فلو وضعنا 40 قبل 80 لحصل الجميع على تقدير C!

القائمة الفارغة الخاصة بـ dia لم تُعطل البرنامج. حارس الدالة أعاد 0.0، وحوّلت دالة grade هذه القيمة إلى F، وطُبع سطر التقرير بكل سلاسة. لقد عولجت الحالة الحدية مرة واحدة، وفي مكان واحد مركزي.


حالات الخطأ الشائعة

NameError: name 'greet' is not defined — رغم أن الدالة مكتوبة في الملف! تم استدعاء الدالة قبل سطر تعريفها def. تقرأ بايثون الملف من الأعلى إلى الأسفل بالترتيب، والاسم لا يكون موجوداً حتى يتم تنفيذ سطر def. ضع دائماً جميع تعريفات الدوال في الجزء العلوي من الملف، وضع كود الاستدعاء والتنفيذ في الأسفل.

TypeError: average() missing 1 required positional argument: 'values' تم استدعاء الدالة دون تمرير الوسيط المطلوب — مثل كتابة average() بدلاً من average(marks). بل إن رسالة الخطأ تحدد لك صراحة اسم المعامل المفقود.

الدالة ترجع القيمة None لا يوجد أمر return في الدالة، أو أنه موجود داخل كتلة شرطية لم يتحقق شرطها، أو أنك كتبت print في مكان كان ينبغي فيه كتابة return.

متغير داخل الدالة غير مرئي خارجها هذا سلوك طبيعي ومتوقع — وسنتناول أسبابه بالتفصيل في الدرس الحادي والعشرين حول نطاقات المتغيرات (scope). في الوقت الحالي: استخدم return لإرجاع أي قيمة تحتاجها في الخارج.

خطأ SyntaxError — نسيان النقطتين الرأسيتين في سطر def مثل if و for، يجب أن ينتهي سطر def بنقطتين رأسيتين دائماً.

IndentationError: expected an indented block after function definition يحتاج سطر def إلى سطر واحد على الأقل مزاح بمسافة بادئة بعده.