13  البرمجة الإجرائية

رأينا في جميع ما سبق كيف أن بايثون لغة أمريَّة (Imperative)؛ أي أنها مجموعة متسلسلة من التعليمات البرمجية التي يتبعها المفسِّر حَسَبَ ورودها (من الأعلى إلى الأسفل).

وفي هذا الفصل نعرف أن بايثون لغة إجرائية (Procedural)؛ وهذا يعني تركيب البرنامج من إجراءات يستدعي كل واحد منها مجموعة أخرى من الإجراءات. فقد يستدعي الإجراء الأول إجرائين، وقد يستدعي كل منهما إجرائين كذلك، …إلخ. حتى تعود النتيجة إلى الإجراء الأوَّل الذي يمثِّل مدخل البرنامج: main. ويتشكل لدينا التسلسل الهرمي كما هو موضح:

فأما الإجراء (Procedure) فهو: قطعة نص برمجي مخزَّنة تُستدعى باسمها بعوامل متغيرة. ويسمى الدالة (Function) أو الروتين (Routine) أو البرنامج الفرعي (Sub-Program). ونقول استدعي الإجراء (Call) أو نفذه (Execute). وتسمى مكان القطعة التي قامت بالاستدعاء: موقع الاستدعاء (Call-site).

def main():
    procedure1()
    procedure2()

def procedure1():
    procedure1_1()
    procedure1_2()

def procedure2():
    procedure2_1()


def procedure1_1():
    print("Doing something 1")

def procedure1_2():
    print("Doing something 2")


def procedure2_1():
    print("Doing something 3")

if __name__ == "__main__":
    main()
Doing something 1
Doing something 2
Doing something 3

أما القطعة الأخيرة: if __name__ == "__main__" فإن المتغير __name__ هو متغير مخصوص في لغة بايثون تعطيه القيمة __main__ إذا تم تشغيل البرنامج عن طريق هذا الملف، بخلاف ما لو تم استيراد هذا الملف. وسيأتي بيان ذلك في الفصل التالي، عندما نشرح الحزم والوحدات.

flowchart LR
    main --> procedure1
    main --> procedure2
    procedure1 --> procedure1_1
    procedure1 --> procedure1_2
    procedure2 --> procedure2_1
    procedure1_1 --> print1[<code>Doing something 1</code>]
    procedure1_2 --> print2[<code>Doing something 2</code>]
    procedure2_1 --> print3[<code>Doing something 3</code>]
 

فأي إجراء يتم تعريفه؛ كالمتغير الذي يتم تعريفه: هو نص برمجي محفوظ ينتظر الاستدعاء حتى يحضر في ذاكرة البرنامج في ظرف تنفيذي ويتم تشغيله بعوامل معيَّنة. لذا فإننا إن لم نشتغل الإجراء الأوَّل main فإن البرنامج وإن كان يحفظ هذه الإجراءات إلا أنها تحتاج إلى الاستدعاء لتعمل.

تعريف الإجراء

يعرَّف الإجراء بـ def ويتكون من قسمين:

الأول: الحد (Function Signature). وهو ثلاثة أجزاء:

  1. الاسم (Name): الذي يُطلَبُ به
  2. العوامل (Parameters): وهي متغيرات تؤثر (تعمل) في النتيجة
  3. نوع العائد (Return Type): نوع القيمة التي يرجع بها الإجراء نتيجة عمله

الثاني: الجسد (Body). وهو القطعة البرمجية التي جُعِلَ اسمه عنوانًا لها. وقد يتضمن الجسد جملة رجوع (Return Statement) للخروج بنتيجة

تأمل المثال التالي:

def add(x, y):
    result = x + y
    return result
  • الحد: def add(x, y)
    • الاسم: add
    • العوامل: x, y ونفترض أنهما من النوع العددي Number
    • العائد: Number (نوع النتيجة)
  • الجسد:
    • result = x + y هنا نرى كيف أن العوامل أُعمِلَت في الإجراء
    • return result تسمى جملة الرجوع وهي التي؛ ترجع بالنتيجة إلى الموضع الذي طلب الإجراء.

التصريح بالأنواع

تسمح بايثون بعدم التصريح بأنواع المتغيرات إلا أنه يجوز، وذلك على النحو التالي:

from numbers import Number

def add(x: Number, y: Number) -> Number:
    result = x + y
    return result
  • وبهذا نعرف أن التصريح بنوع المتغيِّر يكون بإضافة نقطتين رأسيتين (:) ثم النوع Number بعد كل عامل.
  • وبعد السهم لنوع الناتج من الإجراء (->)

ومن الأنواع المبنيَّة (Built-in Types) في بايثون:

  • Number وهو النوع الذي ترجع إليه جميع أنواع الأعداد
  • int الأعداد الصحيحة، نحو: 10
  • float الأعداد العشرية، نحو: 10.5
  • str وهي نوع النص، نحو: "Salam"
  • list قائمة وإن شئت تحديد نوع العنصر الواحد فيها؛ فإنك تضعه بين القوسين المربعين، نحو:
    • list[int]
    • list[float]
    • list[str]

ظرف التنفيذ

تأمل الإجراء التالي:

def calculate_grade(score):
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    else:
        return "F"

وعند طلب التنفيذ نعين العوامل. فينتج لنا بطلب calculate_grade(95) نسخة معيَّنة من تفاصيل الإجراء، نسمّيها ظرف التنفيذ (Execution Frame) ؛ يكون فيه المتغير score=95 تكون هذه النسخة حاضرة في ذاكرة البرنامج وقت تنفيذ الإجراء:

if 95 >= 90:
    return "A"
elif 95 >= 80:
    return "B"
elif 95 >= 70:
    return "C"
elif 95 >= 60:
    return "D"
else:
    return "F"

وبطلب تنفيذ calculate_grade(80) يكون الظرف هكذ:

if 80 >= 90:
    return "A"
elif 80 >= 80:
    return "B"
elif 80 >= 70:
    return "C"
elif 80 >= 60:
    return "D"
else:
    return "F"

لاحظ أن نتيجة السطرين بحسب الظرفين المختلفين:

print(calculate_grade(95))
print(calculate_grade(80))
A
B

تمرير العوامل بالاسم

قد تكون العوامل كثيرة في الإجراء على النحو التالي:

def weather_condition(temperature, humidity, wind_speed):
    if temperature >= 30 and humidity >= 60 and wind_speed >= 10:
        return "Rainy"
    elif temperature >= 20 and humidity >= 50 and wind_speed >= 5:
        return "Cloudy"
    elif temperature >= 10 and humidity >= 30 and wind_speed >= 0:
        return "Sunny"
    return "Normal"

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

cond = weather_condition(30, wind_speed=10, humidity=60)

if cond == "Rainy":
    print("Don't forget your umbrella!")
Don't forget your umbrella!

العوامل الجائزة

الأصل في العوامل المعرَّفة الوجوب؛. فلو أهملت أحدها فإنك ستواجه بالخطأ:

def weather_condition(temperature, humidity, wind_speed):
    pass

weather_condition(30)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 4
      1 def weather_condition(temperature, humidity, wind_speed):
      2     pass
----> 4 weather_condition(30)

TypeError: weather_condition() missing 2 required positional arguments: 'humidity' and 'wind_speed'

تقول رسالة الخطأ (السطر الأخير) أن الإجراء يفتقد عاملين موضعيين إلزاميين، وهما: 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 calculate_bmi(weight: float, height: float) -> float:
    bmi = weight / (height ** 2)
    return round(bmi, 2)

فنتوقع وقوع خطأ هنا لأن bmi غير معرفة إلا في نطاق الإجراء:

print(bmi)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 print(bmi)

NameError: name 'bmi' is not defined

تقول رسالة الخطأ (السطر الأخير) أن المتغير bmi غير معرَّف. وهذا منطقي لأن النطاق الخارجي لا يعلم ما تكنه النطاقات الداخلية الخاصة بالإجراءات. وهو أمر مطلوب جدًّا ومرغوب في البرمجة. وذلك يعني أننا لن نتعب كثيرًا في اختيار الأسماء داخل كل إجراء، مخافة التعارض.

عوامل غير مصرح بها

وعلى العكس فإن المعرَّفات الخارجة معروفة في الداخل؛ وذلك يعني أنها يمكن أن تعمل في الإجراء بشكل غير مباشر. أي أنها عوامل غير مصرَّح بها (هو: max_length في هذا المثال):

max_length = 8

def check_password_strength(password: str) -> str:
    if len(password) > max_length:
        return "strong"
    elif len(password) > max_length // 2:
        return "medium"
    return "weak"

مستويات نطاق التسمية

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

ثمرة الإجراء

تُخرج الإجراءات نتائج معالجتها بإحدى طريقتين:

الأولى: الرجوع: حيث ترجِع النتيجة بجملة return لموضع طلب الإجراء، وغالبًا ما يتم تعيينه لمتغير، نحو: y = sqrt(x).

الثانية: المفعول: نحو: list.sort(xs) حيث يعدِّل الإجراء العامل الذي تم تمريره إليه؛ ولذلك نسميه مفعولا به.

وذلك مثل print() فإن أحد عواملها غير مذكور (وهو المفعول)، وهو: file=sys.stdout ولأجله تكتب النتيجة على الشاشة.

الإجراء الخاوي

والإجراء دائمًا يرجع بنهاية آخر جملة فيه، لكن الذي لا يصرَّحُ فيه بكلمة الرجوع return تُقدَّر له القيمة العدمية: None ويسمى الإجراء حنيها خاويًا (void).

فائدة: إذا رأيت إجراءً خاويًا فاعلم أن له مفعولاً، سواء من عوامله المباشرة أو غير المباشرة. والسبب في ذلك أنه لا بد للإجراء من أن يُثمر، وإلا فالإجراء الذي ليس له أثر، فإن وجوده كعدمه.

تأمل المثال التالي الذي يأخذ عوامل ثم لا يرجع بشيء، لكنه يُظهر النتيجة على الشاشة:

def print_decorated(message: str, n: int):
    print("=" * n)
    print(message)
    print("=" * n)

print_decorated("Salam alykom", 15)
===============
Salam alykom
===============

إطلاق عدد العوامل

قد تتساءل كيف تعمل print وهي تأخذ عددًا لا محدودًا من العوامل؟ حتى نجيب عن ذلك ننظر في عملية فك الأقواس وهي علامة النجمة * قبل المتغيِّر المشير لمجموعة، على النحو التالي:

xs = [10, 20, 30]
print(xs)
print(*xs)
[10, 20, 30]
10 20 30

لاحظ الفرق بين الطلب الأول والثاني:

  1. الأوَّل يطبع المتغير xs ككل، أي: كقائمة من ثلاثة أعناصر
  2. يُمرر واحدًا تلوَ الآخر للإجراء 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)
1
<class 'tuple'> (2, 3, 4, 5)
print: 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)
1
<class 'dict'> {'b': 2, 'c': 3, 'd': 4, 'e': 5}
print: b c d e

وقد يجتمعان في نفس الإجراء *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)
1 2
<class 'tuple'> (3, 4, 5)
<class 'dict'> {'a': 1, 'b': 2, 'c': 3}

ولا بد من تقديم الموضعية قبل الاسمية.

للمزيد راجع العوامل الخاصة.