12  الإجراء

الإجراء / الدالة

الإجراء (Procedure) قطعة نص برمجيّ لها اسم، يتم استدعاؤها بمعمولات مختلفة بحسب معاملاته. ويسمى الدالة (Function) أو الروتين (Routine) أو البرنامج الفرعي (Sub-Program).

استدعاء الإجراءات

ونقول استدعي الإجراء (Call) أو نفذه (Execute). ويسمى مكان القطعة التي قامت بالاستدعاء: موقع الاستدعاء (Call-site).

ويمكن استدعاؤه بمعمول واحد:

  • نحو: int(x) لإنشاء العدد الصحيح من النص. مثل: int("12") ينتج: 12.
  • أو نحو: abs(x) للقيمة المطلقة مثل: abs(-10) ينتج: 10.

وقد يأخذ معمولين:

  • نحو: round(x, n) مثل: round(10.259, 2) ينتج: 10.26.
  • أو نحو: pow(x, y) لرفع العدد x إلى القوة y. مثل: pow(2, 3) ينتج: 8.

وقد يأخذ معمولاً واحدًا لكنَّهُ قائمة:

  • نحو: sum(numbers) مثل: sum([1, 2, 3, 4, 5]) ينتج: 15.
  • أو نحو: max(numbers) لأكبر عدد في القائمة. مثل: max([1, 2, 30, 4, 5]) ينتج: 30.

وقد يكون عدد معمولاته لا محدودًا:

  • نحو: print(*values). فعلامة النجمة (*) تشير لقبول عدد مطلق من العوامل. مثل:
name = "Adam"
age = 25
print("My name is", name, "and I'm", age, "years old")
My name is Adam and I'm 25 years old

فإن print قبلت خمسة عوامل:

  1. النص: "My name is"
  2. قيمة المتغير: name
  3. النص: "and I'm"
  4. قيمة المتغير: age
  5. النص: "years old"

ولدينا الإجراء help(func) يطلب مساعدة الإجراء المعيَّن، بلا أقواس، هكذا:

help(sum)
Help on built-in function sum in module builtins:

sum(iterable, /, start=0)
    Return the sum of a 'start' value (default: 0) plus an iterable of numbers

    When the iterable is empty, return the start value.
    This function is intended specifically for use with numeric values and may
    reject non-numeric types.

وقد عرفت أن طلب التنفيذ يكون بالقوسين بعد اسمه ()، وتوضَع المعمولات فيهما.

وفي كل ما سبق كانت العوامل من النوع المؤثر؛ لأنها كانت تؤثر في النتيجة ولا تتأثر بها.

أما المتأثر فنحو فعل الترتيب من مكتبة القائمة: list.sort(xs) فإن المتأثر به القائمة نفسها، مثل:

xs = [40, 20, 10, 30]
list.sort(xs)

فأما جملة list.sort فإننا حددنا اسم النوع list وأردنا منه الإجراء sort، وأتينا بالقوسين لطلب تنفيذه، ووضعنا المتأثر xs فيه.

ومع أننا لم نعيِّن النتيجة (بجملة التعيين =)، فقد تغيَّرت (تأثرت) القائمة بالإجراء:

print(xs)
[10, 20, 30, 40]

بعكس الترتيب بالإجراء الذي ليس من النطاق العام: sorted(xs) فإنَّ القائمة فيه ليست متأثرًا؛ إذْ يُنتِجُ الإجراءُ قائمةً جديدةً ولا يغير القائمة المُدخلة:

xs = [40, 20, 10, 30]
ys = sorted(xs)
print(xs, "لم يعمل الإجراء في القائمة نفسها")
print(ys, "هي قائمة جديدة")
[40, 20, 10, 30] لم يعمل الإجراء في القائمة نفسها
[10, 20, 30, 40] هي قائمة جديدة

وقد تتساءل ما الفائدة من وجود طريقتين وكلاهما يعمل نفس العمل؟

  1. الطريقة الأولى: list.sort(xs) لا ترجع بشيء بل تعدل نفس القائمة
  2. الطريقة الثانية: sorted(xs) ترجع قائمة جديدة

فأما الطريقة الأولى فأصغر في الذاكرة لأنها لا تُنشئ نُسخة كما تفعل الطريقة الثانية. لكن الطريقة الثانية مفيدة إن أردت أن تُبقي القائمة الأصلية كما هي.

والأمر كذلك في الإجراءين:

  1. list.reverse(xs)
  2. reversed(xs)

ويجتمع المؤثر والمتأثر في نحو:

list.append(xs, 50)

فالإجراء list.append يأخذ القائمة، ويضيف إليها القيمة 50. ولذلك نقول الأوَّل متأثر والثاني مؤثر.

print(xs)
[40, 20, 10, 30, 50]

وقد وضعت اللغات الشيئية (Object-Oriented) مثل بايثون صياغة خاصَّةً: للإجراء المسند إلى الشيء. وبذلك يتحصل لدينا طريقتان كلتاهما تؤديان نفس المعنى:

  • إسناد الإجراء للنوع: list.append(xs, 25)
  • إسناد الإجراء للشيء: xs.append(25)

ومثاله أيضًا في الإجراء list.sort للترتيب وكذلك في list.append للإضافة:

xs = [20, 10, 30, 40]
ys = [20, 10, 30, 40]

list.sort(xs)
ys.sort()
assert xs == ys

list.append(xs, 50)
ys.append(50)
assert xs == ys

وقد تُعَيَّنُ العوامل بأحد طريقتين:

  1. تعيين بالموضع: نحو: round(3.14159, 2)
  2. تعيين بالاسم: نحو: round(number=3.14159, ndigits=2) فلا يشترط فيه ترتيب العوامل.

ويجوز استعمال الطريقتين معًا في نحو: list.sort(numbers, reverse=True) ويشترط فيه تقدم التعيين بالموضع ليكون في مكانه، ثم يتبعه التعيين بالاسم حيث لا يشترط الترتيب فيه.

الاستيراد

وقد رأينا أن بعض المعرَّفات كـlist لا يحتاج إلى استيراد. وبعضها يلزمنا للوصول إليه الاستيراد (import) بل إن أغلب ما يتوفَّر يجب فيه الاستيراد. وليس لذلك قاعدة تضبطه إلا أن تحفظه بالممارسة. وذلك نحو وحدة الرياضيات (math) في نحو:

import math

math.sqrt(16)
4.0

ويجوز استيراد الإجراء باسمه دون غيره بكتابة from في الجملة على النحو التالي:

from math import log2

log2(1024)
10.0

كما يجوز استيراد الجميع بحرف النجمة * على النحو التالي:

from math import *

cos(2*pi) - sin(pi/2)
0.0

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

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

والإجراء من حيث التعريف قسمان:

الأول: جملة التعريف (ويسمى التوقيع: Signature) المبتدأة بكلمة def. وهو ثلاثة أجزاء:

  1. المعرِّف (Identifier): اسمه الذي يُطلَبُ به
  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 تسمى جملة الرجوع وهي التي؛ ترجع بالنتيجة إلى الموضع الذي طلب الإجراء.

ثم يحصل الاستدعاء بذكر اسم الإجراء مع القوسين () لتمرير معمولات لتعيين معاملاته:

add(3, 5)
8

وإليك صورتان لتوضيح ذلك:

صورة توضح العملية والعامل والمعمول

صورة توضح الفرق بين المعمول والمعامل

لكي نحفظها نقول:

  • العامل (Operator) على وزن فاعل، فهو في الجمع علامته: +، وفي الإجراء قوسا الاستدعاء: ()
  • المعمول (Argument) فهو المفعول الحقيقي؛ وهي القيَم الممررة عند الاستدعاء: 3 و 5
  • المعامل (Parameter) فهو المفعول المُفترض؛ في التعريف: x و y
  • أما الدالة / الإجراء (Function / Procedure) فهو الفعل

فيكون حاصل استدعاء الإجراء: عمل العامل في المعمولات؛ أي: فعل الفاعل في المفعولات. وهو تحقيق الجملة الفعلية.

وكوْن القوسيْن () عاملاً يعني أن عدمهما لا يُحدِثُ أثر الإجراء. فلو كتبنا مثلاً:

x = add

فإن x تشير إلى نفس الإجراء، فيمكن بعد ذلك -مثلاً- استدعاءُ الإجراء به:

x(7, 3)
10

وهذا قليل الاستعمال إلا فيما يسمى الدوال العالية (Higher-order Functions).

متى نحتاج للإجراءات؟

  • التكرار: إذا وجدت أنك تكرر نفس القطعة البرمجية مرارًا؛ فهي مظنة التحول لإجراء
  • التعقيد: إذا كانت القطعة ذات سلوك غير بسيط فتحتاج إلى اختبار؛ فالواجب فصلها في إجراء واختبارها بمعمولات مختلفة.
  • القابلية للتركيب: إذا كانت القطعة ككل ذات وظيفة واضحة ومحددة؛ فربما كان من الأفضل أن تجعل لها عنوانًا (تجعلها إجراءً)

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

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

from numbers import Number

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

ومن الأنواع المبنيَّة في بايثون:

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

للاستزادة راجع: MyPy Type hints cheat sheet.

ظرف التنفيذ

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

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[21], 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[25], 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"

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

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

مستويات النطاقات (Scopes) من الداخل إلى الخارج:

  • النطاق المحلي (Local Scope): المتغيرات المعرفة داخل إجراء.
  • النطاق المحيط (Enclosing Scope): المتغيرات المعرفة في إجراء يتحوي ذلك الإجراء.
  • النطاق العام (Global Scope): المتغيرات المعرفة خارج جميع الإجراءات.
  • النطاق المدمج (Built-in Scope): الإجراءات المدمجة في بايثون مثل print().

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

ثمرة الإجراء

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

الأولى: الرجوع: حيث ترجِع النتيجة بجملة 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}

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

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