ملحق E — الدالة

الدالة

الدالة

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

تعدد المعطيات في استدعاء الإجراء

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

فقد يأخذ الإجراء أكثر من معطى:

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

وقد يأخذ معطىاً واحدًا لكنَّهُ يمثل مجموعة معطيات، لكونِه جَمعًا في نفسه (كالقائمة: list):

  • نحو: 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.

شكلان لاستدعاء الإجراء

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

  1. إسناد الإجراء للنوع: list.append(xs, 25)
  2. إسناد الإجراء للكائن: xs.append(25)

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

xs = [20, 10, 30, 40]
ys = [20, 10, 30, 40]
list.sort(xs)
ys.sort()
print(xs == ys)
True
list.append(xs, 50)
ys.append(50)
print(xs == ys)
True

نتيجة الإجراء

لابد للإجراء -حتى يكون نافعًا- أن يكون له أثر. وإنما يحقق الإجراء تأثيرًا بإحدى طريقتين:

أحدها: الرجوع بنتيجة لموضِع الاستدعاء؛ وذلك بجملة الرجوع (return). مثال ذلك:

def pow(x, y):
    return x ** y
y = pow(2, 3)

والأخرى: التغيير في مُعطىً قابل للتغيير (كالقائمة أو الملف). مثال ذلك:

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

والكائنات قد يحصل فيها إحدى الأمرين لغرض ما.

فالقائمة (list) هي مظنَّة التغيير.

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

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

وأما في نحو: xs.sort() فإن الطريقة (.sort()) تغيِّرُ القائمة المُسندَة؛ فلا تحتاجُ لنسخ القائمة في الذاكرة للتعديل عليها.

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

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

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

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

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

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

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[60], 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, height):
    bmi = weight / (height ** 2)
    return round(bmi, 2)

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

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

NameError: name 'bmi' is not defined

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

معطيات غير مصرح بها

وعلى العكس فإن المعرَّفات الخارجة معروفة في الداخل؛ وذلك يعني أنها يمكن أن تعمل في الإجراء بشكل غير مباشر. أي أنها معطيات غير مصرَّح بها (هو: 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"

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

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

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

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

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

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

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

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

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

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

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

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

إطلاق عدد المعطيات

قد تتساءل كيف تعمل 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}

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