الدوال — قرار واحد باسم واحد
كتابة الدوال باستخدام def، الفرق الجوهري بين return و print، معالجة الحالات الخاصة والاستثنائية بالحراس (guards) في المقدمة، وسلاسل التوثيق (docstrings).
كيف تريد أن تتعلم؟
المشكلة التي نقوم بحلها
لدينا مجموعتان من الطلاب ونريد حساب متوسط درجات كل منهما:
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))58.5
61.33السطران متطابقان تقريباً؛ والفارق الوحيد هو اسم المتغير. وإذا ظهرت مجموعة ثالثة فسنضيف سطراً ثالثاً، والمجموعة الرابعة تعني سطراً رابعاً.
المشكلة لا تكمن في الجهد المبذول في الكتابة — فالنسخ واللصق أمر سهل ورخيص. بل المشكلة الحقيقية هي أن قراراً حسابياً واحداً أصبح مكتوباً ومكرراً في عدة أماكن مختلفة. فإذا طُلب منك تقريب المتوسط إلى منزلة عشرية واحدة بدلاً من اثنتين، سيكون عليك البحث عن كل سطر وتعديله يدوياً؛ ونسيان سطر واحد فقط سيجعل البرنامج يتصرف بطريقتين مختلفتين في آن واحد — ودون أي رسالة خطأ.
وماذا لو كانت إحدى القوائم فارغة؟ التعبير sum([]) / len([]) سيقسم على الصفر وسيتوقف البرنامج فوراً. وسيكون عليك إضافة هذا الفحص الوقائي لكل سطر على حدة.
الدالة (Function) هي الطريقة المثلى لكتابة قرار برمجي واحد مرة واحدة فقط وإعطائه اسماً دالاً. بعد ذلك، يتولى استدعاء ذلك الاسم إنجاز العمل بالكامل، ويصبح إجراء أي تعديل مستقبلي محصوراً في مكان واحد فقط.
في نهاية هذا الفصل ستكون قادراً على
- كتابة الدوال واستدعاؤها باستخدام كلمة
def - شرح الفارق الجوهري بين
returnوprint— وهو المفهوم الأهم على الإطلاق هنا - الحفاظ على حجم الدوال صغيراً ومقصوراً على أداء مهمة واحدة محددة
- معالجة الحالات الحدية والخاصة باستخدام جمل الحراسة (guard clauses) في البداية
- كتابة نصوص التوثيق (docstrings) للدوال
المتطلبات السابقة: اشتقاق المجموعات والقوائم — Comprehensions.
دالتك الأولى
def average(values):
return round(sum(values) / len(values), 2)
print(average([72, 45]))
print(average([90, 33, 61]))58.5
61.33الشكل العام للدالة:
def— كلمة مفتاحية تعني "أنا أعرّف دالة الآن"average— اسم الدالة، ويتبع نفس قواعد تسمية المتغيرات(values)— المعامل (parameter)، وهو الباب الذي تدخل منه البيانات إلى الدالة:مع كتلة برمجية مُزاحة بمسافة بادئة — جسم الدالة، تماماً كما فيifأوforreturn— إرجاع النتيجة المحسوبة إلى من استدعى الدالة
كتابة average([72, 45]) تُسمى استدعاءً (calling) للدالة. القائمة [72, 45] هي الوسيط (argument)، أي القيمة الفعلية الممررة للدالة؛ بينما values هو المعامل (parameter)، أي اسم المتغير الداخلي الذي يستقبل تلك القيمة داخل الدالة. هذان المصطلحان يظهران في رسائل الخطأ، لذا يجدر الانتباه للفرق بينهما.
سطران فارغان: يُعد ترك سطرين فارغين بعد تعريف أي دالة عرفاً اصطلاحياً قياسياً في بايثون (وفق دليل أسلوب PEP 8). سيعمل البرنامج بشكل طبيعي حتى لو تركت سطراً واحداً فقط، لكن كل الأكواد الاحترافية في بايثون تُكتب بهذه الطريقة، لذا من المفيد التعود عليها من الآن.
الأمر return ليس مثل الدالة print
هذا هو القسم الأكثر أهمية في هذا الفصل، ومصدر الارتباك الأكبر لدى المبتدئين.
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)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() في الفصلين 12 و 13. والآن يمكنك أن تدرك سبب ذلك بوضوح: فمهمتها كانت تعديل شيء ما في المكان، وليس إرجاع أي قيمة جديدة.
الأمر return يوقف تنفيذ الدالة فوراً
def check(mark):
if mark >= 40:
return "pass"
return "fail"
print(check(72))
print(check(20))pass
failفي اللحظة التي يتم فيها تنفيذ return، ينتهي عمل الدالة فوراً — والأسطر التي تليه لا تُنفذ أبداً. ولهذا السبب لم نكن بحاجة إلى كتابة else هنا: فإذا تحقق الشرط ونُفذ أمر return الأول، يصبح السطر الثاني غير قابل للوصول إليه أصلاً.
هذا النمط — الخروج الفوري بمجرد تحقق الشرط — يختصر التفرعات العميقة لجمل if/else من الفصل 9، ويجعل الكود أسهل في القراءة.
التعامل مع الحالات الحدية — جمل الحراسة (Guards)
تذكر المشكلة التي بدأنا بها هذا الفصل؟ القائمة الفارغة:
def average(values):
return sum(values) / len(values)
print(average([]))ZeroDivisionError: division by zeroالحل يوضع في قمة الدالة مباشرة وقبل أي عمل آخر:
def average(values):
if not values:
return 0.0
return round(sum(values) / len(values), 2)
print(average([72, 45]))
print(average([]))58.5
0.0الشرط if not values هو تطبيق لقاعدة التحقق المنطقي من الفصل 8: القائمة الفارغة تُعامل كقيمة خاطئة (False)، لذا فإن not تجعلها صحيحة (True). لا حاجة لكتابة len(values) == 0.
يُسمى هذا النمط جملة الحراسة (Guard Clause): تخلص من الحالات الشاذة وغير الطبيعية في المقدمة أولاً، ثم اكتب باقي منطق الدالة براحة بال تامة. تجعل الدوال تطبيق هذا الأسلوب غير مكلف على الإطلاق — فالتحقق يُكتب مرة واحدة ويستفيد منه كل من يستدعي الدالة.
هل0.0هي الإجابة الصحيحة للقائمة الفارغة؟ هذا محل نقاش منطقي. فهو يخلط بين "لا أحد يملك درجات" وبين "الجميع حصلوا على صفر". في البرامج الواقعية، غالباً ما تُرجع الدالةNone، أو تطلق استثناءً صريحاً باستخدامraiseالتي سنتعلمها في الفصل 24. استخدمنا0.0هنا للتبسيط — ولكن اعلم أن هذا قرار تصميمي وليس بالضرورة الإجابة البديهية الوحيدة.
نصوص التوثيق (Docstrings)
أي نص مكتوب بين علامات اقتباس ثلاثية في أول سطر من جسم الدالة يُعتبر توثيقاً رسمياً لها:
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__)2.0
Return the mean of values, or 0.0 when there are none.ما يميز نص التوثيق عن التعليقات العادية هو أنه يبقى حياً ومحفوظاً داخل البرنامج أثناء تشغيله — فمحررات الأكواد تعرضه تلقائياً كإرشاد للمطور، وتطبعه دالة help(average).
يوضح نص التوثيق الجيد ما الذي تُعيده الدالة وماذا يحدث في الحالات الحدية الخاصة، وليس تفاصيل كيفية عمل الكود البرمجي خطوة بخطوة؛ فالكود موجود أمامك في الأسفل بالفعل.
مثال متكامل
report.py:
# 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))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 ينهي عمل الدالة فوراً، لذا بقيت الشروط مسطحة. ولا تزال قاعدة الفصل 9 سارية: الشرط الأكثر صرامة يوضع أولاً — فلو وضعنا 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.
متغير داخل الدالة غير مرئي خارجها هذا سلوك طبيعي ومتوقع — وسنتناول أسبابه بالتفصيل في الفصل 21 حول نطاقات المتغيرات (scope). في الوقت الحالي: استخدم return لإرجاع أي قيمة تحتاجها في الخارج.
خطأ SyntaxError — نسيان النقطتين الرأسيتين في سطر def مثل if و for، يجب أن ينتهي سطر def بنقطتين رأسيتين دائماً.
IndentationError: expected an indented block after function definition يحتاج سطر def إلى سطر واحد على الأقل مزاح بمسافة بادئة بعده.
الخطوة ٤ من ٦ — التوقع
تحقّق من فهمك
الدالة (function) تطبع بدلاً من أن تُرجع. ماذا يطبع السطران؟
def double(x):
print(x * 2)
result = double(5)
print(result)- A10 None
- B10 10
- CNone 10
- D10
يوجد return مرتين دون else. ماذا يُطبع؟
def check(mark):
if mark >= 40:
return "pass"
return "fail"
print(check(72))- Apass
- Bpass fail
- Cfail
- DNone
الدالة موجودة في الملف. ماذا يحدث مع ذلك؟
print(greet("rafi"))
def greet(name):
return "hello " + name- A`SyntaxError`
- Bhello rafi
- C`TypeError`
- D`NameError` — استُدعيت الدالة قبل أن يُنفَّذ سطر `def` الخاص بها
الإجابة تتطلب حساباً
سجّل الدخول للتحقق من إجاباتك
الأسئلة موجودة أعلاه، والتفكير في إجاباتها ذهنياً هو الجزء المهم. سجّل الدخول لرؤية الإجابات والتفسيرات والتلميحات ذات المستويات الثلاثة.
دورك الآن
اكتب ملفاً باسم text_tools.py يحتوي على أربع دوال، تحمل كل منها نص توثيق (docstring)، ولا تقوم أي منها بطباعة أي شيء على الشاشة:
clean(text)— تزيل المسافات الزائدة من الأطراف وتحول النص لحروف صغيرةinitials(full_name)— تحول الاسم الكامل"Rafi Ahmed"إلى أحرف أولى"RA"word_count(text)— تحسب عدد الكلمات في النص، وترجع0إذا كان النص فارغاًsummarise(text)— تستدعي الدوال الثلاث السابقة وترجع سطراً واحداً منسقاً يلخص النص
ثم في الجزء السفلي من الملف، استدعِ دالة summarise على بضع نصوص مختلفة واطبع النتائج — وهذا هو المكان الوحيد المسموح فيه بالطباعة في الملف بأكمله.
وأخيراً، أجرِ هذه التجارب الثلاث:
- استدعِ
word_count(""). ماذا كان سيحدث لو لم تضع جملة الحراسة (guard) في البداية؟ - استبدل أمر
returnفي دالةsummariseبدالةprint، ثم خزن نتيجتها في متغير واطبع ذلك المتغير. ماذا ترى على الشاشة، ولماذا؟ - انقل سطر استدعاء الدالة
cleanليكون أعلى من سطرdefالخاص بها، واقرأ رسالة الخطأ الناتجة بعناية.
التجربة الثانية هي أثمن ثلاث دقائق في هذا الفصل؛ فرؤية الفرق الصريح بين print و return بعينيك تعني أنك لن تخلط بينهما مجدداً.
الخطوة ٦ من ٦
التحدي — اختبار الفصل
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
سجّل الدخول لأداء الاختبار