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

الدوال في لغة بايثون Python


عبد اللطيف ايمش

تعرفت في المقالات السابقة على الدوال print()‎ و input()‎ و len()‎، وأصبحت تعرف بتوفير بايثون عدّة دوال مضمنة في اللغة built-in functions مثلها، لكنك تستطيع كتابة دوال خاصة بك أيضًا.

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

 def hello():
     print('Howdy!')
       print('Howdy!!!')
       print('Hello there.')

 hello()
   hello()
   hello()

أول سطر هو عبارة def التي تعرف دالةً باسم hello()‎، والشيفرة التي تلي عبارة def هي جسم الدالة ➋، الذي يحتوي على الشيفرة التي ستنفذ حين استدعاء call الدالة، وليس حين تعريف الدالة.

الأسطر الثلاثة التي تحتوي على hello()‎ ➌ هي استدعاءات الدالة، وعملية استدعاء الدالة هي كتابة اسمها متبوعًا بقوسين، وقد تمرر إليها بعض الوسائط arguments بين القوسين.

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

ولأننا استدعينا الدالة hello()‎ ثلاث مرات، فإن الشيفرة الموجودة داخل الدالة hello()‎ ستنفذ ثلاث مرات، وحينما تشغِّل البرنامج السابق سيظهر لديك:

Howdy!
Howdy!!!
Hello there.
Howdy!
Howdy!!!
Hello there.
Howdy!
Howdy!!!
Hello there.

أحد الأغراض الأساسية من تعريف الدوال هو تجميع الشيفرات مع بعضها والتي يمكن أن تنفذ عدة مرات، فلو لم تكن لدينا الدالة السابقة فسنحتاج إلى نسخ ولصق الشيفرة عدة مرات كما يلي:

print('Howdy!')
print('Howdy!!!')
print('Hello there.')
print('Howdy!')
print('Howdy!!!')
print('Hello there.')
print('Howdy!')
print('Howdy!!!')
print('Hello there.')

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

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

عبارة def مع معاملات

تذكر حينما كنت تستدعي الدالة print()‎ أو len()‎ كنت تمرر إليها قيمًا تسمى بالوسائط arguments، وذلك بكتابتها بين القوسين. يمكنك أيضًا أن تعرف دوالك التي تقبل وسائط، وجرب هذا المثال في ملف باسم helloFunc2.py:

 def hello(name):
     print('Hello, ' + name)

 hello('Alice')
   hello('Bob')

ستبدو المخرجات كما يلي حين تشغيل البرنامج السابق:

Hello, Alice
Hello, Bob

لاحظ أن تعريفنا للدالة hello()‎ في هذا المثال يحتوي على معامل باسم name ➊. المعاملات parameters هي المتغيرات التي تحتوي على قيمة الوسائط arguments. أي حينما نستدعي دالةً مع وسائط arguments فإن قيمة تلك الوسائط ستكون مخزنة في المعاملات parameters.

حين استدعاء الدالة hello()‎ لأول مرة سنمرر إليها القيمة 'Alice' ➌ وسينتقل التنفيذ إلى داخل الدالة، وستُضبَط قيمة المعامل name إلى القيمة 'Alice' تلقائيًا، ومن ثم ستطبع هذه القيمة باستخدام الدالة print()‎ ➋.

أحد الأمور التي من المهم تذكرها حول المعاملات هي أن القيمة المخزنة فيها ستنسى حين إنتهاء تنفيذ الدالة، فلو كتبت print(name) بعد استدعاء hello('Bob')‎ في البرنامج السابق، فسيظهر لك الخطأ NameError لعدم وجود متغير باسم name، وذلك لأن برنامجنا سيحذف المتغير name بعد إنتهاء استدعاء الدالة hello('Bob')‎ ولذا سنشير في print(name)‎‎ إلى المتغير name الذي لن يكون موجودًا.

هذا يشبه كثيرًا كيف تحذف قيمة المتغيرات في برامجنا السابقة من الذاكرة حين انتهاء تنفيذها. وسنتحدث عن سبب حدوث ذلك تفصيلًا لاحقًا في هذا المقال حينما نتتحدث عن النطاق المحلي local scope.

التعريف والاستدعاء والتمرير والوسائط والمعاملات!

كثرت علينا المصطلحات الجديدة كالتعريف define والاستدعاء call والتمرير pass والوسائط arguments والمعاملات parameters. لننظر سويةً إلى الشيفرة الآتية لمراجعتها:

 def sayHello(name):
    print('Hello, ' + name)
 sayHello('Abdullatif')

تعريف الدالة يعني إنشاءها، وكما في عبارة الإسناد spam = 42 التي تنشِئ المتغير spam سنستعمل العبارة def لتعريف الدالة sayHello()‎ ➊. السطر الذي فيه sayHello('Abdullatif')‎ ➋ يستدعي الدالة التي أنشأناها، مما ينقل تنفيذ البرنامج إلى بداية الشيفرة الموجودة داخل الدالة، وسطر الاستدعاء السابق يمرر السلسلة النصية 'Abdullatif' إلى الدالة، والقيمة التي تمرر إلى الدالة تسمى وسيطًا، وسيُسند الوسيط 'Abdullatif' إلى المتغير المحلي المسمى name، وتسمى المتغيرات التي تحمل قيمة الوسائط الممررة إلى الدالة بالمعاملات.

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

القيم المعادة وعبارة return

عندما تستدعي الدالة len()‎ وتمرر إليها وسيطًا مثل 'Hello' فستكون القيمة الناتجة من الدالة هي الرقم 5، الذي يمثل طول السلسلة النصية التي مررتها إليها. يمكننا القول عمومًا أن الناتج إحدى الدوال يسمى بالقيمة المعادة return value من تلك الدالة.

حينما تنشِئ دالةً باستخدام العبارة def فيمكنك أن تحدد ما هي القيمة المعادة من الدالة باستخدام العبارة return. تتألف عبارة return من:

  • الكلمة المحجوزة return
  • قيمة أو تعبير برمجي يجب أن تعيدها الدالة

حين استخدام تعبير برمجي مع عبارة return فستكون القيمة المعادة هي ناتج ذاك التعبير. فمثلًا البرنامج الآتي يعرف دالةً تعيد سلسلةً نصيةً مختلفةً اعتمادًا على الرقم المُمرَّر إليها كوسيط. احفظ الشيفرة الآتية في ملف باسم magic8Ball.py:

 import random

 def getAnswer(answerNumber):
     if answerNumber == 1:
           return 'It is certain'
       elif answerNumber == 2:
           return 'It is decidedly so'
       elif answerNumber == 3:
           return 'Yes'
       elif answerNumber == 4:
           return 'Reply hazy try again'
       elif answerNumber == 5:
           return 'Ask again later'
       elif answerNumber == 6:
           return 'Concentrate and ask again'
       elif answerNumber == 7:
           return 'My reply is no'
       elif answerNumber == 8:
           return 'Outlook not so good'
       elif answerNumber == 9:
           return 'Very doubtful'

 r = random.randint(1, 9)
 fortune = getAnswer(r)
 print(fortune)

حين يبدأ تنفيذ البرنامج السابق فستسورد بايثون الوحدة random ➊، ثم ستعرف الدالة getAnswer()‎ ➋، ولأننا عرفنا الدالة ولم نستدعها فلن تنفذ الشيفرة الموجودة داخلها، بل سينتقل التنفيذ مباشرةً إلى استدعاء الدالة random.randint()‎ التي مررنا إليها وسيطين هما 1 و 9 ➍ وسيعاد منها عدد عشوائي بين 1 و 9 (بما في ذلك الرقمين 1 و 9) وتخزن هذه القيمة في المتغير r.

تستدعى الدالة getAnswer()‎ مع تمرير الوسيط r ➎، وينتقل التنفيذ إلى بداية الدالة getAnswer()‎ ➌، وستخزن قيمة الوسيط r في المعامل answerNumber، ثم اعتمادًا على القيمة الموجودة في answerNumber فستعيد الدالة إحدى السلاسل النصية المعرفة مسبقًا، ومن ثم سيعود التنفيذ إلى النقطة التي استدعيت فيها الدالة getAnswer()‎ ➎ وستسند السلسلة النصية المعادة إلى المتغير ذي الاسم fortune، الذي سيمرر بدوره إلى الدالة print()‎ ➏ ويطبع على الشاشة.

لاحظ أنك تستطيع تمرير القيم المعادة من الدوال كوسيط مباشرةً إلى دوال أخرى، لذا يمكنك أن تختصر الأسطر الثلاثة الآتية:

r = random.randint(1, 9)
fortune = getAnswer(r)
print(fortune)

إلى السطر المكافئ:

print(getAnswer(random.randint(1, 9)))

تذكر أن التعابير البرمجية تتألف من قيم وعوامل، ويمكن استخدام استدعاءات الدوال في تعبير برمجي لأن الاستدعاء سيؤول إلى قيمة معادة من تلك الدالة.

القيمة None

هنالك قيمة في بايثون تسمى None وهي تمثل عدم وجود قيمة. والقيمة None هي القيمة الوحيدة لنوع البيانات NoneType، وقد تسمي لغات البرمجة الأخرى هذه القيمة بالاسم null أو nil أو undefined. وكما في القيم المنطقية True و False فيجب أن نكتب None بحرف N كبير.

يمكن أن تفيد هذه القيمة-التي-ليس-لها-قيمة حين الحاجة إلى استعمال قيمة لا تمثل شيئًا، فإحدى استخدامات None مثلًا هي القيمة المعادة من الدالة print()‎ التي تطبع نصًا على الشاشة، لكنها لا تعيد أي قيمة على عكس دوال أخرى مثل len()‎ أو input()‎، ولأن من المفترض أن يكون هنالك قيمة ناتجة لجميع استدعاءات الدوال في بايثون فاستدعاء print()‎ سيعيد القيمة None. لنجرب السطرين الآتيين في الصدفة التفاعلية لنرى ذلك:

>>> spam = print('Hello!')
Hello!
>>> None == spam
True

تضيف بايثون العبارة return None وراء الكواليس لأي دالة لا يكون لها عبارة return محددة، وهذا ما يشبه كيف تحتوي حلقات التكرار while و for على عبارة continue ضمنية في نهايتها.

ستعاد القيمة None أيضًا إن استخدام عبارة return دون قيمة، أي كتبت الكلمة المفتاحية return كما هي.

وسطاء الكلمات المفتاحية والدالة print()‎

تعرف أغلبية الوسائط بموضعها حين استدعاء الدالة، فمثلًا الاستدعاء random.randint(1, 10)‎ مختلف عن الاستدعاء random.randint(10, 1)‎، فحينما نستدعي الدالة random.randint(1, 10)‎ فستعيد لنا رقمًا صحيحًا بين 1 و 10 لأن أول وسيط هو الحد الأدنى من المجال والوسيط الثاني هو الحد الأقصى، بينما يسبب استدعاء random.randint(10, 1)‎ خطأً.

لكن بدلًا من تعريف قيم الوسائط عبر موضعها، يمكن أن تعرف وسطاء الكلمات المفتاحية keyword arguments بوضع كلمة مفتاحية قبلها حين استدعاء الدالة، وتستخدم وسطاء الكلمات المفتاحية عادةً للمعاملات الاختيارية optional parameters.

فمثلًا تمتلك الدالة print()‎ معاملين اختياريين هما end و sep لضبط ما الذي سيطبع بعد نهاية طبع الوسائط الممررة إليها وما الذي سيطبع بين تلك الوسائط على التوالي.

إذا شغلنا برنامجًا يحتوي على الشيفرة الآتية:

print('Hello')
print('World')

فسيكون الناتج كما يلي:

Hello
World

لاحظ أن السلسلتين النصيتين المطبوعتين مفصولتان بسطر وذلك لأن الدالة print()‎ تضيف تلقائيًا محرف السطر الجديد newline character في نهاية السلسلة النصية التي تمرر إليها كوسيط. إلا أننا نستطيع ضبط قيمة الوسيط end لتغيير محرف السطر الجديد إلى سلسلة نصية مختلفة، فمثلًا إذا كتبت الشيفرة الآتية:

print('Hello', end='')
print('World')

فسيكون الناتج:

HelloWorld

سيطبع الناتج في سطر وحيد لعدم وجود محرف السطر الجديد بعد السلسلة النصية 'Hello' لأننا مررنا سلسلة نصية فارغة، وهذا ما يفيدك إن أردت تعطيل الإضافة التلقائية للسطر الجديد في نهاية كل استدعاء للدالة print()‎.

إذا مررت عدة سلاسل نصية إلى الدالة print()‎ فستفصل الدالة بينها تلقائيًا بفراغ واحد. جرب إدخال السطر الآتي في الصدفة التفاعلية:

>>> print('cats', 'dogs', 'mice')
cats dogs mice

يمكنك تبديل السلسلة النصية التي تفصل بين الوسائط التي ستطبع باستخدام الوسيط sep وتمرير سلسلة نصية مختلفة إليه، جرّب ذلك في الصدفة التفاعلية:

>>> print('cats', 'dogs', 'mice', sep=',')
cats,dogs,mice

يمكنك إضافة وسائط مفتاحية في الدوال التي تكتبها أيضًا، لكن عليك أن تتعلم أولًا عن القوائم list والقواميس dictionary التي سنشرحها في مقالات لاحقة. لكن كل ما عليك معرفته الآن هو أن بعض الدوال لها معاملات اختيارية ويكون لها مفاتيح يمكن الوصول إليها لضبط قيمتها حين استدعاء الدالة.

مكدس الاستدعاء Call Stack

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

22-callstack-معرب.jpg

مكدس القصص في دردشتك.

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

لنفهم ما يحدث بالتفصيل سنجرب المثال الآتي بعد حفظه في ملف باسم abcdCallStack.py:

   def a():
       print('a() starts')
     b()
     d()
       print('a() returns')

   def b():
       print('b() starts')
     c()
       print('b() returns')

   def c():
     print('c() starts')
       print('c() returns')

   def d():
       print('d() starts')
       print('d() returns')

 a()

سينتج البرنامج ما يلي حين تشغيله:

a() starts
b() starts
c() starts
c() returns
b() returns
d() starts
d() returns
a() returns

أعرف أن الفقرة الآتية متداخلة، لكن حاول أن تركز معي فيها: حين استدعاء a()‎ ➎ فستستدعي b()‎ ➊ التي بدورها ستستدعي c()‎ ➌. والدالة c()‎ لا تستدعي غيرها بل تعرض العبارة c() starts ➍ و c() returns قبل أن يعود التنفيذ إلى السطر الذي استدعاها في b()‎ ➌.

بعد أن يعود التنفيذ إلى الشيفرة في b()‎ التي استدعت c()‎ فستنتهي الدالة b()‎ وتطبع b() returns ثم يعود التنفيذ إلى السطر الذي استدعى b()‎ في a()‎ ➊.

سيستمر التنفيذ في الدالة a()‎ وستستدعى الدالة d()‎، والتي تشبه الدالة c()‎ في كونها لا تستدعي دالةً غيرها بل تطبع d()‎ starts و d() returns قبل أن تعود إلى السطر الذي استدعاها في a()‎ ثم سيكمل التنفيذ من هناك، وسيطبع آخر سطر من a()‎ العبارة a()‎ returns قبل أن ينتهي تنفيذ الدالة a()‎ ونصل إلى نهاية البرنامج.

مكدس الاستدعاء call stack هو الآلية التي تستعملها بايثون لتذكر أين سيعود التنفيذ بعد انتهاء تنفيذ استدعاء كل دالة. لا يخزن مكدس الاستدعاء في متغير في برنامجك وإنما تتولى بايثون أمره خلف الكواليس. فحينما يستدعي برنامجك دالةً فستنشِئ بايثون كائن إطار frame object فوق مكدس الاستدعاء، وتخزن كائنات الإطار frame objects رقم السطر الذي استدعى الدالة لكي تعرف بايثون أين يجب أن تعيد القيمة الناتجة من ذاك الاستدعاء. إذا استدعيت دالة أخرى فستضع بايثون كائن إطار آخر في المكدس فوق السابق.

بعد إعادة استدعاء الدالة فستحذف بايثون كائن الإطار من أعلى المكدس وتكمل التنفيذ من السطر المخزن في ذاك الكائن. لاحظ أن كائنات الإطار تضاف وتحذف من أعلى المكدس وليس من أي مكان آخر. يوضح الشكل الموالي حالة مكدس الاستدعاء في البرنامج abcdCallStack.py حين استدعاء والعودة من كل دالة:

23-abcd-callstack(1).jpg

كائنات الإطار لمكدس الاستدعاء في كل مرحلة من مراحل تنفيذ البرنامج abcdCallStack.py.

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

المجالات العامة والمحلية

تكون المعاملات parameters والمتغيرات الموجودة داخل إحدى الدوال ضمن مجال محلي local scope. أما المتغيرات التي تسند قيمتها خارج جميع الدوال تكون موجودة في المجال العام global scope.

وبالتالي يسمى المتغير الموجود في مجال محلي بالمتغير المحلي local variable، بينما يسمى المتغير الموجود في المجال العام بالمتغير العام global variable؛ ويجب أن يكون المتغير عامًا أو محليًا، ولا يمكنه أن يكون كلاهما معًا.

يمكنك تخيل المجالات scopes على أنها حاوية للمتغيرات؛ فحينما ينتهي وجود أحد المجالات المحلية فستحذف جميع المتغيرات الموجودة فيه. لاحظ وجود مجال عام وحيد الذي سينُشَأ حينما يبدأ تنفيذ برنامجك وينتهي بإنتهاء تنفيذه. يمكنك التأكد من حذف المتغيرات في المجال العام بعد إنتهاء تنفيذ البرنامج بتجربة الوصول إلى المتغيرات من برنامج آخر.

يُنشَأ المجال المحلي في كل مرة تستدعى فيها إحدى الدوال. فتكون جميع المتغيرات المسندة ضمن الدالة موجودة في المجال المحلي. وحين إعادة قيمة return من الدالة فسينتهي وجود المجال المحلي وستحذف قيمة تلك المتغيرات؛ ولن يتذكر برنامج قيمة المتغيرات المخزنة من الاستدعاء السابق للدالة. تخزن المتغيرات المحلية أيضًا في كانئات الإطار frame objects في مكدس الاستدعاء call stack.

يهمنا معرفة المجال المستخدم لعدة أسباب:

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

السبب في امتلاك بايثون لمجالات مختلفة بدل من جعل جميع المتغيرات عامة هو أن بعض المتغيرات تعدل من شيفرة معينة ضمن دالة ما، وتتفاعل هذه الدالة مع بقية البرنامج عبر المعاملات parameters الممررة إليها وعبر القيمة المعادة منها؛ وهذا ما يقلل عدد الأسطر البرمجية التي تتعامل مع متغير ما وتسبب مشكلة برمجية، فإذا كان يحتوي برنامجك على متغيرات عامة فقط وحصل خطأ بسبب ضبط أحد المتغيرات إلى قيمة خطأ فسيصعب كثيرًا تتبع المشكلة في برنامجك، فقد يكون عدد الأسطر البرمجية بالمئات أو الآلاف التي قد تستطيع تعديل قيمة هذا المتغير؛ أما لو كانت العلة البرمجية بسبب قيمة خطأ لمتغير محلي فستعرف تحديدًا ما هي الأسطر البرمجية المسؤولة عن ضبط تلك القيمة وستحل المشكلة بسهولة وسرعة.

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

لا يمكن استخدام المتغيرات المحلية في المجال العام

أمعن النظر في البرنامج الآتي الذي سيتسبب بخطأ حين محاولة تشغيله:

def spam():
 eggs = 31337
spam()
print(eggs)

إذا جربت هذا البرنامج فسيبدو الناتج كما يلي:

Traceback (most recent call last):
  File "C:/test1.py", line 4, in <module>
    print(eggs)
NameError: name 'eggs' is not defined

سيحدث الخطأ بسبب وجود المتغير eggs داخل المجال المحلي المنشأ من الدالة spam()‎ ➊. فبعد انتهاء تنفيذ الدالة spam فسيحذف المجال المحلي ولن يبقى هنالك أي متغير باسم eggs، وحينما يحاول البرنامج تشغيل السطر print(eggs)‎ فستعطيك بايثون خطأً تقول فيه أن المتغير eggs غير معرف، وهذا منطقي إذا فكرت مليًا بالأمر؛ فحينما يكون تنفيذ البرنامج في المجال العام فلا توجد أي مجالات محلية ولن تكون هنالك أي متغيرات محلية، وبالتالي لا يمكننا استخدام سوى المتغيرات العامة في المجال العام.

لا يمكن استخدام المتغيرات المحلية في مجالات محلية أخرى

ينشأ مجال محلي جديد في كل مرة تستدعى فيها إحدى الدوال، بما في ذلك حين استدعاء دالة ضمن دالة أخرى. انظر إلى المثال الآتي:

  def spam():
     eggs = 99
     olive()
     print(eggs)

   def olive():
       steak = 101
     eggs = 0

 spam()

حينما يبدأ تشغيل البرنامج فستستدعى الدالة spam()‎ ➎ وسينشأ مجال محلي، وسيضبط المتغير المحلي eggs ➊ إلى 99، ثم ستستدعى الدالة olive()‎ ➋، ثم سينشأ مجال محلي جديد؛ فمن الممكن أن تكون عدة مجالات محلية موجودة جنبًا إلى جنب. وسنضبط قيمة المتغير المحلي Steak إلى 101، وسننشِئ المتغير المحلي eggs -المختلف كليًا عن المتغير الذي يحمل نفس الاسم في المجال المحلي للدالة spam()‎- ونضبط قيمته إلى 0 ➍. حين إعادة الدالة olive()‎ فسيحذف المجال المحلي المنشأ بسبب استدعائها، بما في ذلك المتغير eggs الخاص بها. وسيكمل تنفيذ البرنامج في الدالة spam()‎ ليطبع لنا قيمة المتغير eggs ➌؛ ولأن المجال المحلي الخاص بالدالة spam()‎ ما يزال موجودًا فستكون قيمة eggs هي 99 كما ضبطناها سابقًا في ذلك المجال.

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

يمكن قراءة المتغيرات العامة من مجال محلي

أمعن النظر في المثال الآتي:

def spam():
    print(eggs)
eggs = 42
spam()
print(eggs)

حينما حاولنا طباعة المتغير eggs ضمن الدالة spam()‎ بحثت بايثون عن متغير أو معامل باسم eggs في الدالة spam()‎ لكنها لم تجد، فحينها ستعدّه إشارةً إلى المتغير العام eggs، ولهذا سيطبع البرنامج السابق القيمة 42 حين تنفيذه.

المتغيرات المحلية والعامة التي تحمل الاسم نفسه

من المقبول تمامًا من الناحية التقنية في بايثون استخدام نفس الاسم لمتغير في المجال العام وآخر في المجال المحلي؛ لكن لتسهيل مقروئية الشيفرة فحاول تجنب فعل ذلك. لترى ما سيحدث فجرب الشيفرة الآتية:

   def spam():
     eggs = 'spam local'
       print(eggs)    # 'spam local'

   def olive():
     eggs = 'olive local'
       print(eggs)    # 'olive local'
       spam()
       print(eggs)    # 'olive local'

 eggs = 'global'
   olive()
   print(eggs)        # 'global'

سيظهر الناتج الآتي حينما تجرب تشغيل البرنامج السابق:

olive local
spam local
olive local
global

هنالك ثلاثة متغيرات مختلفة في البرنامج، لكنها كلها مسماة eggs، وهي كما يلي:

  • ➊ متغير باسم eggs موجود في المجال المحلي للدالة spam()‎.
  • ➋ متغير باسم eggs موجود في المجال المحلي للدالة olive()‎.
  • ➌ متغير باسم eggs موجود في المجال العام.

ولأن هذه المتغيرات المختلفة لها نفس الاسم فسيكون من العسير تتبع أيها يستعمل الآن؛ لذا تجنب استخدام نفس الاسم لأكثر من متغير.

العبارة البرمجية global

إذا أردت تعديل قيمة متغير عام ضمن دالة، فعليك استخدام العبارة البرمجية global. فإذا كان لديك سطر يشبه global eggs في بداية إحدى الدوال فهذا سيخبر بايثون أن «المتغير eggs في هذه الدالة يشير إلى متغير عام، ولا حاجة إلى إ،شاء متغير محلي بهذا الاسم». فمثلًا جرب الشيفرة الآتية:

def spam():
   global eggs
   eggs = 'spam'

eggs = 'global'
spam()
print(eggs)

استدعاء الدالة print()‎ سيؤدي إلى إظهار الناتج الآتي:

spam

ولأننا صرحنا أن المتغير eggs هو عام global في بداية الدالة spam()‎ ➊، فحينما نضبط eggs إلى 'spam' ➋ فستجرى عملية الإسناد إلى المتغير eggs العام، ولن ينشأ أي متغير محلي.

هنالك أربع قواعد لمعرفة إن كان المتغير في المجال المحلي أم العام:

  • إذا كان المتغير مستخدمًا في المجال العام، أي خارج جميع الدوال، فسيكون متغيرًا عامًا دومًا.
  • إذا كانت هنالك عبارة global في إحدى الدوال، فسيكون المتغير عامًا في تلك الدالة.
  • خلاف ذلك إذا استخدم المتغير في عبارة الإسناد داخل دالة ما، فسيكون متغيرًا محليًا.
  • لكن إن لم يستخدم ذاك المتغير في عبارة إسناد داخل الدالة فسيكون متغيرًا عامًا.

لتأخذ فكرة أفضل عن هذه القواعد فاكتب البرنامج الآتي في محرر الشيفرات واحفظه وجربه:

def spam():
   global eggs
     eggs = 'spam' # this is the global

def olive():
   eggs = 'olive' # this is a local

def Steak():
   print(eggs) # this is the global

eggs = 42 # this is the global
spam()
print(eggs)

سيكون المتغير eggs في الدالة spam()‎ عامًا لوجود العبارة global في بداية الدالة ➊، وسيكون المتغير eggs محليًا في الدالة olive()‎ لاستعماله في عبارة إسناد في تلك الدالة ➋، وسيكون eggs عامًا في steak()‎ ➌ لعدم استخدامه في عبارة إسناد أو العبارة global. إذا شغلت البرنامج السابق فستكون النتيجة هي:

spam

كقاعدة عامة: سيكون المتغير في دالةٍ ما إما عامًا أو محليًا، ولا يمكن استخدام متغير محلي في دالة باسم eggs ثم استخدام متغير عام بنفس الاسم لاحقًا في الدالة ذاتها.

اقتباس

ملاحظة: إذا أردت تعديل قيمة مخزنة في متغير عام ضمن دالة فيجب عليك دومًا استخدام العبارة global.

إذا حاولت استخدام متغير محلي ضمن دالة قبل أن تسند قيمةً له كما في البرنامج الآتي، فستظهر لك رسالة خطأ:

   def spam():
       print(eggs) # ERROR!
     eggs = 'spam local'

 eggs = 'global'
   spam()

ستظهر رسالة الخطأ الآتية إذا حاولت تجربة البرنامج السابق:

Traceback (most recent call last):
  File "C:/sameNameError.py", line 6, in <module>
    spam()
  File "C:/sameNameError.py", line 2, in spam
    print(eggs) # ERROR!
UnboundLocalError: local variable 'eggs' referenced before assignment

يظهر الخطأ لأن بايثون سترى عبارة إسناد للمتغير eggs ضمن الدالة spam()‎ ➊ وبالتالي ستعد المتغير على أنه محلي، لكننا نحاول طباعة قيمة eggs قبل إسناد أي قيمة له، أي أن المتغير المحلي eggs غير موجود، فسيظهر الخطأ ولن تستعمل بايثون المتغير العام eggs ➋.

تعامل مع الدوال على أنها «صناديق سوداء»

عادةً كل ما تحتاج إليه لاستخدام دالة هو معرفة ما هي المدخلات (أي ما هي المعاملات التي تأخذها) وما هي المخرجات؛ فلا حاجة إلى أن تثقل على نفسك بمعرفة كيف تعمل شيفرة تلك الدالة. فحاول أن تنظر إلى الدوال نظرة شاملة عالية المستوى، وبإمكانك معاملتها على أنها «صندوق أسود».

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

التعامل مع الاستثناءات

حينما يحدث خطأ -أو بتعبير أدق «استثناء» exception- في برنامجك فهذا يعني توقف عملية التنفيذ كلها. ولا تريد أن يحدث ذلك عمليًا في البرامج الحقيقية، وإنما تريد أن يحس برنامجك بوجود الأخطاء ويتعامل معها ثم يكمل تنفيذه بسلام.

فالبرنامج الآتي يتسبب بخطأ القسمة على صفر. جربه:

def spam(divideBy):
    return 42 / divideBy

print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))

عرفنا الدالة spam()‎ ومررنا إليها وسيطًا بقيم مختلفة وطبعنا قيمة قسمة العدد 42 على القيمة الممررة. هذا هو ناتج تنفيذ الشيفرة السابقة:

21.0
3.5
Traceback (most recent call last):
  File "C:/zeroDivide.py", line 6, in <module>
    print(spam(0))
  File "C:/zeroDivide.py", line 2, in spam
    return 42 / divideBy
ZeroDivisionError: division by zero

يظهر الاستثناء ZeroDivisionError حينما نقسم عددًا على صفر. ويظهر لنا رقم السطر الذي يسبب هذا الاستثناء، وستعرف منه أن العبارة return في الدالة spam()‎ هي من تسبب الخطأ.

يمكن التعامل مع الاستثناءات باستخدام العبارتين try و except. إذ نضع الشيفرة التي قد تسبب خطأ أو استثناءً ضمن قسم العبارة try، وسينتقل تنفيذ البرنامج إلى بداية القسم الذي يلي العبارة except في حال حدوث استثناء.

يمكنك وضع الشيفرة التي قد تسبب بخطأ القسمة على الصفر ضمن كتلة try واستخدام كتلة except للتعامل مع حدوث الخطأ:

def spam(divideBy):
    try:
        return 42 / divideBy
    except ZeroDivisionError:
        print('Error: Invalid argument.')

print(spam(2))
print(spam(12))
print(spam(0))
print(spam(1))

حينما تتسبب الشيفرة الموجودة ضمن try باستثناء، فسينتقل تنفيذ البرنامج مباشرةً إلى الشيفرة الموجودة في except، وبعد تنفيذ تلك الشيفرة فسيكمل تنفيذ البرنامج بشكل طبيعي. ستكون نتيجة تنفيذ الشيفرة السابقة هي:

21.0
3.5
Error: Invalid argument.
None
42.0

لاحظ أن أية أخطاء تحدث أثناء استدعاءات الدوال ضمن كتلة try فستعالج أيضًا. جرب البرنامج الآتي التي يستدعي الدالة spam()‎ ضمن كتلة try:

def spam(divideBy):
    return 42 / divideBy

try:
    print(spam(2))
    print(spam(12))
    print(spam(0))
    print(spam(1))
except ZeroDivisionError:
    print('Error: Invalid argument.')

سيكون الناتج كما يلي:

21.0
3.5
Error: Invalid argument.

سبب عدم تنفيذ print(spam(1))‎ هو انتقال التنفيذ إلى الشيفرة الموجودة في كتلة except مباشرةً، ولن تعود لإكمال بقية كتلة try، بل ستكمل تنفيذ بقية البرنامج كالمعتاد.

برنامج قصير لرسم زكزاك

لنستعمل المفاهيم البرمجية التي تعلمناها حتى الآن لإنشاء برنامج حركي بسيط. سينشِئ هذا البرنامج شكل زكزاك إلى أن يوقفه المستخدم بالضغط على زر Stop في محرر Mu أو بالضغط على Ctrl+C. سيبدو ناتج البرنامج بعد تنفيذه كما يلي:

    ********
   ********
  ********
 ********
********
 ********
  ********
   ********
    ********

اكتب الشيفرة الآتية واحفظها في ملف باسم zigzag.py:

import time, sys
indent = 0 # كم فراغًا نضع كمسافة بادئة
indentIncreasing = True # هل ستزيد المسافة البادئة أم لا

try:
    while True: # حلقة تكرار البرنامج الأساسية
        print(' ' * indent, end='')
        print('********')
        time.sleep(0.1) # توقف لعُشر ثانية

        if indentIncreasing:
            # زيادة المسافة البادئة
            indent = indent + 1
            if indent == 20:
                # تغيير الاتجاه
                indentIncreasing = False

        else:
            # إنقاص عدد الفراغات
            indent = indent - 1
            if indent == 0:
                # تغيير الاتجاه
                indentIncreasing = True
except KeyboardInterrupt:
    sys.exit()

لننظر إلى الشيفرة سطرًا بسطر بدءًا من الأعلى.

import time, sys
indent = 0 # كم فراغًا نضع كمسافة بادئة
indentIncreasing = True # هل ستزيد المسافة البادئة أم لا

في البداية علينا أن نستورد الوحدتين time و sys، وسيستخدم برنامجنا متغيرين اثنين: المتغير indent الذي يتتبع كم فراغًا يجب أن نضع كمسافة بادئة قبل النجوم الثمانية، و indentIncreasing الذي يحتوي على قيمة منطقية بوليانية لتحدد إذا كانت المسافة البادئة ستزيد أم تنقص.

try:
    while True: # حلقة تكرار البرنامج الأساسية
        print(' ' * indent, end='')
        print('********')
        time.sleep(0.1) # توقف لعُشر ثانية

ثم وضعنا بقية البرنامج داخل عبارة try؛ فحينما يضغط المستخدم على Ctrl+C أثناء تشغيل برنامج بايثون فستطلق الاستثناء KeyboardInterrupt، وإذا لم تكن هنالك عبارة try-except لمعالجة الاستثناء فسينهار البرنامج وتظهر رسالة خطأ قبيحة. لتفادي ذلك سنعالج الاستثناء KeyboardInterrupt بأنفسنا باستدعاء الدالة sys.exit()‎ (هذه الشيفرة موجودة بعد نهاية كتلة try).

حلقة التكرار اللانهائية while True: ستكرر التعليمات الموجودة في برنامجنا للأبد، واستخدمنا التعبير ‎' ' * indent لطباعة العدد الصحيح من المسافات البادئة، لكننا لا نريد أن ننتقل إلى سطر جديد بعد تلك الفراغات فنمرر الوسيط end=''‎ إلى الدالة print()‎. الاستدعاء الثاني للدالة print()‎ سيطبع لنا 8 نجوم.

لم نشرح الدالة time.sleep()‎ بعد، لكن يكفي القول أنها توقف تشغيل البرنامج مؤقتًا لعُشر ثانية 0.1.

        if indentIncreasing:
            # زيادة المسافة البادئة
            indent = indent + 1
            if indent == 20:
                # تغيير الاتجاه
                indentIncreasing = False

ثم سنعدل مقدار المسافات البادئة للمرة القادمة التي تنفذ فيها حلقة while. فإذا كان indentIncreasing هو True فسنضيف واحد إلى indent. لكن حينما تصل المسافة البادئة إلى 20 فنرغب بتقليل المسافة البادئة:

        else:
            # إنقاص عدد الفراغات
            indent = indent - 1
            if indent == 0:
                # تغيير الاتجاه
                indentIncreasing = True

أما إذا كانت indentIncreasing هي False فسننقص واحد من المتغير indent، وحينما تصل قيمته إلى 0 فسنحتاج إلى زيادة المسافات البادئة مجددًا، وفي كلتا الحالتين سيعود تنفيذ البرنامج إلى بداية حلقة التكرار لطباعة النجوم مجددًا.

except KeyboardInterrupt:
    sys.exit()

إذا ضغط المستخدم على Ctrl+C في أي مرحلة من مراحل تنفيذ حلقة التكرار الموجودة داخل كتلة try فسيطلق الاستثناء KeyboardInterrrupt ثم يعالج في عبارة except، سيستمر تنفيذ البرنامج داخل كتلة expect الذي سيشغل الدالة sys.exit()‎ لإنهاء البرنامج. وعلى الرغم من أن حلقة التكرار لانهائية، لكننا نوفر طريقة آمنة لإنهاء تشغيل التطبيق من المستخدم.

الخلاصة

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

تعلمنا استخدام العبارتين try و except التي تتولى معالجة الاستثناءات، فكان حدوث أي خطأ في البرنامج سيؤدي إلى انهياره كما رأينا في المقالات السابقة، لكن بتعلمنا لمعالجة الاستثناءات أصبح بإمكاننا بناء تطبيقات تتعامل مع الأخطاء الشائعة دون مشاكل.

والآن بعد أن تعلمت الدوال في هذا المقال، ما رأيك أن تجرب ما تعلمته في التطبيق العملي الموالي؟ وطبعًا، لا تنسَ مشاركتنا نتائج تطبيقك في التعليقات:

اكتب دالةً باسم collatz()‎ التي تقبل معاملًا واحدًا اسمه number، إذا كان number زوجيًا فستطبع الدالة number // 2 ثم تعيد تلك القيمة، وإذا كان العدد number فرديًا فستطبع وتعيد ناتج 3 * number + 1.

ثم اكتب برنامجًا يسمح للمستخدم بإدخال رقم صحيح واستمر باستدعاء الدالة collatz()‎ على ذاك الرقم حتى تعيد الدالة القيمة 1. (ستعمل هذه الدالة لجميع الأعداد الصحيحة، وستكون النتيجة دومًا 1! لا يعرف علماء الرياضيات تحديدًا لماذا، لكن برنامجك هو تطبيق عملي على معضلة كولاتز، حتى أن بعضهم يطلق عليها «أبسط معضلة رياضية مستحيلة»).

تذكر أن تحول القيمة المعادة من input()‎ إلى رقم صحيح عبر الدالة int()‎، وإلا فستعدها بايثون على أنها سلسلة نصية.

تلميحة: يكون العدد number زوجيًا إذا كان باقي القسمة على 2 هو 0 أي number % 2 == 0 وفرديًا إذا كان number % 2 == 1. يجب أن يبدو شكل تنفيذ البرنامج كما يلي:

Enter number:
3
10
5
16
8
4
2
1

ترجمة -وبتصرف- للفصل Functions من كتاب Automate the boring stuff with Python لصاحبه Al Sweigart.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...