اذهب إلى المحتوى

مفهوم الدوال Functions في البرمجة


أحمد عصام النجار

تُعَدّ الدوال Functions في لغات البرمجة كتلًا منظمةً من الشيفرة تؤدي مهامًا محددةً، ويمكن إعادة استخدامها في أيّ مكان في البرنامج، كما توفِّر للمبرمج طريقةً أفضل لتنظيم كتابته للبرامج، بحيث لا يُضطر إلى إعادة كتابة مهمة واحدة أكثر من مرة، وسنتعرف في هذا المقال على مفهوم الدوال وأنواعها وكيفية كتابة دوال مخصصة في لغة بايثون وتنفيذها.

تعريف دالة function

تُعرَّف الدالة في لغة بايثون باستخدام الكلمة المفتاحية def -وهي اختصار كلمة definition أي تعريف- متبوعةً باسم الدالة والتي تخضع لقواعد المعرِّفات في بايثون التي ذكرناها سابقًا في مقال أساسيات البرمجة في لغة بايثون، ويلي اسم الدالة قوسان تُعرَّف معامِلات الدالة بداخلهما -والتي سنشرح أنواعها فيما بعد- ثم يُتبَع ذلك بالنقطتين :، وأسفل ذلك تُكتَب كتلة الشيفرة -أي جسم الدالة- التي ستُنفَّذ عند استدعاء الدالة.

قد تُنتِج الدالة قيمةً ما وتعيدها مكان استدعاء الدالة باستخدام الكلمة المفتاحية return التي تعني بالعربية أَعِدْ، إذ تُستخدَم بعض الدوال لمعالجة بعض البيانات وتنفيذ بعض العمليات عليها مثل العمليات الحسابية، كما أنّ استخدام الكلمة المفتاحية return ليس شرطًا، إذ يمكن ألا تعيد الدالة أيّ قيم على الإطلاق، ومع ذلك فإنه عند إعادة قيمة من الدالة، فإنه لا توجد أنواع محدَّدة من البيانات التي يمكن إعادتها.

من المتعارف عليه أثناء كتابة جسم الدالة الاحتفاظ بأول سطر لكتابة تعليق يشرح ويبيّن مهمة الدالة، وهو ليس بأمر ضروري، لكن يُفضَّل تطبيقه للحفاظ على الشكل العام للبرنامج، وبناءً على ما شرحناه، فإنه من المُمكن صياغة قالب تعريف الدوال كما في المثال التالي:

def function_name( arguments ):
   "function's comment"
   function code 
   return [expression]

يمكننا البدء في تعريف دالة بمثال عن كتابة دالة تحسب مساحة الدوائر لتُستدعى من أجل حساب مساحة أية دائرة باختلاف نصف قطرها، ولكتابة تلك الدالة يجب معرفة قانون حساب مساحة الدائرة وهوπ*r2 ‎ بحيث تكون π هي قيمة ثابتة تساوي 3.141592654؛ أما r2 فهي مربع نصف قطر الدائرة، وهي المتغير المطلوب للدالة، إذ تختلف كل دائرة في نصف قطرها، في حين تبقى π ثابتةً لا تتغير، وبناءً على سنطبق العملية ثم نعيد الناتج من الدالة كما يلي:

def circle_area (radius):
    r_squared = radius ** 2
    pi = 3.141592654
    area = pi * r_squared
    return area

عرَّفنا في الدالة السابقة متغير r_squared الذي يحتوي على قيمة مربع نصف القطر؛ ولذلك كانت قيمته ناتج عملية حسابية استُخدِم فيها عامل الأُس ** لتربيع نصف القطر، بعد ذلك عرَّفنا متغير يحمل قيمة π لتُستخدَم تلك القيمة لاحقًا، ثم عرَّفنا متغير area الذي سيحمل قيمة مساحة الدائرة، وذلك بضرب يتم قيمة π في مربع نصف القطر، وكلاهما قد جرى تعيينهما في إحدى المتغيرات بالفعل، إذًا بات المتغير area في نهاية الدالة يحمل قيمة مساحة الدائرة، لذلك استخدِمَت الكلمة المفتاحية return لإرجاع المتغير -أي قيمته- في السطر الأخير.

استدعاء دالة

لا تستطيع تلك الدالة وأي دالة نعرفها تنفيذ أيّ شيء دون استدعائها، وذلك لأن الدالة تم تعريفها وكتابة كتلتها، لكنها في الحقيقة لم يتم استدعاؤها لحساب أي شيء.

سنكتب في المثال الآتي بضع تعليمات لاستدعاء تلك الدالة وتمرير قيمة نصف قطر لتُحسَب مساحة الدائرة وطباعة القيمة، ولفعل ذلك سنعرِّف متغيرًا يحمل قيمة نصف قطر افتراضية أولًا ثم نستدعي الدالة.

r = 10
print (circle_area(r))
>> 314.1592654

معاملات الدوال

تُعَدّ المعاملات Parameters وسيلة للتخاطب مع الدوال وتمرير قيم إليها اثناء استدعائها وهي عبارة عن متغيرات لها أسماء مثل غيرها من المتغيرات تُعرَّف بداخل الأقواس بتحديد اسم لكل معامِل، ويمكن استخدام تلك الأسماء داخل جسم الدالة والتعامل معها مثل أيّ متغير من المتغيرات، لكن لا يمكن استخدام تلك المعاملات إلا بداخل جسم الدالة فقط، وتمرَّر قيمها عند استدعاء الدالة لاستخدامها، كما من الممكن أن تحتوي الدالة على الكثير من المعاملات، أو ألا تحتوي على أية معاملات على الإطلاق.

يمكن تمرير المعاملات المطلوبة في الدوال بنفس ترتيبها في حالة وجود أكثر من معامل، ويمكننا لمزيد من التوضيح لعملية استدعاء الدوال وإسناد قيم المعاملات تخيل وجود دالة تُدعى example ولديها ثلاثة معاملات هي a و b و c كما في المثال التالي:

def example(a, b, c):
    return a * b * c

يمكن إسناد قيمة المعاملات الثلاثة بالترتيب عند استدعاء الدالة كما في المثال التالي:

example(1, 2, 3)

ستُسنَد القيمة العددية 1 إلى المعامل a والقيمة العددية 2 إلى المعامل b والقيمة العددية 3 إلى المعامل c، لكن يمكن إسناد تلك القيم بدون استخدام ذلك الترتيب بذكر أسماء المعاملات وهو ما يسمى بالمعاملات المسماة Keyword Arguments كما في المثال التالي:

example(c = 3, b = 2, a = 1)

لاحظ أنه أسندنا قيمةً إلى المعامل c أولًا على الرغم من كونه آخر معامل في الدالة لأننا استخدمنا أسماء المعاملات لإسناد القيم، ففي تلك الحالة نستطيع تعيين قيم المعاملات دون التقيد بترتيب المعاملات.

يمكننا استكمال مثال دالة حساب مساحة الدالة بتطوير البرنامج قليلًا، إذ بدلًا من طباعة مساحة دائرة ناتجة عن نصف قطر محدد وثابت في البرنامج، فسنستقبِل قيمة نصف القطر تلك من المستخدِم عن طريق دالة input التي شرحناها سابقًا حيث سنمرر لها معاملًا يمثل رسالة تُطبَع للمستخدِم ليُطلَب منه إدخال قيمة عددية لنصف قطر الدائرة الذي يريد حساب مساحتها:

r_str = input ('Please enter R value: ')
r = float(r_str)
print (circle_area(radius=r))

لاحظ أننا حولنا القيمة التي أخذناها من المستخدم إلى قيمة عددية وذلك لأن خرج دالة input دائمًا قيمًا نصيةً وبالتالي يجب تحويل نوع البيانات في المتغير إلى نوع عددي في حالتنا عن طريق الدالة int، أو إلى النوع العددي العشري Float عن طريق الدالة float، ومن الأفضل بالطبع استخدام الدالة float لأن العدد العشري قد يكون عددًا صحيحًا؛ أما في حالة استخدام الدالة int، فإنه عند التحويل إلى عدد صحيح، فسينتِج المفسر خطأً لاحتواء النص على عدد عشري ولا يمكن تحويله إلى عدد صحيح إذا أدخل المستخدِم قيمًا عشرية.

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

المعاملات الافتراضية

يمكن إضافة معامِلات لها قيم افتراضية، عندها يكون تمرير قيمة للمعامل غير ضروري أو اختياري عند استدعائه، ويكون ذلك عبر كتابة اسم المعامِل ثم استخدام عامل الإسناد = ثم كتابة القيمة الافتراضية للمعامِل.

في المثال التالي كتبنا دالةً لضرب عددين ببعضها إذ يُضاف كل عدد على أساس معامِل في الدالة مع صياغة المعامل الثاني بحيث يكون معامِلًا اختياريًا له قيمةً افتراضيةً ولتكن 1، ففي تلك الحالة في عدم تمرير قيمة للمعامِل الاختياري عند الاستدعاء، فستُنفَّذ الدالة مع تعويض قيمة المعامل الثاني بالقيمة الافتراضية 1.

def multiply (x, y = 1):
    return x * y

print (multiple(5))
>> 5

نلاحظ أنه لم نمرِّر المعامِل الثاني، وبما أنه يحمل قيمةً افتراضيةً هي العدد 1، فكانت النتيجة في ضرب العدد 5 بالعدد 1.

المعاملات الديناميكية ‎*args

من الميزات المفيدة في بايثون وجود إمكانية إضافة عدد غير محدَّد من المعامِلات في الدوال عند استدعائها، وتُضاف تلك الميزة عبر إضافة رمز النجمة * عند تعريف الدالة وقبل كتابة اسم المعامِل الذي سيحمل جميل القيم التي ستمرَّر عند استدعاء الدالة مرتبةً حسبما تم تمريره عند الاستدعاء.

في المثال التالي استقبلنا عددًا غير محدود من القيم، ثم استخدمنا حلقة التكرار for، من أجل جمع كل القيم وإعادة الناتج من الدالة.

def addition (*args):
    total = 0
    for number in args:
        total += number
    return total
print ( addition(5, 6, 7, 8, 14) )
>> 40

كما يمكن إضافة عدد محدد من المعاملات في الدالة أولًا، ثم إضافة معامل يمثل بقية المعاملات إن وجدت آخر المعاملات حصرًا ولا نضيفه قبلها، وإليك مثال بتعديل الدالة السابقة مثلًا بحيث تجمع كل المعاملات الإضافية الزائدة عن المعامل x ثم تضرب الناتج فيه:

def addition (x, *args):
    total = 0
    for number in args:
        total += number
    return x * total
print ( addition(2, 5, 6, 7, 8, 14) )
>> 80

ملاحظة: في حالة استخدام ميزة المعاملات الديناميكية في إحدى الدوال، لا يمكن تمرير قيم المعاملات عبر تحديد أسمائها بل يجب تمرير المعاملات حسب ترتيب تعريفها في الدالة.

كما أن من الميزات الأخرى في بايثون إمكانية إرجاع أكثر من قيمة من الدالة باستخدام الكلمة المفتاحية return ثم الفصل بين القيم المُراد إعادتها بالفاصلة، وعندئذ تُعيد الدالة قائمة تحتوي على القيم بالترتيب نفسه، إذ يمكننا مثلًا التعديل على المثال السابق لإعادة قيمتين كما يلي:

def addition(x, *args):
    total = 0
    for number in args:
        total += number
    return x * total, total
result = addition(2, 5, 6, 7, 8, 14)
print ( result[0], result[1] )
>> 80 40

المعاملات الديناميكية المسماة ‎**kwargs

قد تحتاج أحيانًا إلى تمرير عدة معاملات مسماة دون معرفة عددها إلى الدالة والتعامل معها باسمها بدلًا من أن تكون ممرَّرة بالترتيب وعشوائيًا إلى الدالة، لذا يمكن يمكن في هذه الحالة استعمال مفهوم المعاملات الديناميكية المسماة باستخدام ** قبل اسم المعامل في تعريف الدالة، ويصبح نوع المعامل آنذاك قاموسًا ويمكن الوصول إلى عناصره بسهولة، إليك مثال:

def print_name(**kid):
  print("His last name is " + kid["lname"])

print_name(fname = "Jamil", mname= "Ahmad", lname = "Alomar")

الدوال مجهولة الاسم

الدوال مجهولة الاسم Anonymous Functions وتُدعى أيضًا دوال لامدا Lambda Functions، وهي دوال لا تُعرَّف بالكلمة المفتاحية def وإنما تستخدم الكلمة المفتاحية lambda، وتُعرَّف بالشكل التالي بحيث توضع معاملات الدالة مكان args والتعليمة الوحيدة في الدالة مكان statement:

lambda args : statement

توجد الكثير من الاختلافات بينها وبين الدوال التي يعرِّفها المستخدِم، ونستطيع تلخيص تلك الاختلافات في نقطتين مهمتين قبل الانتقال إلى شرح هذا النوع من الدوال وحالات استخداماتها:

  • لا تستطيع الدوال المجهولة إعادة أكثر من قيمة واحدة.
  • لا تستطيع الدوال المجهولة احتواء أكثر من تعليمة واحدة.

بالنسبة للنقطة الأخيرة فهي مهمة جدًا، ففي حين يمكن للدوال التي يعرفها المستخدِم أن يحتوي جسمها على عدد غير محدود من التعليمات والأسطر، إذ تُستخدَم الدوال المجهولة غالبًا لتنفيذ عملية بسيطة مثل العمليات الحسابية لإعادة قيمة ما.

نعرِّف في المثال التالي دالةً مجهولة الاسم بسيطةً مهمتها الوحيدة هي جمع رقمين:

addition = lambda x, y : x + y

نلاحظ أنه قد أسنِدت الدالة إلى متغير addition، إذ يكون اسم المتغير هو نفسه اسم الدالة حتى نستطيع استدعاء الدالة عند الحاجة، وبذلك نستطيع استدعاء الدالة مع تمرير المعاملات المطلوبة x و y للحصول على ناتج الدالة بالشكل التالي:

print (addition(1, 2))
>> 3

نطاق المتغيرات Variables Scope

يجب هنا معرفة نطاق المتغيرات عند التعامل مع الدوال، فبناءً على معرفتك بها، تستطيع تحديد المتغيرات التي يمكنك استخدامها داخل الدالة، والمتغيرات التي يمكنك استخدامها في باقي البرنامج، فليست كل المتغيرات متاحة للوصول في كل الأماكن.

يمكن تقسيم نطاقات المتغيرات إلى نطاقين هما نطاق محلي Local Scope ونطاق عام Global Scope؛ أما قاعدة نطاقات المتغيرات فهي بسيطة للغاية، فعند تعريف متغير داخل دالة ما، فسيصبح ضمن النطاق المحلي للدالة، ولا يمكن الوصول إليه من خارجها، في حين أنّ المتغيرات المعرَّفة خارج الدالة تقع ضمن النطاق العام، ويمكن الوصول إليها من أيّ مكان، ومن داخل أي كتلة أو دالة، وبالتالي يمكن استدعاء المتغيرات المعرَّفة خارج الدالة من داخلها، بينما لا يمكن استدعاء المتغيرات المعرَّفة داخل الدالة من خارجها، وبناءً على ذلك سيعمل المثال التالي دون مشكلة، إذ تحاول الدالة طباعة متغير يقع تحت النطاق العام:

x = 'Hello'
def example():
    print (x)
example()
>> Hello

أما المثال التالي فلن يعمل أبدًا، إذ يحاول البرنامج خارج الدالة طباعة متغير معرَّف داخل النطاق المحلي للدالة:

def example():
    x = 'Hello'
print (x)
>> NameError: name 'x' is not defined

ولذلك يجب دائمًا مراعاة نطاقات المتغيرات أثناء كتابة البرامج.

الوحدات Modules

تُعَدّ الوحدات طريقةً لتنظيم الشيفرة وتجميعها في ملفات منفصلة، إذ يوفِّر ذلك التنظيم سهولةً في التعامل مع ملفات البرنامج، وسهولةً في قراءة الشيفرة والتعليمات، وتكون الوحدة بشكلها البسيط ملفًا بصيغة py يحتوي على أصناف أو دوال ومتغيرات تُستدعى من ملفات أخرى في البرنامج لاستخدامها مثل وجود وحدة منفصلة يُكتب فيها دوال تجري عمليات حسابية مختلفة مثل دالة لحساب مساحة الدوائر، وأخرى لحساب مساحة المثلثات، …إلخ. ثم تُستدعى تلك الوحدة من أيّ ملف يُراد فيها تنفيذ تلك المهام التي جرى تعريفها وكتابتها مسبقًا في إحدى تلك الدوال.

تُنشأ الوحدات بإنشاء ملف بايثون منفصل في المجلد نفسه الذي يحتوي على مشروعك، فإذا كان برنامجك الذي تكتبه حاليًا موجودًا في ملف program.py مثلًا، فبإمكانك بجوار ذلك الملف إنشاء وحدة من خلال إنشاء ملف آخر باسم الوحدة، إذ نستطيع في مثالنا أعلاه تسمية الوحدة باسم calculations.py، بحيث تكون الكلمة calculations هي اسم الوحدة؛ أما py فهي صيغة كل الملفات التي تحتوي على شيفرة بايثون، وبعد ذلك نستطيع استيراد تلك الوحدة من الملف الرئيسي لبرنامجك باستخدام الكلمة المفتاحية import متبوعة باسم الوحدة، وعندئذ يُتاح لك الوصول إلى متغيرات ودوال تلك الوحدة في ملفك.

لنضف الدالة التي كتبناها سابقًا لحساب مساحة الدائرة في تلك الوحدة الجديدة، لكن مع تعديل بسيط وهو أننا سنعرِّف المتغير الذي يحمل قيمة π في نطاق المتغيرات العام في ملف الوحدة كما يلي:

pi = 3.141592654
def circle_area (radius):
    r_squared = radius ** 2
    area = pi * r_squared
    return area

أما في ملف برنامجنا الرئيسي الآن، فسنستطيع البدء في استيراد الوحدة، ثم استخدام دالة حساب مساحة الدائرة والوصول إلى المتغير الذي يحمل قيمة π مباشرةً، إذ نستطيع مثلًا استقبال قيمة نصف القطر من المستخدِم أثناء تشغيل البرنامج، ثم استدعاء الدالة من الوحدة مباشرةً وطباعة النتيجة للمستخدِم كما في المثال الآتي:

import calculations
r = input ('Please enter R value: ')
r = float (r)
print (calculations.circle_area(r))

كما يمكن الوصول إلى متغيرات الوحدة مثل المتغير pi الذي يحمل قيمة π كما في المثال التالي:

import calculations
print (calculations.pi)
>> 3.141592654

من الميزات المفيدة التي تقدمها الوحدات في بايثون أنه بالإمكان استيراد متغير أو دالة معينة من الوحدة، عبر استخدام الكلمة المفتاحية from متبوعةً باسم الوحدة، ثم استخدام أمر الاستيراد import متبوعًا باسم المتغير أو الدالة المُراد استيرادها كما في المثال التالي:

from calculations import pi
print (pi)
>> 3.141592654

الجدير بالذكر أنه عند محاولة استيراد إحدى الوحدات أو المكتبات، فسيُجري مفسر بايثون بضع محاولات لتحديد مكان الوحدة المطلوبة، وتلك المحاولات مرتبة كما يلي:

  1. البحث عن اسم الوحدة في الملفات الموجودة في المجلد نفسه الذي يحتوي على البرنامج.
  2. إذا لم يستطع إيجاد الوحدة، فسيبحث في جميع مسارات المجلدات المخزنة في متغير البيئة PYTHONPATH.
  3. إذا لم يستطع إيجاد الوحدة، فسيبحث في المسار الرئيسي المثبَّت فيه لغة بايثون، والذي يحتوي عادةً على المكتبات التي ثبِّتت على الحاسوب لتُستخدَم في جميع برامج بايثون.

ومن أهم المعلومات الواجب ذكرها، قبل الانتهاء من الحديث عن الوحدات، أنه يوجد اختلاف بين الوحدات وبين المكتبات على الرغم من كون طريقة الاستيراد لكلاهما متشابهة، إذ تستورَد المكتبة كاملًا عن طريق الكلمة المفتاحية الخاصة بالاستيراد، ويستورَد جزء من المكتبة بالكلمة المفتاحية from، لكن قد تحتوي المكتبة على العديد من الوحدات والملفات، فغالبًا تكون المكتبة تطبيقًا يحتوي على العديد من الوحدات، التي بدورها تحتوي على تصنيفات ودوال ومتغيرات، وتكون تلك المكتبة في مجلد منفصل.

يتطلب إنشاء مكتبة إنشاء مجلد يحتوي على ملف فارغ باسم init__.py__ ليُعرَّف المجلد على أساس مكتبة بايثون، ثم يُنشأ أيّ عدد من الوحدات -أي الملفات- لتُستورَد بعد ذلك من خلال استيراد اسم المكتبة، ولمزيد من التوضيح فإنه يمكن إنشاء مكتبة -أي مجلد- باسم calculations من أجل المثال الذي أوردناه لدالة حساب مساحة الدائرة، ثم ننشئ ملفًا فارغًا باسم init__.py__ ثم ننشئ ملفًا باسم circle.py يحتوي على محتوى الوحدة نفسها التي كتبناها للتو وبعد ذلك تكون قد حوِّلت الوحدة calculations إلى مكتبة يمكن استيراد إحدى وحداتها كما في المثال التالي:

from calculations import circle
print (circle.pi)
>> 3.141592654

نلاحظ أننا استوردنا الوحدة circle من المكتبة ولم نستورِد المكتبة كلها، ونستطيع التفصيل بصورة أكبر من ذلك حتى، إذ نستطيع استيراد المتغير π فقط من داخل الوحدة كما في المثال التالي:

from calculations.circle import pi
print (pi)
>> 3.141592654

وبناءً على ذلك يوجد في بايثون قدر كبير من الديناميكية عن نظيراتها من اللغات، إذ أنه كما أوردنا توجد كلمات مفتاحية تستخدَم بطرق مختلفة، كما أن ديناميكية تعريف المتغيرات وأنواع البيانات فيها أيضًا ميزة للغاية، وتوفِّر مزيدًا من التسهيلات للمبرمجين ليكون التركيز الرئيسي للمبرمج هو كتابة مهمات ودوال برنامجه، في حين يتعامل مفسر اللغة مع الحاسوب والذاكرة.

للمزيد من التفاصيل حول هذه النقطة، ننصحك بقراءة مقال البرمجة باستخدام الوحدات.

خاتمة

إلى هنا، تعرفنا على مفهوم الدوال وكيفية تعريف دالة في لغة بايثون وهي الدوال المخصصة التي يحتاج إليها المبرمج في شيفرته ولكن عمومًا هنالك الكثير من الدوال الجاهزة المكتوبة مسبقًا توفرها أي لغة برمجة ومنها لغة بايثون تسهل على المبرمج وتجنبه من كتابة دوال مكررة في كل برنامج وهو ما سنتعرف عليه في المقال التالي.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...