الدوال — قرار واحد باسم واحد
كتابة الدوال باستخدام def، الفرق الجوهري بين return و print، معالجة الحالات الخاصة والاستثنائية بالحراس (guards) في المقدمة، وسلاسل التوثيق (docstrings).
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
لدينا مجموعتان من الطلاب ونريد حساب متوسط درجات كل منهما:
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() في الدرسين الثاني عشر والثالث عشر. والآن يمكنك أن تدرك سبب ذلك بوضوح: فمهمتها كانت تعديل شيء ما في المكان، وليس إرجاع أي قيمة جديدة.
الأمر return يوقف تنفيذ الدالة فوراً
def check(mark):
if mark >= 40:
return "pass"
return "fail"
print(check(72))
print(check(20))pass
failفي اللحظة التي يتم فيها تنفيذ return، ينتهي عمل الدالة فوراً — والأسطر التي تليه لا تُنفذ أبداً. ولهذا السبب لم نكن بحاجة إلى كتابة else هنا: فإذا تحقق الشرط ونُفذ أمر return الأول، يصبح السطر الثاني غير قابل للوصول إليه أصلاً.
هذا النمط — الخروج الفوري بمجرد تحقق الشرط — يختصر التفرعات العميقة لجمل if/else من الدرس التاسع، ويجعل الكود مسطحاً ومريحاً جداً في القراءة.
التعامل مع الحالات الحدية — جمل الحراسة (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 هو تطبيق لقاعدة التحقق المنطقي من الدرس الثامن: القائمة الفارغة تُعامل كقيمة خاطئة (False)، لذا فإن not تجعلها صحيحة (True). لا حاجة لكتابة len(values) == 0.
يُسمى هذا النمط جملة الحراسة (Guard Clause): تخلص من الحالات الشاذة وغير الطبيعية في المقدمة أولاً، ثم اكتب باقي منطق الدالة براحة بال تامة. تجعل الدوال تطبيق هذا الأسلوب غير مكلف على الإطلاق — فالتحقق يُكتب مرة واحدة ويستفيد منه كل من يستدعي الدالة.
هل0.0هي الإجابة الصحيحة للقائمة الفارغة؟ هذا محل نقاش منطقي. فهو يخلط بين "لا أحد يملك درجات" وبين "الجميع حصلوا على صفر". في البرامج الواقعية، غالباً ما تُرجع الدالةNone، أو تطلق استثناءً صريحاً باستخدامraiseالتي سنتعلمها في الدرس الرابع والعشرين. استخدمنا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 ينهي عمل الدالة فوراً، لذا بقيت الشروط مسطحة وأنيقة. ولا تزال قاعدة الدرس التاسع سارية: الشرط الأكثر صرامة يوضع أولاً — فلو وضعنا 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 إلى سطر واحد على الأقل مزاح بمسافة بادئة بعده.
Step 4 of 6 — Predict
Check your understanding
The function prints rather than returns. What do the two lines print?
def double(x):
print(x * 2)
result = double(5)
print(result)- A10 None
- B10 10
- CNone 10
- D10
Two returns and no else. What is printed?
def check(mark):
if mark >= 40:
return "pass"
return "fail"
print(check(72))- Apass
- Bfail
- Cpass fail
- DNone
The function is right there in the file. What happens even so?
print(greet("rafi"))
def greet(name):
return "hello " + name- AA `NameError` — the function was called before its `def` line ran
- Bhello rafi
- CA `TypeError`
- DA `SyntaxError`
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
دورك الآن
اكتب ملفاً باسم 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 بعينيك تضمن أنك لن تخلط بينهما مجدداً طوال مسيرتك البرمجية.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz