الدوال في Python: def والمعاملات وreturn وlambda وتلميحات الأنواع
الدالة في Python قطعة كود لها اسم، تُعرَّف بالكلمة def ويمكن استدعاؤها بقيم مختلفة، وتعيد النتيجة بالكلمة return. إذا لم تكتب return تعيد الدالة None. تستقبل الدالة معاملات موضعية أو مسماة، ويمكن أن تكون لها قيم افتراضية، وتُكتب تلميحات الأنواع بعد المعاملات، وتفحصها أدوات مثل mypy قبل التشغيل.
الدالة في Python قطعة كود لها اسم، يمكن استدعاؤها عدة مرات بقيم مدخلة مختلفة. تُعرَّف بالكلمة def، وتستقبل القيم عبر المعاملات، وتعيد النتيجة عبر return. والدوال هي الوسيلة الرئيسية لتحويل سكريبت طويل إلى خطوات مفهومة وقابلة للفحص. وفي هذه المقالة: تعريف الدالة بـ def في بايثون، والمعاملات، وlambda، ونطاق المتغيرات، وتلميحات الأنواع.
كيف تعرّف دالة: def في بايثون
يبدأ تعريف الدالة بالكلمة def، ثم الاسم والمعاملات بين قوسين ونقطتين. ويُكتب الجسم مسافة بادئة بأربع مسافات، فالمسافة البادئة في Python ليست زينة بل جزء من الصياغة.
def greet(name):
"""تُعيد تحية للمستخدم."""
return f"مرحباً، {name}!"
print(greet("أمينة")) # مرحباً، أمينة!
السطر النصي بين علامات الاقتباس الثلاثية مباشرةً بعد العنوان هو docstring، أي توثيق الدالة. تعرضه الأمر help(greet) وتلميحات المحرر. وتُكتب أسماء الدوال حسب اصطلاح PEP 8 بحروف صغيرة مع شرطات سفلية: add_expense وget_rate.
السطر def greet(name): يُسمّى توقيع الدالة: الاسم مع قائمة المعاملات (والتلميحات إن وُجدت). ومن التوقيع يفهم القارئ كيف يستدعي الدالة دون أن ينظر في جسمها.
ماذا يفعل return في Python
تنهي return الدالة وتعيد قيمة إلى من استدعاها. الكود الذي يأتي بعد return المنفَّذة لا يعمل. وإذا لم توجد return أو كانت بلا قيمة، تعيد الدالة None.
def area(w, h):
w * h # الحساب تم ثم رُمي الناتج
print(area(3, 4)) # None
def area(w, h):
return w * h
print(area(3, 4)) # 12
نسيان return من أكثر الأخطاء شيوعاً عند المبتدئين: الدالة «تعمل»، لكنها تعيد None، ويظهر الخطأ في السطر الذي يليه. ويمكن إرجاع أكثر من قيمة، والواقع أنها مجرد صف يسهل فكّه:
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([4, 1, 9])
print(low, high) # 1 9
الإرجاع المبكر يساعد على تقليل التداخل: تعالج الحالات الخاصة أولاً، ثم تكتب المنطق الأساسي.
def discount(price, is_member):
if price <= 0:
return 0
if not is_member:
return price
return price * 0.9
معاملات الدالة في Python: الموضعية والمسماة
المعاملات هي الأسماء في تعريف الدالة، والوسائط (arguments) هي القيم التي تُمرَّر عند الاستدعاء. يمكن تمرير الوسائط بالترتيب (موضعياً) أو بالاسم. والوسائط المسماة تجعل الاستدعاء واضحاً بذاته، ولا يبقى للترتيب أهمية.
def add_expense(title, amount, category="متنوع"):
return {"title": title, "amount": amount, "category": category}
add_expense("قهوة", 250) # موضعياً
add_expense(amount=250, title="قهوة") # بالاسم
add_expense("تاكسي", 600, category="مواصلات") # مختلط
المعامل ذو القيمة الافتراضية اختياري. القواعد: المعاملات التي لها قيم افتراضية تأتي بعد المعاملات الإجبارية، وعند الاستدعاء تأتي الوسائط الموضعية قبل المسماة. وإلا يرفع Python الخطأ SyntaxError.
فخ: القيمة الافتراضية القابلة للتعديل
القيمة الافتراضية تُحسب مرة واحدة، عند تنفيذ سطر def، لا عند كل استدعاء. فإذا كانت قائمة أو قاموساً، فستكون مشتركة بين كل الاستدعاءات.
def add_item(item, acc=[]):
acc.append(item)
return acc
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — القائمة «تتذكر» الاستدعاء السابق
الحل الصحيح هو None قيمةً افتراضية، وإنشاء كائن جديد داخل الدالة:
def add_item(item, acc=None):
if acc is None:
acc = []
acc.append(item)
return acc
*args وkwargs: عدد غير محدد من الوسائط
النجمة قبل المعامل تجمع كل الوسائط الموضعية الزائدة في صف (tuple)، والنجمتان تجمعان كل الوسائط المسماة الزائدة في قاموس (dict). وأسماء args وkwargs مجرد اصطلاح، والمهم هو النجمات نفسها.
def total(*args):
return sum(args)
print(total(100, 250, 40)) # 390
def make_user(name, **kwargs):
user = {"name": name}
user.update(kwargs)
return user
print(make_user("أمينة", city="الرياض", age=30))
# {'name': 'أمينة', 'city': 'الرياض', 'age': 30}
النجمات نفسها تعمل في الاتجاه المعاكس: عند الاستدعاء تفكّ متتالية أو قاموساً إلى وسائط:
values = [3, 4]
print(pow(*values)) # 81
opts = {"sep": " | ", "end": "\n"}
print("a", "b", **opts) # a | b
معاملات موضعية فقط ومسماة فقط
الشرطة المائلة / والنجمة * في قائمة المعاملات تعملان كحاجز: كل ما قبل / يُمرَّر بالترتيب فقط (ظهرت هذه الصياغة في Python 3.8، PEP 570)، وكل ما بعد * يُمرَّر بالاسم فقط.
def convert(amount, /, *, rate, currency="USD"):
return round(amount / rate, 2), currency
convert(100, rate=90.5) # (1.1, 'USD')
convert(amount=100, rate=90.5) # TypeError: amount يُمرَّر موضعياً فقط
convert(100, 90.5) # TypeError: rate بالاسم فقط
المعاملات المسماة فقط تحمي من الخلط في استدعاءات مثل convert(100, 90.5, "EUR")، حيث لا يتضح ما هو كل شيء.
نطاق المتغيرات: قاعدة LEGB
يبحث Python عن اسم المتغير وفق سلسلة: Local (داخل الدالة) ← Enclosing (داخل الدالة الخارجية) ← Global (على مستوى الوحدة) ← Built-in (الأسماء المضمنة مثل len). ويمكن قراءة متغير عام داخل دالة، أما لإسناد قيمة جديدة إليه فتحتاج إلى التصريح global.
limit = 1000
def check(amount):
return amount > limit # قراءة المتغير العام — مسموحة
def raise_limit():
global limit
limit = 2000 # بدون global كان سيُنشأ متغير محلي
عملياً الأفضل تجنب global: الدالة التي تغيّر حالة خارجية يصعب اختبارها. والأفضل أن تستقبل القيمة معاملاً وتعيد النتيجة.
الدوال المتداخلة والإغلاقات
يمكن تعريف دالة داخل دالة أخرى. الدالة المتداخلة ترى متغيرات الدالة الخارجية حتى بعد انتهاء الخارجية، وهذا ما يُسمّى إغلاقاً (closure). ولتعديل مثل هذا المتغير يُستخدم nonlocal.
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c(), c(), c()) # 1 2 3
الإغلاقات أساس المزخرفات (decorators) ومصانع الدوال.
الدالة كائن أيضاً
في Python الدالة كائن عادي: يمكن وضعها في متغير، وتمريرها إلى دالة أخرى، وإعادتها من دالة، وتخزينها في قاموس. ولها خصائص مثل __name__ و__doc__ و__defaults__ و__annotations__.
def double(x):
return x * 2
f = double
print(f(5), f.__name__) # 10 double
print(list(map(double, [1, 2, 3]))) # [2, 4, 6]
Lambda في Python: الدوال المجهولة
دالة lambda دالة قصيرة بلا اسم، يتكون جسمها من تعبير واحد. تُعاد نتيجة التعبير تلقائياً، ولا تُكتب return.
square = lambda x: x ** 2
print(square(4)) # 16
expenses = [("قهوة", 250), ("تاكسي", 600), ("غداء", 450)]
expenses.sort(key=lambda e: e[1], reverse=True)
print(expenses) # [('تاكسي', 600), ('غداء', 450), ('قهوة', 250)]
print(list(filter(lambda n: n % 2 == 0, range(10)))) # [0, 2, 4, 6, 8]
تناسب lambda كمعامل قصير لـ sorted وmax وmap وfilter. ولا يوصي PEP 8 بإسناد lambda إلى متغير كما في square أعلاه: إذا احتاجت الدالة إلى اسم فاكتب def، فيكون لها __name__ واضح في التتبع، ومكان للتوثيق.
| def | lambda | |
|---|---|---|
| الاسم | له اسم | لا اسم (<lambda>) |
| الجسم | أي عدد من التعليمات | تعبير واحد |
| return | صريحة | ضمنية |
| التوثيق والتلميحات | نعم | لا |
| مكان الاستخدام | في كل مكان | key قصير أو دالة استدعاء |
تلميحات الأنواع: إرشادات لا فحوص
تصف التلميحات الأنواع المتوقعة للمعاملات والنتيجة. لا يفحصها Python أثناء التشغيل، فالدالة التي فيها x: int تقبل نصاً دون مشكلة. أما الفحص فتقوم به أدوات خارجية مثل mypy وpyright وty، وكذلك المحرر الذي يبرز الخطأ قبل التشغيل.
def add_expense(title: str, amount: float, category: str = "متنوع") -> dict[str, object]:
return {"title": title, "amount": amount, "category": category}
def area(w: float, h: float) -> float:
w * h # mypy: Missing return statement
يمكن تشغيل الفحص دون تثبيت دائم بالأمر uvx mypy .. ويوضح المثال أعلاه فائدة التلميحات: نسيان return مع -> float يلتقطه المحلل فوراً.
منذ Python 3.12 (PEP 695) تُكتب الدوال العامة دون TypeVar: يُذكر معامل النوع بين أقواس مربعة، ويُعرَّف الاسم المستعار للنوع بالكلمة type.
def first[T](xs: list[T]) -> T:
return xs[0]
type Money = int | float
في Python 3.14 (PEP 649) صارت التلميحات كسولة: تُحسب عند أول استخدام، فيمكن الإشارة إلى صنف مُعرَّف لاحقاً دون علامات اقتباس، ولم تعد الحاجة إلى from __future__ import annotations قائمة.
«لم يُمرَّر» أو «مُرِّر None»: الحارس sentinel في 3.15
القيمة الافتراضية None لا تميّز بين «لم يُمرَّر المعامل» و«مُرِّر None عن قصد». وقد كان يُحل ذلك لسنوات بعلامة يدوية:
_MISSING = object()
def get_rate(currency, default=_MISSING):
rates = {"USD": 90.5}
if currency in rates:
return rates[currency]
if default is _MISSING:
raise KeyError(currency)
return default
في Python 3.15 ظهرت الدالة المضمنة sentinel (PEP 661): MISSING = sentinel("MISSING") تنشئ علامة فريدة لها تمثيل نصي واضح، تبقى كما هي عند النسخ، وتُحفظ بـ pickle، ويمكن استخدامها في تلميح من نوع float | MISSING. وعلى الإصدارات القديمة تبقى الطريقة بـ object() صالحة.
أخطاء شائعة مع الدوال
- نسيان return: الدالة تعيد
None. - نسيان الأقواس:
print(greet)تطبع كائن الدالة، لا نتيجة الاستدعاء. - قيمة افتراضية قابلة للتعديل: قائمة واحدة مشتركة بين كل الاستدعاءات.
- عدد وسائط خاطئ:
TypeError: greet() missing 1 required positional argument: 'name'. - اسم يطابق دالة مضمنة: دالة باسم
listأوsumتحجب الدالة المضمنة.
الخلاصة
تُعرَّف الدوال في Python بالكلمة def، وتعيد النتيجة بـ return، وتستقبل الوسائط بالترتيب أو بالاسم. تمنحك *args و**kwargs المرونة، وتمنحك / و* صرامة الاستدعاء، وتوفر lambda الإيجاز للدوال القصيرة التي تُستخدم مرة واحدة، وتلتقط التلميحات مع mypy الأخطاء قبل التشغيل. وفي دورة بايثون تُشرح هذه الموضوعات عملياً: يُقسَّم متتبّع المصروفات إلى دوال ذات تلميحات، ويُفحص بـ mypy.
الأسئلة الشائعة
ما الفرق بين المعامل (parameter) والوسيطة (argument)؟
المعامل اسم في تعريف الدالة، والوسيطة القيمة التي تُمرَّر عند الاستدعاء. في def greet(name) الاسم name معامل، وفي greet("أمينة") النص "أمينة" وسيطة.
هل يمكن استدعاء الدالة قبل تعريفها؟
على مستوى الوحدة لا يمكن: يجب أن يُنفَّذ سطر def قبل الاستدعاء، وإلا ظهر NameError. أما داخل دالة أخرى فيمكن استدعاء دالة معرّفة أسفلها، بشرط أن يحدث الاستدعاء بعد تعريفها.
كم قيمة يمكن أن تعيد الدالة؟
رسمياً قيمة واحدة، لكنها قد تكون صفاً: return a, b يعيد صفاً من عنصرين، ويُفكّ كالتالي: x, y = f().
هل التلميحات إلزامية؟
لا، يعمل Python بدونها. لكن في الكود الذي يقرؤه غيرك تسهّل التلميحات الصيانة، وتتيح لـ mypy أو pyright اكتشاف الأخطاء قبل التشغيل.
هل يمكن كتابة if داخل lambda؟
يمكن فقط التعبير الشرطي: lambda x: "زوجي" if x % 2 == 0 else "فردي". ولا تُسمح التعليمات والحلقات والإسنادات داخل lambda.
يُشرح هذا الموضوع بالتفصيل مع التطبيق العملي في دورة «Python 3.15: от новичка до профи».
الانتقال إلى الدورة