flowchart LR
a[Input] --> b[Function]
b --> c[Output]
style b fill:#000,stroke:#333,color:#fff
الإجراء / الدالة
الدالة (Function) قطعة نص برمجيّ لها اسم، يتم استدعاؤها بمعطيات مختلفة بحسب معاملاته. ويسمى الإجراء (Procedure) أو الروتين (Routine) أو البرنامج الفرعي (Sub-Program).
- مثل
printالتي تظهر قيم المتفيرات كحروف تحت الخلية - مثل
absوهي تحول العدد السالب إلى موجب
الفرق بين الكلمات المفتاحية والدوال
بخلاف الدالة / الإجراء، فإن الكلمة المفتاحية (Keyword) لها دلالة خاصَّة في لغة بايثون، مثل if للاختيار، أو for للكر أو + للجمع أو حتى and للجمع بين القيَم الثنائية. فهي كلمات محجوزة لا يمكن استخدامها كأسماء للمتغيرات أو الدوال أو أي معرفات أخرى. ويمكن تصنيف الكلمات المحجوزة في بايثون حسب الوظيفة وكذلك الإجراءات الجاهزة. راجع في ذلك ملحق الكلمات الخاصة.
استيراد واستدعاء الدوال
import mathفالعالم بالرياضيات برمج هذه الدوال وأعطاها اسما، فنستطيع استعمالها في حل المسائل الرياضية:
print(math.sqrt(16)) # الجذر التربيعي
print(math.pow(2, 3)) # القوة
print(math.cos(math.radians(45))) # جيب الزاوية 45 درجةولولا مفهوم تخزين الأوامر وتسميتها، للزم أن نكتب الأوامر البرمجية التي تؤدي هذه الحسابات في كل مرة نريدها. وهذا يتعذر علينا لا لأنها خطوات كثيرة فحسب بل لأنها ليست بسيطة بحيث يتقنها كل مبرمج أصلاً.
وقد تحتوي الوحدات على مسميات، كالثابت \(\pi\) الذي يستعمل في علم المثلثات:
print(math.pi)3.141592653589793
تسمى النقطة (.) عامل إسناد (Dot Operator) في نحو العبارة math.pi أو عبارة math.sin(A)؛ وتفسرها بايثون أنها إشارة للمسمى المتضمن في الوحدة المسنَد إليها. سواءٌ كان ذلك دالة أو متغيرًا.
ويجدر بالذكر أن بعض الدوال في لغة بايثون لا نحتاج فيها لإسنادها لوحدةٍ ما؛ فهي مبنيَّة (Built-in)، نحو: print()، بل ولا تحتاج إلى التصريح باستيرادها بجملة (import). ومثال ذلك أيضًا round() لتقريب العدد. ولم يتبيَّن لي وجه الفرق بين ما جُعِلَ مبنيًّا في وحدة أو مبنيًّا عائمًا.
print(round(math.pi, 4))ويُمكن إشاعة المسميات المتضمَّنة في وِحدةٍ ما بجملة الاستيراد المبتدأة بمِن (from) بحيث لا نحتاج لإسنادها في كل مرة، وذلك يتم هذا النحو:
from math import sin, radians
c = 1000
A = 40
B = 60
C = 80
a = c * sin(radians(A)) / sin(radians(C))
h = a * sin(radians(B))
print(h)ويُمكِن استيراد الكُلّ بعلامة النجمة (*)، على هذا النحو:
import mathfrom math import *
z = cos(2*pi) - sin(pi/2)
print(z)تنبيه: استيراد الكل (*) قد يتعارض مع مسمياتنا فيما بعد، ويصعب أن نعرف ذلك بسهولة، لذلك يجب أن يستعمل بحذر!
والمكتبة (Library) اسمٌ يطلق على مجموعة الوحدات.
وللاطلاع على الوحدات المدمجة (Built-in Modules) في لغة بايثون، يمكن الرجوع إلى صفحات بايثون المرجعية للمكتبة الأساسية (Standard Library) https://docs.python.org/3/library. حيث تجد -مثلاً-:
- وحدة الإحصاء: https://docs.python.org/3/library/statistics.html
- وحدة العشوائية: https://docs.python.org/3/library/random.html
- وحدة الوقت والتاريخ: https://docs.python.org/3/library/datetime.html
وهذه كلها يمكن استيرادها ثم استعمالها لأنها من ضمن بايثون نفسها، فلا تحتاج إلى تنزيل وتثبيت.
طلب المساعدة
وقد عرفت أن طلب التنفيذ يكون بالقوسين بعد اسمه ()، وتوضَع المعطيات فيهما. ولدينا الإجراء help(func) يطلب مساعدة الإجراء المعيَّن، بلا أقواس، هكذا:
help(sum)إنشاء دالة
أسباب إنشاء الإجراء:
- التكرار: إذا وجدت أنك تكرر نفس القطعة البرمجية مرارًا
- التعقيد: إذا كانت العملية تحتاج لكد الذهن أو لمعرفة لا تتوفر عند الجميع
- القابلية للتركيب: إذا كانت القطعة ككل ذات وظيفة واضحة ومحددة، ورأيت أنها تنسجم مع غيرها من القطع إذا وضعت لها اسمًا
ونمثل بتعريف الإجراء هذا:
def calculate_bmi(weight, height):
sq = height ** 2
bmi = weight / sq
return round(bmi, 2)ويبتدأ تعريفه بكلمة def (تعني: Define)، ويليها اسمه، ويليه بين القوسين: معطياته: (weight, height). ويلي ذلك علامة الابتداء (:)، ونسرد بعدها جسده؛ وهي الأوامِر التي تعالج هذه المعطيات. ويختص الإجراء بجملة الرجوع (return x) التي تعود بالنتيجة x للمكان الذي استُدعيَ منه الإجراء.
ثم يحصل الاستدعاء (Call) بذكر اسم الإجراء مع عامل الاستدعاء (Call Operator) وهما القوسان بعده () وهما كالظرف تُمَرر إليه المعطيات فيهما.
result = calculate_bmi(70, 1.80)
print(result)21.6
تعيين معطيات الإجراء بالاسم
ويجوز تعيين المعطى بالاسم لا بالموضع:
result = calculate_bmi(height=1.80, weight=70)
print(result)21.6
ولاحظ أننا قلبنا الترتيب لنبين أنه ليس بلازمٍ إذا تمَّ التعيين بالاسم.
المعرفات في الإجراء منكرة خارجه
ومن خصائص الإجراء أن أي اسم يتم تعريفه داخل الإجراء فإنه معروفٌ في نطاقه وليس يتسرب العلم به إلى الخارج.
فنتوقع وقوع خطأ هنا لأن bmi غير معرَّفة في الخارج:
print(bmi)تقول رسالة الخطأ (السطر الأخير) أن المتغير bmi غير معرَّف. وهذا منطقي لأن النطاق الخارجي لا يعلم ما تكنه النطاقات الداخلية الخاصة بالإجراءات. وهو أمر مطلوب جدًّا ومرغوب في البرمجة. وذلك يعني أننا لن نتعب كثيرًا في اختيار الأسماء داخل كل إجراء، مخافة التعارض.
مستويات نطاق التسمية
إذا اتفق متغيران في الاسم فقد يختلفان في المسمى، والقريب أوْلى من البعيد على هذا النحو:
مستويات النطاقات (Scopes) من الداخل إلى الخارج:
- النطاق المحلي (Local Scope): المتغيرات المعرفة داخل إجراء.
- النطاق المحيط (Enclosing Scope): المتغيرات المعرفة في إجراء يتحوي ذلك الإجراء.
- النطاق العام (Global Scope): المتغيرات المعرفة خارج جميع الإجراءات.
- النطاق المبني (Built-in Scope): الإجراءات المبنية في بايثون مثل
print().
فهو كاستعمال الضمائر في اللغة. وحتى نتفادى اللبس: اجعل استعمال المتغير تاليًا لأخص نطاق له.
معطيات من النطاق المحيط
وعلى العكس فإن المعرَّفات الخارجة معروفة في الداخل؛ وذلك يعني أنها يمكن أن تعمل في الإجراء بشكل غير مباشر. أي أنها معطيات غير مصرَّح بها (هو: max_length في هذا المثال):
max_length = 8
def check_password_strength(password):
if len(password) > max_length:
return "strong"
elif len(password) > max_length // 2:
return "medium"
return "weak"المعطيات الجائزة
الأصل في المعطيات المعرَّفة الوجوب؛. فلو أهملت أحدها فإنك ستواجه بالخطأ:
def weather_condition(temperature, humidity, wind_speed):
pass
weather_condition(30)تقول رسالة الخطأ (السطر الأخير) أن الإجراء يفتقد معطىيْن موضعيين إلزاميين، وهما: humidity و wind_speed.
فلو أردنا أن يكون عمل الإجراء بحسب أحد المعطيات بالتعيين، على نحو:
- لو عينت السلزيوس فالتحويل لفهرنهايت:
convert_temperature(celsius=32) - لو عينت الفهرنهايت فالتحويل لسلزيوس:
convert_temperature(fahrenheit=89.6)
وإليك معادلة التحويل بين نوعيْ درجة الحرارة:
\[ F = \frac{9}{5} C + 32 \]
فأي معطى نعرفه بقيمة افتراضية فإن بايثون تعتبره اختياريًّا، ولو بالقيمة العدميَّة None. فنعرِّفُ المعطيات بقيَم عدميَّة، ونفحص وجودها بالشرط is not None لنُعمِلَها أو نهملها:
def convert_temperature(celsius = None, fahrenheit = None):
if celsius is not None:
fahrenheit = (9 / 5) * celsius + 32
return fahrenheit
elif fahrenheit is not None:
celsius = (fahrenheit - 32) * (5 / 9)
return celsiusنتأكد:
assert convert_temperature(celsius=32) == 89.6
assert convert_temperature(fahrenheit=89.6) == 32ذكر أنواع المعطيات في الإجراء
تسمح بايثون بعدم التصريح بأنواع المعاملات إلا أنه يجوز ذلك. فيتم ذكره:
- بعد النقطتين رأسيتين (
:) للمدخلات - وبعد السهم (
->) للمخرجات
def add(x: int, y: int) -> int:
result = x + y
return resultومن الأنواع المبنيَّة في بايثون:
intالأعداد الصحيحة، نحو:10floatالأعداد العشرية، نحو:10.5strوهي نوع النص، نحو:"Salam"
للاستزادة راجع: MyPy Type hints cheat sheet.
دالة: قوة كلمة المرور
وإليك مثالاً آخر لدالة تتحقق من قوة كلمة المرور:
def check_password_stength(password: str) -> str:
if len(password) < 8:
return "Weak"
elif len(password) < 12:
return "Medium"
else:
return "Strong"لاحظ أننا استعملنا الدالة المبنية len() لحساب عدد الأحرف في النص.
ثم نستعمل دالتنا:
print(check_password_stength("1234"))
print(check_password_stength("1234567890"))
print(check_password_stength("12345678901234567890"))دالة: حساب المسافة المستقيمة بين نقطتين
في هذا المثال نعرف نقطتين ثم نحسب المسافة بينهما. والمسافة الإقليدية بين نقطتين \((x_1, y_1)\) و \((x_2, y_2)\) تتبع معادلة فيثاغورس:
\[ \text{distance} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2} \]
ونكتبها في بايثون هكذا كدالة:
import math
def distance(x1, y1, x2, y2):
return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)ثم نستعملها:
print(distance(0, 0, 3, 4))
print(distance(x1=1, y1=1, x2=-2, y2=-2)) # أو بتعيين المعطيات بالاسمإطلاق عدد المعطيات
ونقول استدعي الدالة (Call) أو نفذه (Execute). ويسمى مكان القطعة التي قامت بالاستدعاء: موقع الاستدعاء (Call-site).
فقد يأخذ الإجراء أكثر من معطى:
round(10.259, 2)10.26
pow(2, 3)8
وقد يأخذ معطىاً واحدًا لكنَّهُ يمثل مجموعة معطيات، لكونِه جَمعًا في نفسه (كالقائمة: list):
# مثال على دالة تجمع عناصر قائمة:
numbers = [1, 2, 3, 4, 5]
sum(numbers)15
# مثال على دالة تعطي أكبر عنصر في القائمة:
numbers2 = [1, 2, 30, 4, 5]
max(numbers2)30
وقد يكون عدد معطياته لا محدودًا:
- نحو:
print(*values). فعلامة النجمة (*) تشير لقبول عدد مطلق من العوامل. مثل:
قد تتساءل كيف تعمل print وهي تأخذ عددًا لا محدودًا من المعطيات؟
name = "Adam"
age = 25
print("My name is", name, "and I'm", age, "years old")فإن print قبلت خمسة عوامل:
- النص:
"My name is" - قيمة المتغير:
name - النص:
"and I'm" - قيمة المتغير:
age - النص:
"years old"
حتى نجيب عن ذلك ننظر في عملية فك الأقواس وهي علامة النجمة * قبل المتغيِّر المشير لمجموعة، على النحو التالي:
xs = [10, 20, 30]لاحظ الفرق بين الطلب الأول والثاني:
- الأوَّل يطبع المتغير
xsككل، أي: كقائمة من ثلاثة أعناصر - يُمرر واحدًا تلوَ الآخر للإجراء
printبعد فك الأقواس كأنك كتبت:print(10, 20, 30)
وحين تستعمل علامة النجمة في تعريف معاملات الإجراء، فإن أي معطى موضعي زائد عن العدد، يكونُ فيه كصفّ (وسيأتي الكلام عن الصف في باب: التسلسل). وبالمثال يتضح المقال. وقد اصطُلح على تسميته args وهي اختصار كلمة Arguments:
def show(a, *args):
print(a)
print(type(args), args)
print('print:', *args)
show(1, 2, 3, 4, 5)وكذلك العوامل المعيَّنة بالاسم، لكن بالنجمتين ** وتكون قاموسًا (وسيأتي الكلام عنه في باب المجموعة المرقمة) ومعنى الكلمة kwargs هو: Keyword Arguments:
def show(a, **kwargs):
print(a)
print(type(kwargs), kwargs)
print('print:', *kwargs)
show(a=1, b=2, c=3, d=4, e=5)وقد يجتمعان في نفس الإجراء *args و **kwargs نحو:
def show(x, y, *args, **kwargs):
print(x, y)
print(type(args), args)
print(type(kwargs), kwargs)
show(1, 2, 3, 4, 5, a=1, b=2, c=3)ولا بد من تقديم الموضعية قبل الاسمية.
صورة البرنامج كسلسلة استدعاءات متراكمة
غالبًا ما تفكك المهمة الكبيرة إلى خطوات جزئية على شكل إجراءات منفصلة، ويتم استدعاؤها وربط نتائجها في الإجراء الأوَّل (main()). لذلك نتصوَّر أن البرنامج عبارة عن سلسلة من الاستدعاءات، التي قد تتضمن في طيها استدعاءات أخرى فتتراكم.
def main():
p1()
print("middle")
p2()
print("finish")
def p1():
p1_1()
p1_2()
def p2():
p2_1()
def p1_1():
print("one-one")
def p1_2():
print("one-two")
def p2_1():
print("two-one")
main()لاحظ أننا نصوِّر كومة الاستدعاءات (Call Stack) بمرور الوقت من اليسار إلى اليمين، بحيث كلما ازداد عرض الطبقة كان ذلك دليلاً على قضاء وقت أكثر في تنفيذ تلك الجزئية، مما يساعدنا في معرفة الأجزاء التي تحتاج لتسريع في البرنامج حتى يكون في المحصلة سريعاً.
فأي إجراء يتم تعريفه؛ كالمتغير الذي يتم تعريفه: هو نص برمجي محفوظ ينتظر الاستدعاء حتى يحضر في ذاكرة البرنامج في ظرف تنفيذي ويتم تشغيله بعوامل معيَّنة. ثم يعود إلى الإجراء الذي استدعاه، وهكذا دواليك. لذا فإننا إن لم نشغيل الإجراء الأوَّل main فإن البرنامج وإن كان يحفظ هذه الإجراءات إلا أنها تحتاج إلى الاستدعاء لتعمل.
الإجراء المتسلسل
الإجراء المتسلسل (Recursive Function): هو إجراء يطلب نفسه؛ بشكل مباشر أو غير مباشر. وحتى يكون مثمرًا: يجب أن تؤول سلسلة الطلبات هذه إلى جملة تُنهي التسلسل.
مثال: المضروب
فمثلا: تعرف الرياضيات مضروب العدد
\[ !n = n(n-1)(n-2)\cdots(1) \]
فهي عملية ضرب لكل عدد مع الذي قبله حتى ينتهي للواحد. ونمثل هنا لمضروب العدد 5:
\[ !5 = (5)(4)(3)(2)(1) = 120 \]
ولك أن تصف نفس العملية هكذا:
\[ !n = n !(n-1) \]
أي أن مضروب العدد هو ضربُ هذا العدد في مضروب العدد الذي قبله. وذلك يتسلسل على النحو التالي:
\[ \begin{align*} !5 &= (5)!(4) \\ &= (5)(4!(3)) \\ &= (5)(4(3!(2))) \\ &= (5)(4(3(2!(1)))) \\ &= (5)(4(3(2(1)))) \\ &= (5)(4)(3)(2)(1) \\ &= 120 \end{align*} \]
إذاً نعرِّف المعادلة في بايثون هكذا:
def factorial(n):
# Recursive case (تسلسل)
if n > 0:
recursive_result = factorial(n - 1)
return n * recursive_result
# Terminal case (نهاية)
return 1
factorial(5)حيث لدينا حالتان:
- عندما تكون
n > 0يتم الطلب الذاتي :recursive_result = factorial(n - 1)إذْ هي جملة متسلسلة تكدِّس طلبات فوق طلبات؛ لكنها تؤول في النهاية إلى الجملة التي تُنهي التسلسل return 1هي الجملة التي تنهي التسلسل
وهنا قطعة نص برمجي نستعملها لتصور تسلسل الطلبات:
الكود
def factorial(n, depth=0):
# Recursive case (تسلسل)
print(f"{' ' * depth}Call factorial({n})")
if n > 0:
result = n * factorial(n - 1, depth + 1)
print(f"{' ' * depth}Return {result} from factorial({n})")
return result
# Terminal case (نهاية)
print(f"{' ' * depth}Return 1 from factorial({n})")
return 1
factorial(5)- فكل طلب يُنشأ له ظرف تنفيذ جديد تكون بالنسبة له قيمة
nهي المعيَّنة له وقت النداء. - وهكذا يتم تكديس الطلبات حتى ينتهي التسلسل عند الطلب
factorial(0)الذي يؤول لنتيجة مباشرة:return 1فيخلَّى هذا الظرف من الذاكرة وتعود نتيجته إلى الظرف المباشر الذي استدعاه وهو ظرفfactorial(1). - فتتعين القيمة
recursive_result = 1وينتقل إلى الجملة التي بعدها وهي جملة الرجوع بنتيجةreturn n * recursive_resultوهُما معيَّنان، أي تكون الجملة في واقع الظرف:return 1 * 1. - وهذه النتيجة تعود للظرف الذي استدعاه وهو
factorial(2)… إلخ.
طلب الإجراء المتسلسل يؤدي إلى ظروف متداخلة تؤول إلى ظرف واحد في النهاية.
يستعمل الإجراء المتسلسل وكذلك هياكل البيانات المتسلسلة بشكل كبير في الخوارزميات الفعالة.
يسهل كتابة بعض الخوارزميات باستعمال الإجراء المتسلسل. لكن قد تكون (أحياًنا) أقل أداءً من استعمال الحلقات.
للمزيد راجع ملحق الدالة.

