وسائط الدوال — الأسماء والقيم الافتراضية والنجوم
تمرير الوسائط بالاسم، القيم الافتراضية وقواعد ترتيبها، *args و **kwargs، وفك حزم القوائم أو القواميس عند استدعاء الدوال.
كيف تريد أن تتعلم؟
المشكلة التي نقوم بحلها
كانت الدوال في الفصل السابق تُستدعى بطريقة واحدة فقط: تمرير عدد من الوسائط مساوٍ تماماً لعدد المعاملات، وبنفس الترتيب الدقيق.
لكن في الواقع العملي، هذا لا يكفي أبداً. تأمل دالة مخصصة لطلبات الشراء:
def order(item, quantity):
return f"{quantity} x {item}"
print(order("pen", 3))
print(order(3, "pen"))3 x pen
pen x 3السطر الثاني لم يطلق أي خطأ — بل أعطى نتيجة لا معنى لها. بايثون لا تتحقق من أنواع الوسائط: هنا تبادل نص (string) ورقم مكانيهما، ولأن f-string تستطيع طباعة الاثنين، لم يوقف البرنامجَ شيء. لا تملك بايثون أي وسيلة لمعرفة أن الترتيب قد عُكس.
وماذا لو كانت معظم الطلبات تقريباً تخص عنصراً واحداً فقط؟ ستضطر لكتابة الرقم 1 في كل مرة. وإذا كانت الدالة تحتوي على أربعة خيارات إضافية اختيارية؟ ستضطر لتمرير الخيارات الأربعة جميعاً في كل استدعاء، بما في ذلك الخيارات التي لا تريد تغييرها أصلاً.
يتناول هذا الفصل الطرق الأخرى لتمرير الوسائط: بالاسم، وبالقيم الافتراضية، وبأعداد غير محددة ومفتوحة.
في نهاية هذا الفصل ستكون قادراً على
- تمرير الوسائط بالاسم (Keyword Arguments)، ومعرفة متى ينبغي لك فعل ذلك
- تعيين قيم افتراضية للمعاملات والالتزام بقواعد ترتيبها
- جعل بعض المعاملات لا تُمرَّر إلا بالاسم
- استقبال أي عدد من الوسائط الموضعية باستخدام
*args - استقبال وسائط بأي اسم وبأي عدد باستخدام
**kwargs - فك حزم القوائم أو القواميس وتمريرها في استدعاء الدوال
المتطلبات السابقة: الدوال — قرار واحد باسم واحد.
التمرير بالاسم (Keyword Arguments)
بدلاً من إجهاد نفسك في تذكر ترتيب المعاملات، اكتب اسم المعامل صراحة:
def order(item, quantity):
return f"{quantity} x {item}"
print(order(quantity=3, item="pen"))3 x penمع استخدام الأسماء، يفقد الترتيب أهميته — فقد كُتب quantity أولاً ومع ذلك استقر في مكانه الصحيح تماماً.
متى ينبغي لك استخدام التمرير بالاسم؟ عندما لا يشرح الوسيط نفسه بنفسه. فكتابة order("pen", 3) واضحة ومفهومة. لكن كتابة send(True, False) لا تخبرك بأي شيء عن معنى هاتين القيمتين، في حين أن send(retry=True, verbose=False) توضح المعنى جلياً. كقاعدة عامة: اذكر الأسماء دائماً عند تمرير قيم منطقية True/False أو أرقام مجردة غير واضحة الدلالة.
القيم الافتراضية (Default Values)
def order(item, quantity=1):
return f"{quantity} x {item}"
print(order("pen"))
print(order("pen", 5))
print(order("pen", quantity=5))1 x pen
5 x pen
5 x penتخبر العبارة quantity=1 بايثون: "افترضي أن الكمية تساوي واحداً إذا لم يحدد المتصل خلاف ذلك". وهكذا تصبح الحالة الشائعة والأكثر تكراراً قصيرة في الكتابة، مع بقاء القدرة على تخصيص الحالات النادرة متاحة دائماً.
هناك قاعدة ذهبية صارمة يجب الالتزام بها: المعاملات ذات القيم الافتراضية تأتي دائماً في النهاية.
def order(quantity=1, item):
return itemSyntaxError: parameter without a default follows parameter with a defaultوالسبب منطقي وبديهي بمجرد التفكير فيه: فلو كتبنا order("pen")، كيف ستعرف بايثون لأي معامل كُتبت القيمة "pen"؟ بوضع المعاملات الإجبارية أولاً، يزول أي لبس تماماً.
أي عدد من الوسائط — المعامل *args
def total(*prices):
return sum(prices)
print(total(15, 60))
print(total(15, 60, 850))
print(total())75
925
0تخبر النجمة في *prices بايثون: "اجمعي كل الوسائط الموضعية المتبقية هنا معاً". فلا حاجة لتحديد عدد العناصر مسبقاً، وتمرير صفر من العناصر أمر مقبول تماماً.
وما هو نوع الكائن الذي يجمع هذه الوسائط؟
def show(*things):
print(type(things))
print(things)
show(1, "two")<class 'tuple'>
(1, 'two')إنه صف (tuple) — نفس الصف الذي درسناه في الفصل 14. والمنطق هنا في غاية البراعة: فعدد الوسائط الممررة يتحدد ويستقر في لحظة الاستدعاء ولا ينبغي تغييره بعد ذلك، لذا فإن الوعاء غير القابل للتعديل (immutable) هو الأنسب تماماً لهذه المهمة.
الاسمpricesليس إلزامياً. الاسم الشائع اصطلاحياً في مجتمع بايثون هو*args، وهو ما ستراه في معظم الأكواد والمكتبات. لكن استخدام اسم ذي دلالة واضحة أفضل متى ما وُجد — فكتابة*pricesتخبر قارئ الكود بنوع ما يصل، بينما*argsلا تفعل ذلك.
وسائط بأي اسم — المعامل **kwargs
def describe(**fields):
print(type(fields))
for key, value in fields.items():
print(f"{key}: {value}")
describe(item="pen", quantity=3)<class 'dict'>
item: pen
quantity: 3تجمع النجمتان جميع الوسائط التي تم تمريرها بالاسم، وتكون النتيجة قاموساً (dict) — أزواجاً من الأسماء والقيم، وهو بالضبط الشكل المنطقي المتوقع.
نجمة واحدة تعطيك صَفاً (tuple)، ونجمتان تعطيك قاموساً (dictionary). احفظ هذا الاقتران وسيتضح لك كل ما تبقى بسهولة.
معاملات لا تُمرَّر إلا بالاسم
المعامل الذي يأتي بعد *args لا يمكن تمريره حسب الموقع، لأن *args تأخذ كل الوسائط الموضعية المتبقية لنفسها. لذلك لا يمكن تمريره إلا بالاسم. ويُسمى هذا معاملاً بالاسم فقط (keyword-only):
def total(*prices, discount=0.0):
return sum(prices) * (1 - discount)
print(total(100, 50))
print(total(100, 50, discount=0.1))
print(total(100, 50, 0.1))150.0
135.0
150.1انظر إلى السطر الثالث. لم يصبح 0.1 هو الخصم — بل دخل إلى prices كسعر إضافي، والجواب خاطئ دون أي خطأ. لتمرير discount يجب أن تكتب discount=0.1.
يمكنك أن تجعل معاملاً بالاسم فقط حتى لو لم تكن تريد *args: ضع * وحدها في قائمة المعاملات. كل ما يأتي بعدها يصبح بالاسم فقط:
def send(message, *, retry=False):
return f"{message} (retry={retry})"
print(send("hi", retry=True))
print(send("hi", True))hi (retry=True)
TypeError: send() takes 1 positional argument but 2 were given(قبل السطر الثاني تطبع بايثون أيضاً بعض أسطر الـ traceback؛ وهنا يظهر السطر الأخير فقط.)
هذا مفيد جداً لإعدادات True/False: من يستدعي الدالة مضطر إلى كتابة retry=True، فيفهم كل من يقرأ الاستدعاء معنى True.
الترتيب الصارم للمعاملات
عند كتابة جميع الأنواع معاً، تكون القاعدة الكاملة كما يلي:
- المعاملات العادية أولاً. ومن بينها، تأتي المعاملات التي ليس لها قيمة افتراضية قبل التي لها قيمة افتراضية.
- ثم
*args(أو*وحدها). - ثم المعاملات بالاسم فقط — بقيم افتراضية أو بدونها، وبأي ترتيب.
- وفي النهاية
**kwargs.
def f(a, b=1, *args, c, d=2, **kwargs):
print(a, b, args, c, d, kwargs)
f(10, c=3)
f(10, 20, 30, 40, c=3, e=5)10 1 () 3 2 {}
10 20 (30, 40) 3 2 {'e': 5}ليس لـ c قيمة افتراضية، ومع ذلك جاء بعد b=1 الذي له قيمة افتراضية، وهذا مسموح — لأن كل ما بعد *args يصل بالاسم، فلا يوجد أي التباس في مكان كل قيمة. قاعدة “القيم الافتراضية بعد غيرها” تنطبق فقط على الجزء الذي قبل *args.
قد يبدو هذا الترتيب متشدداً للوهلة الأولى، لكن سببه بسيط: يجب أن تعرف بايثون أين يستقر كل وسيط، وبما أن *args تأخذ “كل ما تبقى”، فلا يمكن لأي معامل موضعي أن يأتي بعدها.
فك الحزم أثناء الاستدعاء
يمكن استخدام النجوم عند استدعاء الدالة أيضاً، حيث تؤدي حينها المهمة المعاكسة تماماً:
def order(item, quantity):
return f"{quantity} x {item}"
values = ["pen", 3]
fields = {"item": "bag", "quantity": 2}
print(order(*values))
print(order(**fields))3 x pen
2 x bagتقوم *values بفك حزمة القائمة — وهو تماماً مثل كتابة order(values[0], values[1]). وتقوم **fields بفك حزمة القاموس، ويجب أن تتطابق مفاتيحه مع أسماء معاملات الدالة بدقة.
النجمة في تعريف الدالة تعني التجميع (gather)؛ والنجمة في استدعاء الدالة تعني فك الحزم (unpack) — نفس الرمز يؤدي وظيفتين متعاكستين حسب موقعه.
مثال متكامل
invoice.py:
# 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"))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: يستفيد من قاعدة الفصل 8 — فالقيمة 0.0 تُعامل كقيمة خاطئة (False)، وبالتالي لن يُطبع سطر الخصم إذا لم يكن هناك خصم. ولأن discount يأتي بعد *items فهو بالاسم فقط — يجب تمريره بصيغة discount=0.1.
المعامل `extra يستوعب ما لا تعرفه الدالة مسبقاً.** الوسيط reference="A-77" لا يطابق أي معامل رسمي، لذا استقر داخل extra` وطُبع كملاحظة ملحقة. وهذه هي الطريقة التي تتيح للدوال استقبال حقول ومعلومات إضافية لم يفكر فيها مصمم الدالة مسبقاً.
العبارة for _, price in items هي فك الحزم من الفصل 14، حيث تشير الشرطة السفلية _ إلى أن اسم العنصر الأول غير مطلوب هنا. المتغير 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))`.
الخطوة ٤ من ٦ — التوقع
تحقّق من فهمك
للمعامل quantity قيمة افتراضية. ماذا يطبع السطران؟
def order(item, quantity=1):
return f"{quantity} x {item}"
print(order("pen"))
print(order("pen", 5))- A1 x pen 5 x pen
- B1 x pen 1 x pen
- Cخطأ `TypeError`
- Dpen x 1 pen x 5
ما نوع الشيء الذي يجمعه *things؟
def show(*things):
print(type(things))
print(len(things))
show(1, "two", 3.0)- A<class 'tuple'> 3
- B<class 'tuple'> 1
- C<class 'list'> 3
- D<class 'dict'> 3
وُضع المعامل ذو القيمة الافتراضية أولاً. ماذا يحدث؟
def order(quantity=1, item):
return item- Aيعمل بشكل طبيعي
- Bيعمل، لكن استدعاء `order("pen")` يفشل
- C`SyntaxError` — لا يمكن أن يسبق معاملٌ ذو قيمة افتراضية معاملاً مطلوباً
- D`TypeError`
الإجابة تتطلب حساباً
سجّل الدخول للتحقق من إجاباتك
الأسئلة موجودة أعلاه، والتفكير في إجاباتها ذهنياً هو الجزء المهم. سجّل الدخول لرؤية الإجابات والتفسيرات والتلميحات ذات المستويات الثلاثة.
دورك الآن
اكتب ملفاً باسم message.py يحتوي على دالة واحدة بالصيغة التالية:
def notify(recipient, *lines, subject="Notice", urgent=False, **details)يجب على الدالة أن تُرجع (return) رسالة منسقة كنص بدلاً من طباعتها، تحتوي على: المستلم، والموضوع، وكل سطر من سطور الرسالة في سطر مستقل، وكل زوج من تفاصيل details كملاحظة في الأسفل. وإذا كانت قيمة urgent صحيحة (True)، فأضف تنبيهاً قبل سطر الموضوع.
ثم استدعِ الدالة بأربع طرق مختلفة:
- بتمرير المستلم فقط دون أي أسطر نصية على الإطلاق
- بتمرير ثلاثة أسطر نصية مع ترك باقي المعاملات على قيمها الافتراضية
- بتمرير
urgent=Trueوموضوع مخصصsubject، كلاهما بالاسم - بإنشاء قائمة وقاموس وفك حزمهما في الاستدعاء باستخدام
*و**
ثم اكسر البرنامج عمداً ثلاث مرات، وانظر ماذا يحدث في كل مرة:
- انقل المعامل
subject="Notice"ليكون قبلrecipient. قبل أن يعمل الملف أصلاً ستحصل علىSyntaxError: parameter without a default follows parameter with a default. - اكتب
notify("rafi", subjekt="hi")بخطأ إملائي. لن يظهر أي خطأ:**detailsتقبل الاسم الخاطئ أيضاً بهدوء، فيصبحsubjektملاحظة ويبقى الموضوع"Notice". اطبع الرسالة المُرجعة وانظر بنفسك. - اكتب
notify("rafi", recipient="dia"). ستحصل علىTypeError: notify() got multiple values for argument 'recipient'.
تلتقط بايثون خطأين من الثلاثة فوراً وتفوّت واحداً — وهذا الواحد هو الأخطر. هذا هو ثمن السهولة التي تعطيها **kwargs: لم يعد أحد يتحقق من الإملاء. تعرّف على الرسائل مرة واحدة وستلتقطها في ثوانٍ لاحقاً، وستعرف أيضاً أين يختبئ الخطأ الصامت.
الخطوة ٦ من ٦
التحدي — اختبار الفصل
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
سجّل الدخول لأداء الاختبار