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

كتابة دوال فعالة في بايثون


محمد الخضور

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

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

أسماء الدوال

تتبع أسماء الدوال عمومًا الاصطلاحات نفسها المُستخدمة في تسمية المعرفات بأنواعها، إلا أن اسم الدالة يتضمن عادةً صيغةً فعليةً، إذ أنها تُنفذ أمرًا ما غالبًا، كما يمكن تضمين صيغة اسمية لتوصيف ما تفعله الدالة؛ فعلى سبيل المثال، تدل أسماء دوال مثل ()refreshConnection على تحديث الإتصال و ()setPassword على تعيين كلمة مرور و ()extract_version على استخراج رقم الإصدار، ويوضّح كل منها ما تفعله الدالة ولماذا تفعله.

قد لا نحتاج لصيغة اسمية ضمن أسماء التوابع المُنشأة على أنها جزء من صنف أو وحدة ما، مثل تابع باسم ()reset من ضمن صنف باسم SatelliteConnection أو الدالة ()open من الوحدة webbrowser، إذ يوفر في هذه الحالة اسم الصنف أو الوحدة المعلومات اللازمة لفهم سياق عمل الدالة أو التابع؛ فمن الواضح في المثال السابق أن التابع ()reset يعيد تعيين الاتصال عبر الأقمار الصناعية satellite connection وأن الدالة ()open تفتح متصفح الويب.

يُفضّل استخدام أسماء طويلة تؤدي المعنى على أسماء مختصرة وقصيرة جدًا، إذ سيفهم المختص بالرياضيات مباشرةً أن دالةً باسم ()gcd ستعيد القاسم المشترك الأكبر لعددين، أما بالنسبة لغير المختصين، فسيجدون الاسم ()getGreatestCommonDenominator أوضح.

وتذكّر ألا تستخدم أي من أسماء الدوال أو الوحدات المبنية مسبقًا في بايثون لتسمية دوالك، مثل:

  • all
  • any
  • date
  • email
  • file
  • format
  • hash
  • id
  • input
  • list
  • min
  • max
  • object
  • open
  • random
  • set
  • str
  • sum
  • test
  • type

مفاضلات أحجام الدوال

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

لنتعرف بدايةً على فوائد الدوال الصغيرة:

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

تنطوي الدوال القصيرة أيضًا على بعض العيوب ومنها:

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

يتمسّك البعض بالمبدأ التوجيهي القائل: "الأقصر أفضل" بمبالغة، مفترضين أنه على كل دالة أن تكون بحدود ثلاثة أو أربعة أسطر برمجية على الأكثر، وهذا منافٍ للمنطق، فعلى سبيل المثال، لنأخذ دالة قراءة انتقالات اللاعب ()getPlayerMove المستخدمة في بناء لعبة برج هانوي لاحقًا، ولا يهمنا الآن فهم كيفية عمل هذه الشيفرات، وإنما الهدف منها فقط الاطلاع على الهيكلية العامة لهذه الدالة:

def getPlayerMove(towers):
    """Asks the player for a move. Returns (fromTower, toTower)."""

    while True:  # الاستمرار بالطلب من المستخدم حتى إدخال انتقال صحيح
        print('Enter the letters of "from" and "to" towers, or QUIT.')
        print("(e.g. AB to moves a disk from tower A to tower B.)")
        print()
        response = input("> ").upper().strip()

        if response == "QUIT":
            print("Thanks for playing!")
            sys.exit()

        # التأكّد من أنّ المستخدم قد أدخل أحرفًا صحيحة لاسم للبرج
        if response not in ("AB", "AC", "BA", "BC", "CA", "CB"):
            print("Enter one of AB, AC, BA, BC, CA, or CB.")
            continue  # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا

        # استخدام أسماء ذات توصيفية أعلى
        fromTower, toTower = response[0], response[1]

        if len(towers[fromTower]) == 0:
            # لا يمكن أن يكون فارغًا "from" برج البداية
            print("You selected a tower with no disks.")
            continue  # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا
        elif len(towers[toTower]) == 0:
            # فارغ "to" يمكن نقل أي قرص إلى برج وجهة
            return fromTower, toTower
        elif towers[toTower][-1] < towers[fromTower][-1]:
            print("Can't put larger disks on top of smaller ones.")
            continue  # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا
        else:
            # الانتقال صحيح، لذا سنعيد اسم برج البداية وبرج الوجهة المُحددان
            return fromTower, toTower

تمتد الدالة السابقة على 34 سطر برمجي، ورغم أنها تغطي عدّة مهام تتضمن السماح للّاعب بإجراء انتقال جديد والتحقق من صحة هذا الانتقال والطلب منه اختيار انتقال آخر في حال كون السابق غير مسموح، إلا أن كل من المهام السابقة تندرج تحت بند قراءة انتقالات اللاعب، وفي حال كنا مصممين على كتابة دوال قصيرة، فمن الممكن تجزئة شيفرات الدالة ()getPlayerMove إلى دوال أصغر، على النحو التالي:

def getPlayerMove(towers):
    """Asks the player for a move. Returns (fromTower, toTower)."""

    while True:  # الاستمرار بالطلب من المستخدم حتى إدخال انتقال صحيح
        response = askForPlayerMove()
        terminateIfResponseIsQuit(response)
        if not isValidTowerLetters(response):
            continue # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا

        # استخدام أسماء ذات توصيفية أعلى
        fromTower, toTower = response[0], response[1]

        if towerWithNoDisksSelected(towers, fromTower):
            continue  # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا
        elif len(towers[toTower]) == 0:
            # فارغ "to" يمكن نقل أي قرص إلى برج وجهة
            return fromTower, toTower
        elif largerDiskIsOnSmallerDisk(towers, fromTower, toTower):
            continue  # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا
        else:
            # الانتقال صحيح، لذا سنعيد اسم برج البداية وبرج الوجهة المُحددان
            return fromTower, toTower

def askForPlayerMove():
    """Prompt the player, and return which towers they select."""
    print('Enter the letters of "from" and "to" towers, or QUIT.')
    print("(e.g. AB to moves a disk from tower A to tower B.)")
    print()
    return input("> ").upper().strip()

def terminateIfResponseIsQuit(response):
    """Terminate the program if response is 'QUIT'"""
    if response == "QUIT":
        print("Thanks for playing!")
        sys.exit()

def isValidTowerLetters(towerLetters):
    """Return True if `towerLetters` is valid."""
    if towerLetters not in ("AB", "AC", "BA", "BC", "CA", "CB"):
        print("Enter one of AB, AC, BA, BC, CA, or CB.")
        return False
    return True

def towerWithNoDisksSelected(towers, selectedTower):
    """Return True if `selectedTower` has no disks."""
    if len(towers[selectedTower]) == 0:
        print("You selected a tower with no disks.")
        return True
    return False

def largerDiskIsOnSmallerDisk(towers, fromTower, toTower):
    """Return True if a larger disk would move on a smaller disk."""
    if towers[toTower][-1] < towers[fromTower][-1]:
        print("Can't put larger disks on top of smaller ones.")
        return True
    return False

تمتد الدوال الستة السابقة على 56 سطر، أي بحدود ضعف ما كانت عليه شيفرة الدالة الأصلية مع أدائها للمهام ذاتها، ورغم أن كل دالة من الدوال الستة أسهل للفهم وحدها من فهم الدالة ()getPlayerMove الأصلية كاملةً، إلا أن تجميع هذه الدوال معًا يزيد من التعقيد، فقد يواجه قراء شيفرة كهذه صعوبةً في فهم كيفية توافق هذه الدوال مع بعضها بعضًا، كما أن الدالة ()getPlayerMove من بين الدوال الستة هي الوحيدة التي ستُستدعى في باقي أجزاء البرنامج، في حين أن الدوال الخمسة المُتبقية لن تُستدعى سوى لمرة واحدة ومن قِبل الدالة ()getPlayerMove نفسها، إلا أنّ كتلة الدوال السابقة لا تعبّر بضخامتها عن هذه الحقيقة، ناهيك عن الحاجة لابتكار أسماء وسلاسل توثيق نصية docstrings جديدة (السلسلة النصية المحصورة بين علامات اقتباس ثلاثية أسفل كل تعليمة تصريح عن دالة def) لكل دالة جديدة، والذي يؤدي إلى وجود أسماء متشابهة تسبب الارتباك، مثل الدالتين ()getPlayerMove و ()askForPlayerMove.

لا تزال الدالة ()getPlayerMove الجديدة بعد التقسيم تتجاوز ثلاث أو أربع أسطر برمجية، فلو كنا نتبع المبدأ التوجيهي "الأقصر أفضل" بحرفيته لكنا مضطرين لتقسيمها إلى المزيد من التوابع الفرعية الأصغر؛ ففي مثل هذه الحالة، قد تؤدي سياسة استخدام دوال قصيرة جدًا إلى الحصول على دوال أبسط، إلا أن التعقيد الإجمالية للبرنامج سيزداد جذريًا. وفقًا لرأي صاحب المقال: يجب ألا تتجاوز التوابع في الحالة المثالية 30 سطرًا برمجيًا، وما عدا ذلك ألا تتجاوز طبعًا حدود 200 سطر برمجي. اجعل دوالك أقصر ما يمكن ضمن حدود الممكن والمعقول وليس أكثر.

معاملات ووسطاء الدوال

تُعرف معاملات الدالة Function Parameters بأنها أسماء المتغيرات الموجودة بين قوسي الدالة ضمن تعليمة def المسؤولة عن التصريح عن الدالة، في حين أن وسطاء الدالة Function Arguments هي القيم الممررة بين قوسي الدالة عند استدعائها. وكلما زاد عدد معاملات الدالة، زادت إمكانية ضبط وتعميم شيفرتها، ولكن في الوقت نفسه زاد تعقيدها.

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

الوسطاء الافتراضية Default Arguments

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

نحدد وسيطًا افترضيًا ضمن تعليمة def بكتابته عقب اسم المعامل وإشارة مساواة. على سبيل المثال، في الدالة ()introduction التالية، للمعامل المسمى greetings القيمة Hello وهي قيمة افتراضية تُستخدم في حال عدم تحديد قيمة له لدى استدعاء الدالة:

>>> def introduction(name, greeting='Hello'):
...     print(greeting + ', ' + name)
...
>>> introduction('Alice')
Hello, Alice
>>> introduction('Hiro', 'Ohiyo gozaimasu')
Ohiyo gozaimasu, Hiro

عند استدعاء الدالة ()introduction دون تمرير قيمة للوسيط الثاني فيها، تستخدم السلسلة النصية Hello افتراضيًا. نلاحظ أن المعاملات ذات الوسطاء الافتراضية تأتي دومًا بعد تلك التي بدون وسطاء افتراضية.

يمكنك العودة إلى مقال البنى الصحيحة المؤدية إلى الأخطاء الشائعة في بايثون الذي وضحنا فيه أهمية عدم استخدام الكائنات المتغيّرة mutable objects مثل القائمة الفارغة [] أو القاموس الفارغ {} على أنها قيم افتراضية وذلك ضمن الفقرة "لا تستخدم القيم المتغيرة من أجل وسطاء افتراضية"، إذ بيّنا فيها المشكلة التي تتسبب بها هذه المنهجية وكيفية حلها.

استخدام * و ** لتمرير الوسطاء إلى الدوال

من الممكن استخدام الصيغة * أو ** (وتُلفظ نجمة star ونجمة نجمة star star على التوالي) لتمرير مجموعة من الوسطاء إلى الدوال مثل قيم منفصلة؛ إذ نستخدم الصيغة * لتمرير العناصر ضمن كائن تكراري، مثل القائمة أو الصف؛ أما الصيغة ** فتسمح بتمرير أزواج مفتاح-قيمة في كائن مفهرس بالمفاتيح mapping object مثل القاموس بمثابة وسطاء منفصلة.

يمكن على سبيل المثال تمرير عدة وسطاء إلى الدالة ()print، فتوضع مسافات فيما بينها افتراضيًا، كما هو مُبيّن في الشيفرة التالية:

>>> print('cat', 'dog', 'moose')
cat dog moose

تدعى هذه الوسطاء بالوسطاء الموضعية positional، إذ يحدد موقعها ضمن استدعاء الدالة الوسيط المُحدد لكل معامل. ولكن ماذا لو خُزّنت السلاسل النصية هذه ضمن قائمة وحاولنا تمرير القائمة كاملةً إلى الدالة؟ ستعتقد الدالة ()print أنك ترغب بطباعة السلسلة كاملةً على أنها قيمة واحدة على النحو التالي:

>>> args = ['cat', 'dog', 'moose']
>>> print(args)
['cat', 'dog', 'moose']

نلاحظ أن تمرير القائمة إلى الدالة ()print يطبع القائمة كما هي، بما في ذلك الأقواس المعقوفة وعلامات الاقتباس ومحارف الفاصلة.

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

>>> # مثال عن شيفرة أصعب للقراءة
>>> args = ['cat', 'dog', 'moose']
>>> print(args[0], args[1], args[2])
cat dog moose

توجد طريقة أسهل لتمرير هذه العناصر إلى الدالة ()print، إذ يمكن استخدام الصيغة * لتفسير العناصر ضمن قائمة أو أي نمط بيانات تكراري على أنها وسطاء موضعية منفردة ضمن الدالة. لنكتب المثال التالي في الصدفة التفاعلية:

>>> args = ['cat', 'dog', 'moose']
>>> print(*args)
cat dog moose

تتيح الصيغة * إمكانية تمرير عناصر القائمة إلى الدالة مثل قيم مفردة بغض النظر عن عدد العناصر في القائمة، كما يمكن استخدام الصيغة ** لتمرير أنماط البيانات المفهرسة بالمفاتيح، مثل القواميس على أنها وسطاء مسماة Keyword argument مستقلة، إذ تُسبق الوسطاء المسماة باسم المعامل وإشارة مساواة. على سبيل المثال، لدى الدالة ()print وسيطًا مسمى يدعى sep والذي يحدد سلسلةً نصيةً لتوضع ما بين الوسطاء التي ستُعرض، وتُعين افتراضيًا لتكون مسافةً فارغة ' '. يمكن إسناد قيمة جديدة للوسيط المسمى وذلك إما باستخدام تعليمة إسناد أو الصيغة **. لنكتب ما يلي في الصدفة التفاعلية مثلًا:

>>> print('cat', 'dog', 'moose', sep='-')
cat-dog-moose
>>> kwargsForPrint = {'sep': '-'}
>>> print('cat', 'dog', 'moose', **kwargsForPrint)
cat-dog-moose

نلاحظ أن التعليمات السابقة تعطي خرجًا متماثلًا، إذ استخدمنا في المثال سطرًا برمجيًا واحدًا لإعداد القاموس kwargsForPrint المتضمن قيمة الوسيط المسمى الخاص بالدالة ()print، ولكن قد نحتاج في الحالات الأعقد للمزيد من الشيفرات لدى إعداد قاموس بالوسطاء المسماة. تتيح الصيغة ** لنا إمكانية إنشاء قاموس مخصص بإعدادات الضبط لنمرره عند استدعاء الدالة، وهذا أمر مفيد لا سيما للدوال والتوابع ذات العدد الكبير من الوسطاء المسماة.

بذلك نجد بأنه مع استخدام صيغتي * و **، مع تعديل قائمة أو قاموس أثناء التنفيذ، يمكن تزويد الدالة عند استدعائها بعددٍ متغير من الوسطاء.

استخدام * لإنشاء دوال مرنة Variadic Functions

يمكن أيضًا استخدام الصيغة * ضمن تعليمة التصريح عن الدوال def بغية إنشاء دوال مرنة تستقبل عددًا متغيرًا من الوسطاء الموضعيين. على سبيل المثال، تعد الدالة ()print دالةً مرنة، لأننا نستطيع تمرير أي عدد نريده من السلاسل النصية إليها، مثل ('!print('Hello أو (print('My name is', name، ورغم أننا استخدمنا الصيغة * في الفقرة السابقة عند استدعاء الدوال، سنستخدمها الآن أثناء التصريح عن الدوال.

لنلقي نظرةً على المثال التالي، وفيه ننشئ دالةً باسم ()product تستقبل أي عدد من الوسطاء لتوجد جدائها:

>>> def product(*args):
...     result = 1
...     for num in args:
...         result *= num
...     return result
...
>>> product(3, 3)
9
>>> product(2, 1, 2, 3)
12

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

تتطلب معرفة التوقيت الأنسب لاستخدام الصيغة * بعض التفكير، فالبديل الآخر بغية إنشاء دالة مرنة هو استخدام معامل وحيد يقبل القائمة مثل قيمة مُمرَّرة، أو أي نمط بيانات تكراري آخر، ليتضمن عددًا متغيرًا من العناصر، وهو المبدأ الذي تستخدمه الدالة ()sum المبنية مُسيقًا في بايثون:

>>> sum([2, 1, 2, 3])
8

تتوقع الدالة ()sum تمرير وسيط واحد تكراري إليها، فتمرير عدة وسطاء يؤدي إلى ظهور استثناء كما يلي:

>>> sum(2, 1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sum() takes at most 2 arguments (4 given)

في حين تقبل الدالتين ()min و ()max المبنيتين مُسبقًا في بايثون، واللتان توجدان القيمة الأصغر والقيمة الأكبر على التوالي من بين مجموعة قيم، تمرير وسيط تكراري وحيد إليها أو عدة وسطاء منفردة على النحو التالي:

>>> min([2, 1, 3, 5, 8])
1
>>> min(2, 1, 3, 5, 8)
1
>>> max([2, 1, 3, 5, 8])
8
>>> max(2, 1, 3, 5, 8)
8

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

تعتمد آلية تصميم المعاملات على الطريقة المتوقعة لاستخدام المبرمج للشيفرة، إذ تقبل الدالة ()print عدة وسطاء لأن المبرمجين يمررون غالبًا سلسلةً من السلاسل النصية أو المتغيرات المتضمنة لسلاسل نصية إليها، مثل (print('My name is', name. يُعد تجميع هذه السلاسل النصية ضمن قائمة ما يتطلب تنفيذ عدة خطوات قبل تمريرها إلى الدالة ()print أمرًا غير شائع، وفي حال تمرير قائمة إلى الدالة ()print، ستُطبع كاملةً بأقواسها المعقوفة ومحارف الفاصلة ما بين العناصر، وبالتالي لا يمكن استخدامها لطباعة قيم قائمة منفردةً.

أما بالنسبة للدالة ()sum، فما من سبب لاستدعائها مع تمرير وسطاء منفصلة طالما أنه من الممكن في بايثون استخدام عامل الجمع + لهذا الغرض، إذ يمكن ببساطة كتابة شيفرة مثل 8+4+2، وبالتالي ما من ضرورة لإتاحة إمكانية كتابة شيفرة على النحو (2,4,8)sum، ما يفسر سبب وجوب تمرير العدد المتغير من الوسطاء إلى الدالة ()sum على هيئة قائمة.

تتيح الدالتان ()min و ()max استخدام كلا الأليتين؛ فإذا مرر المبرمج إلى أي منهما وسيطًا واحدًا، تفترض الدالة بأن هذا الوسيط هو قائمة أو صف من القيم لتعمل على تقييمها؛ أما إذا مرر إليها عدّة وسطاء، فتفترض أن هذه القيم تمثل ما ستقيّمه، فكلا الدالتين قادرتين على التعامل مع القوائم أثناء تنفيذ البرنامج، كما في الاستدعاء (min(allExpenses، كما أن لديهما القدرة على التعامل مع الوسطاء المنفصلة التي يختارها المبرمج أثناء كتابة شيفراته، كما في الاستدعاء (max(0, someNumber. وبالتالي فإن هاتين الدالتين مصممتين للتعامل مع كلا نوعي الوسطاء. توضح الدالة ()myMinFunction التالية الفكرة، والتي تؤدي نفس وظيفة الدالة ()min:

def myMinFunction(*args):
    if len(args) == 1:
    1 values = args[0]
    else:
    2 values = args

    if len(values) == 0:
    3 raise ValueError('myMinFunction() args is an empty sequence')

 4 for i, value in enumerate(values):
        if i == 0 or value < smallestValue:
            smallestValue = value
    return smallestValue

تستخدم الدالة ()myMinFunction الصيغة * لتستقبل أعدادًا مختلفة من الوسطاء على هيئة صف، فإذا احتوى هذا الصف على قيمة وحيدة، نفترض أنها سلسلة من القيم لتُقيّم كما هو مبين في السطر رقم 1 من الشيفرة السابقة، وفيما عدا ذلك، نفترض أن المعامل arg هو صف من القيم لتُقيّم كما هو مبين في السطر رقم 2 من الشيفرة السابقة.ففي كلتا الحالتين ستتضمن المتغيرات المخصصة لاستقبال القيم على سلسلة من القيم لتعمل باقي أجزاء الشيفرة على تقييمها.

كما هو الحال في الدالة ()min الفعلية المبنية مسبقًا في بايثون، عملنا على إظهار خطأ قيمة ValueError في حال استدعاء الدالة دون تمرير أي قيمة أو تمرير سلسلة فارغة إليها، وذلك في السطر رقم 3، أما باقي الشيفرة فتمر على القيم المُمرَّرة لتعيد القيمة الأصغر بينها في السطر رقم 4. بغية تبسيط الدالة ()myMinFunction السابقة، جعلناها تتعامل فقط مع القوائم والصفوف من بين القيم التكرارية الممكنة.

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

استخدام ** لإنشاء دوال مرنة

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

لنأخذ دالة مفترضة باسم ()formMolecule لتشكيل مركب كيميائي والتي تمتلك معامل لكل عنصر من العناصر الكيميائية المعروفة البالغ عددها 118:

>>> def formMolecule(hydrogen, helium, lithium, beryllium, boron, --snip--

سيكون تمرير القيمة 2 لمعامل عدد ذرات الهيدروجين hydrogen والقيمة 1 لمعامل عدد ذرات الأكسجين oxygen للحصول على جزيء الماء water عمليةً شاقةً وصعبة القراءة، إذ أننا سنضطر إلى إسناد القيمة 0 إلى كل معاملات العناصر غير المطلوبة، على النحو التالي:

>>> formMolecule(2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 --snip--
'water'

يمكن جعل الدالة أكثر تنظيمًا باستخدام معاملات مسماة مع وسيط افتراضي لكل منها، ما يجنبنا عبء تمرير وسيط إلى كل معامل عند استدعاء الدالة.

ملاحظة: رغم التعريف الواضح لكلا مصطلحي الوسيط والمعامل، إلا أن المبرمجين عادةً ما يستخدمون مصطلحي الوسيط المسمى keyword argument والمعامل المسمى keyword parameter تبادليًا.

على سبيل المثال، عينا في تعليمة التصريح عن الدالة التالية وسيطًا افتراضيًا يساوي القيمة 0 لكل من المعاملات المسماة، على النحو التالي:

>>> def formMolecule(hydrogen=0, helium=0, lithium=0, beryllium=0, --snip--

وهذا ما يجعل من استدعاء الدالة ()formMolecule أسهل، لأننا لن نضطر إلى تحديد وسطاء سوى للمعاملات ذات القيم المختلفة عن قيمة الوسيط الافتراضي، كما من الممكن أيضًا تحديد الوسطاء المسماة بأي ترتيب، كما في المثال التالي:

>>> formMolecule(hydrogen=2, oxygen=1)
'water'
>>> formMolecule(oxygen=1, hydrogen=2)
'water'
>>> formMolecule(carbon=8, hydrogen=10, nitrogen=4, oxygen=2)
'caffeine'

مع ذلك، تبقى عبارة التصريح السابقة صعبة وغير عملية بوجود 118 اسم معامل، وماذا لو جرى اكتشاف عناصر كيميائية جديدة؟ سنضطر إلى تحديث تعليمة def مع كامل توثيقات معاملات الدالة.

يمكن الاعتماد على حل بديل من خلال تجميع كافة المعاملات مع وسطائها على هيئة أزواج مفتاح-قيمة ضمن قاموس، وذلك باستخدام الصيغة ** للوسطاء المسماة، ويمكن -من الناحية التقنية- تسمية المعامل المسبوق بالصيغة ** بأي اسم، إلا أن العادة جرت على تسميته kwargs، على النحو التالي:

>>> def formMolecules(**kwargs):
...     if len(kwargs) == 2 and kwargs['hydrogen'] == 2 and 
kwargs['oxygen'] == 1:
...         return 'water'
...     # (هنا مكان بقية شيفرات الدالة)
...
>>> formMolecules(hydrogen=2, oxygen=1)
'water'

تشير الصيغة ** إلى أن المعامل kwargs قادر على التعامل مع كافة الوسطاء المسماة الممررة عند استدعاء الدالة، إذ ستُخزن على هيئة أزواج مفتاح-قيمة ضمن قاموس مخصص للمعامل kwargs. الآن، في حال اكتشاف عناصر كيميائية جديدة، كل ما علينا فعله هو تحديث شيرة الدالة وليس عبارة التصريح عنها، ذلك لأن كافة الوسطاء المسماة موضوعة ضمن المعامل kwarg:

1 >>> def formMolecules(**kwargs):
2 ...     if len(kwargs) == 1 and kwargs.get('unobtanium') == 12:
...         return 'aether'
...     # (هنا مكان بقية شيفرات الدالة)
...
>>> formMolecules(unobtanium=12)
'aether'

نلاحظ من السطر رقم 1 في الشيفرة السابقة أن تعليمة def بقيت كما في الحالة السابقة، وأننا لم نضطر إلى تعديل سوى شيفرة الدالة فقط المشار إليها بالسطر رقم 2، فمع استخدام الصيغة **، تغدو كتابة كل من تعليمة التصريح عن الدالة واستدعائها أبسط مع الحفاظ على سهولة قراءة الشيفرة وفهمها.

استخدام * و ** لإنشاء دوال مغلفة Wrapper Functions

لعل إحدى الاستخدامات الشائعة لصيغتي * و ** ضمن تعليمة def هي إنشاء الدوال المغلّفة، والتي تُمرر الوسطاء إلى دالة أخرى لتعيد القيمة المعادة الخاصة بتلك الدالة، إذ يمكن استخدام صيغتي * و ** لتمرير أي من وسطاء دالة ما أو جميعها إلى الدالة المغلّفة. على سبيل المثال، يمكن إنشاء دالة باسم ()printLowercase لتكون مغلّفة لدالة ()print المبنية مسبقًا، إذ أنها تعتمد على الدالة ()print في أداء مهمتها الحقيقية في الطباعة، إلا أنها قبل ذلك تحوّل أحرف وسطاء السلاسل النصية إلى حالة الأحرف الصغيرة، على النحو التالي:

1 >>> def printLower(*args, **kwargs):
2 ...     args = list(args)
...     for i, value in enumerate(args):
...         args[i] = str(value).lower()
3 ...     return print(*args, **kwargs)
...
>>> name = 'Albert'
>>> printLower('Hello,', name)
hello, albert
>>> printLower('DOG', 'CAT', 'MOOSE', sep=', ')
dog, cat, moose

تستخدم الدالة ()printLower في السطر رقم 1 من الشيفرة السابقة الصيغة * لتتعامل مع أعداد مختلفة من الوسطاء الموضعيين الموجودة ضمن صف مُسند إلى المعامل args، في حين أن الصيغة ** تعمل على إسناد أي وسطاء مسماة إلى قاموس ضمن المعامل kwargs. إذا استخدمت دالة ما كلا المعاملين args* و kwargs** معًا، فيجب أن يأتي المعامل args* قبل المعامل kwargs**، إذ نمرر كلا المعاملين السابقين إلى الدالة المغلّفة ()print، ولكن قبل ذلك تُعدّل دالتنا الجديدة بعضًا من الوسطاء لتجعل الصف الموجود في المعامل args على هيئة قائمة وذلك في السطر المشار إليه بالرقم 2 من الشيفرة أعلاه.

نمرّر -بعد تغيير السلاسل النصية الموجودة في المعامل args إلى حالة الأحرف الصغيرة- كلًا من العناصر الموجودة في هذا المعامل إضافةً إلى أزواج مفتاح-قيمة الموجودة في المعامل kwargs مثل وسطاء منفصلين إلى الدالة ()print باستخدام صيغتي * و ** وذلك في السطر المشار إليه بالرقم 3، كما تُعاد القيمة المعادة من الدالة ()print على أنها قيمة الدالة ()printLower المعادة، وهذه الخطوات كفيلة بتغليف الدالة ()print.

الخلاصة

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

ترجمة -وبتصرف- للجزء الأول من الفصل العاشر "كتابة دوال فعالة في بايثون" من كتاب Beyond the Basic 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.


×
×
  • أضف...