الفصل 20

وسائط الدوال — الأسماء والقيم الافتراضية والنجوم

تمرير الوسائط بالاسم، القيم الافتراضية وقواعد ترتيبها، *args و **kwargs، وفك حزم القوائم أو القواميس عند استدعاء الدوال.

32 دقيقةPython 3.12
  1. 1المشكلة
  2. 2الفهم
  3. 3أمثلة محلولة
  4. 4التوقع
  5. 5التطبيق
  6. 6التحدي

المشكلة التي نقوم بحلها

كانت الدوال في الدرس السابق تُستدعى بطريقة واحدة فقط: تمرير عدد من الوسائط مساوٍ تماماً لعدد المعاملات، وبنفس الترتيب الدقيق.

لكن في الواقع العملي، هذا لا يكفي أبداً. تأمل دالة مخصصة لطلبات الشراء:

python
def order(item, quantity):
    return f"{quantity} x {item}"


print(order("pen", 3))
print(order(3, "pen"))
text
3 x pen
pen x 3

السطر الثاني لم يطلق أي خطأ — بل أعطى ببساطة نتيجة لا معنى لها! فعندما يكون المعاملان من نفس نوع البيانات (أو متوافقين)، لا تملك بايثون أي وسيلة لمعرفة أن الترتيب قد عُكس عن طريق الخطأ.

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

يتناول هذا الفصل الطرق الأخرى لتمرير الوسائط: بالاسم، وبالقيم الافتراضية، وبأعداد غير محددة ومفتوحة.

في نهاية هذا الدرس ستكون قادراً على

  • تمرير الوسائط بالاسم (Keyword Arguments)، ومعرفة متى ينبغي لك فعل ذلك
  • تعيين قيم افتراضية للمعاملات والالتزام بقواعد ترتيبها
  • استقبال أي عدد من الوسائط الموضعية باستخدام *args
  • استقبال وسائط بأي اسم وبأي عدد باستخدام **kwargs
  • فك حزم القوائم أو القواميس وتمريرها في استدعاء الدوال

المتطلبات السابقة: الدوال — قرار واحد باسم واحد.


التمرير بالاسم (Keyword Arguments)

بدلاً من إجهاد نفسك في تذكر ترتيب المعاملات، اكتب اسم المعامل صراحة:

python
def order(item, quantity):
    return f"{quantity} x {item}"


print(order(quantity=3, item="pen"))
text
3 x pen

مع استخدام الأسماء، يفقد الترتيب أهميته — فقد كُتب quantity أولاً ومع ذلك استقر في مكانه الصحيح تماماً.

متى ينبغي لك استخدام التمرير بالاسم؟ عندما لا يشرح الوسيط نفسه بنفسه. فكتابة order("pen", 3) واضحة ومفهومة. لكن كتابة send(True, False) لا تخبرك بأي شيء عن معنى هاتين القيمتين، في حين أن send(retry=True, verbose=False) توضح المعنى جلياً. كقاعدة عامة: اذكر الأسماء دائماً عند تمرير قيم منطقية True/False أو أرقام مجردة غير واضحة الدلالة.

القيم الافتراضية (Default Values)

python
def order(item, quantity=1):
    return f"{quantity} x {item}"


print(order("pen"))
print(order("pen", 5))
print(order("pen", quantity=5))
text
1 x pen
5 x pen
5 x pen

تخبر العبارة quantity=1 بايثون: "افترضي أن الكمية تساوي واحداً إذا لم يحدد المتصل خلاف ذلك". وهكذا تصبح الحالة الشائعة والأكثر تكراراً قصيرة في الكتابة، مع بقاء القدرة على تخصيص الحالات النادرة متاحة دائماً.

هناك قاعدة ذهبية صارمة يجب الالتزام بها: المعاملات ذات القيم الافتراضية تأتي دائماً في النهاية.

text
def order(quantity=1, item):
    return item
text
SyntaxError: parameter without a default follows parameter with a default

والسبب منطقي وبديهي بمجرد التفكير فيه: فلو كتبنا order("pen")، كيف ستعرف بايثون لأي معامل كُتبت القيمة "pen"؟ بوضع المعاملات الإجبارية أولاً، يزول أي لبس تماماً.

أي عدد من الوسائط — المعامل *args

python
def total(*prices):
    return sum(prices)


print(total(15, 60))
print(total(15, 60, 850))
print(total())
text
75
925
0

تخبر النجمة في *prices بايثون: "اجمعي كل الوسائط الموضعية المتبقية هنا معاً". فلا حاجة لتحديد عدد العناصر مسبقاً، وتمرير صفر من العناصر أمر مقبول تماماً.

وما هو نوع الكائن الذي يجمع هذه الوسائط؟

python
def show(*things):
    print(type(things))
    print(things)


show(1, "two")
text
<class 'tuple'>
(1, 'two')

إنه صف (tuple) — نفس الصف الذي درسناه في الدرس الرابع عشر! والمنطق هنا في غاية البراعة: فعدد الوسائط الممررة يتحدد ويستقر في لحظة الاستدعاء ولا ينبغي تغييره بعد ذلك، لذا فإن الوعاء غير القابل للتعديل (immutable) هو الأنسب تماماً لهذه المهمة.

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

وسائط بأي اسم — المعامل **kwargs

python
def describe(**fields):
    print(type(fields))
    for key, value in fields.items():
        print(f"{key}: {value}")


describe(item="pen", quantity=3)
text
<class 'dict'>
item: pen
quantity: 3

تجمع النجمتان جميع الوسائط التي تم تمريرها بالاسم، وتكون النتيجة قاموساً (dict) — أزواجاً من الأسماء والقيم، وهو بالضبط الشكل المنطقي المتوقع.

نجمة واحدة تعطيك صَفاً (tuple)، ونجمتان تعطيك قاموساً (dictionary). احفظ هذا الاقتران وسيتضح لك كل ما تبقى بسهولة.

الترتيب الصارم للمعاملات

عند كتابة جميع الأنواع معاً في تعريف دالة واحدة، يكون الترتيب الإلزامي هو: *المعاملات العادية أولاً، ثم `args، ثم المعاملات ذات القيم الافتراضية، ثم kwargs` في النهاية.

text
def f(a, *args, b=1, **kwargs):

قد يبدو هذا الترتيب متشدداً للوهلة الأولى، لكن سببه بسيط: يجب أن تعرف بايثون بوضوح أين يستقر كل وسيط، وبما أن *args تلتهم "كل ما تبقى من الوسائط الموضعية"، فلا يمكن لأي معامل موضعي عادي أن يأتي بعدها.

فك الحزم أثناء الاستدعاء

يمكن استخدام النجوم عند استدعاء الدالة أيضاً، حيث تؤدي حينها المهمة المعاكسة تماماً:

python
def order(item, quantity):
    return f"{quantity} x {item}"


values = ["pen", 3]
fields = {"item": "bag", "quantity": 2}

print(order(*values))
print(order(**fields))
text
3 x pen
2 x bag

تقوم *values بفك حزمة القائمة — وهو تماماً مثل كتابة order(values[0], values[1]). وتقوم **fields بفك حزمة القاموس، ويجب أن تتطابق مفاتيحه مع أسماء معاملات الدالة بدقة.

النجمة في تعريف الدالة تعني التجميع (gather)؛ والنجمة في استدعاء الدالة تعني فك الحزم (unpack) — نفس الرمز يؤدي وظيفتين متعاكستين حسب موقعه.


مثال متكامل

invoice.py:

python
# Arguments, in the order Python expects them
def invoice(customer, *items, currency="unit", discount=0.0, **extra):
    total = sum(price for _, price in items)
    after = total * (1 - discount)

    lines = [f"Invoice for {customer}"]
    for name, price in items:
        lines.append(f"  {name:<10} {price:>8.2f} {currency}")
    lines.append(f"  {'subtotal':<10} {total:>8.2f} {currency}")
    if discount:
        lines.append(f"  {'discount':<10} {discount * 100:>7.0f}%")
    lines.append(f"  {'total':<10} {after:>8.2f} {currency}")
    for key, value in extra.items():
        lines.append(f"  note {key}: {value}")
    return "\n".join(lines)


print(invoice("rafi", ("pen", 15.0), ("bag", 850.0)))
print()
print(invoice("dia", ("ink", 120.0), discount=0.1, reference="A-77"))
text
Invoice for rafi
  pen           15.00 unit
  bag          850.00 unit
  subtotal     865.00 unit
  total        865.00 unit

Invoice for dia
  ink          120.00 unit
  subtotal     120.00 unit
  discount        10%
  total        108.00 unit
  note reference: A-77

أربع نقاط تستحق الانتباه:

المعامل customer إجباري وكل ما عداه اختياري. لا معنى لفاتورة بدون اسم عميل، لذا فهو معامل عادي. وقد لا تحتوي الفاتورة على بنود بعد، لذا استُخدم *items.

القيمة discount=0.0 ليست مجرد قيمة افتراضية، بل تُبسط الشرط أيضاً. السطر if discount: يستفيد من قاعدة الدرس الثامن — فالقيمة 0.0 تُعامل كقيمة خاطئة (False)، وبالتالي لن يُطبع سطر الخصم إذا لم يكن هناك خصم.

المعامل `extra يستوعب ما لا تعرفه الدالة مسبقاً.** الوسيط reference="A-77" لا يطابق أي معامل رسمي، لذا استقر داخل extra` وطُبع كملاحظة ملحقة. وهذه هي الطريقة التي تتيح للدوال استقبال حقول ومعلومات إضافية لم يفكر فيها مصمم الدالة مسبقاً.

العبارة for _, price in items هي فك الحزم من الدرس الرابع عشر، حيث تشير الشرطة السفلية _ إلى أن اسم العنصر الأول غير مطلوب هنا. المتغير items هو صف من الصفوف، ويتم فك حزمة كل صف داخلي بسلاسة.


حالات الخطأ الشائعة

SyntaxError: parameter without a default follows parameter with a default وضع معامل ذي قيمة افتراضية قبل معامل إجباري. ضع دائماً المعاملات الإجبارية أولاً، وتليها المعاملات ذات القيم الافتراضية.

TypeError: order() takes 2 positional arguments but 3 were given تمرير وسائط أكثر مما تستقبله الدالة. توضح الرسالة عدد الوسائط المتوقعة والعدد الفعلي الممرر.

TypeError: order() got an unexpected keyword argument 'qty' تم تمرير وسيط بالاسم، لكن لا يوجد معامل بهذا الاسم في الدالة. تأكد من صحة كتابة الحروف — أو لاحظ أن الدالة لو كانت تحتوي على **kwargs لما اعترضت، بل كانت ستبتلعه بهدوء.

TypeError: order() got multiple values for argument 'item' تمرير نفس المعامل مرتين: مرة كموقع ومرة بالاسم — مثل order("pen", item="bag").

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

*فك الحزم باستخدام ` مع عدم تطابق عدد العناصر** عند كتابة order(*values)، يجب أن تحتوي values على عدد من العناصر مساوٍ تماماً لعدد معاملات الدالة. تحقق من ذلك بواسطة print(len(values))`.