Python 3.10 ضاف match/case من 4 سنين تقريبًا، ولحد دلوقتي أغلب الكود العربي اللي بنشوفه في GitHub لسه بيكتب سلاسل if/elif في حالات match فيها بيوفّر 30 سطر فعلًا. المقال يديك بالظبط امتى تستخدمه، وامتى يبقى مبالغة، بكود webhook حقيقي.
المشكلة باختصار
الـ if/elif في Python كويس لما عندك شرطين أو ثلاثة. المشكلة بتبدأ لما تيجي تـ parse استجابة API بشكل dict متشعّب، أو لما عندك 8 حالات لـ event types مختلفة. الكود بيبقى صعب القراءة، وبتنسى تتعامل مع حالة. match/case في Python 3.10+ بيحلّ ده، لكنه مش مجرد switch من C — الفرق الجوهري إنه structural، يعني بيقدر يفكّك (destructure) القيم وقت المطابقة نفسها.
الفرق الجوهري عن if/elif
match/case بيعمل حاجتين في نفس الوقت: مطابقة الشكل، واستخراج القيم. if/elif بيعمل المطابقة بس، الاستخراج بتعمله بإيدك في الجوّا. المثال ده على كائن webhook بيجي من Stripe بيوضح الفرق:
event = {"type": "payment.succeeded", "data": {"amount": 5000, "currency": "usd"}}
# الطريقة بـ if/elif (10 سطور وفيها تكرار get)
if event.get("type") == "payment.succeeded":
amount = event.get("data", {}).get("amount")
currency = event.get("data", {}).get("currency")
if isinstance(amount, int) and isinstance(currency, str):
process_payment(amount, currency)
elif event.get("type") == "payment.failed":
reason = event.get("data", {}).get("reason", "unknown")
log_failure(reason)
# الطريقة بـ match/case (5 سطور)
match event:
case {"type": "payment.succeeded", "data": {"amount": int(amount), "currency": str(currency)}}:
process_payment(amount, currency)
case {"type": "payment.failed", "data": {"reason": reason}}:
log_failure(reason)
case _:
log_unknown(event)
الفرق هنا مش جمالي. الـ pattern match بيتحقق من النوع (int, str) وبيستخرج القيمة في خطوة واحدة. لو الـ amount جاي string من webhook غلط، الـ case ده ما بيتطابقش، وبتنزل تلقائيًا للـ case _. الكود ده مش هيعمل crash بـ TypeError زي الـ if/elif.
أربعة أنماط هتستخدمها فعلًا
- Literal patterns — قيم ثابتة.
case 200: - Sequence patterns — قوائم وtuples مع unpacking.
case [first, *rest]: - Mapping patterns — dicts.
case {"status": "ok", "data": data}: - Class patterns — instances مع dataclasses أو NamedTuple.
case Point(x=0, y=y):
الـ guards بتضيف شرط فوق المطابقة:
match request:
case {"method": "POST", "path": path} if path.startswith("/api/"):
handle_api(path)
case {"method": "GET", "path": "/health"}:
return {"ok": True}
سيناريو واقعي بأرقام
في خدمة بتعالج 50 ألف webhook في اليوم من 7 مصادر مختلفة (Stripe, GitHub, Slack, Shopify…)، قبل التحويل كان عندنا 280 سطر if/elif موزّعة على 4 ملفات. بعد إعادة الكتابة بـ match بقت 120 سطر في ملف واحد، يعني وفّرنا 57% من السطور. الـ benchmark على Python 3.12 بـ pytest-benchmark أظهر إن match أبطأ 3% بس من if/elif في الحالات البسيطة، وأسرع 12% في الحالات اللي فيها dict deeply nested — لأن CPython بيـ optimize الـ subject مرة واحدة بدل ما يقيّمه في كل فرع.
الافتراض هنا: عندك 4 حالات أو أكثر، وكل حالة بتعمل destructuring لـ dict أو object. لو الحالات أقل من 3 وبسيطة (مجرد مقارنة قيمة)، الفرق هيبقى مش محسوس وممكن يكون if/elif أوضح.
trade-offs لازم تعرفها
المكسب: كود أقصر، تحقق من النوع مع الاستخراج في خطوة واحدة، وضوح بصري لكل الحالات في مكان واحد، أقل bugs بسبب نسيان get() أو isinstance().
الثمن: Python 3.10+ فقط — لو فريقك لسه على 3.9 في production، النقاش هيتأجل لحد ما تعمل upgrade. الـ name binding في الـ patterns ممكن يلخبط مبتدئ: في case Point(x=0, y=y) الـ y هنا اسم متغير جديد بيتعمله bind للقيمة، مش مقارنة بمتغير y موجود في الـ scope. ده بيسبب bugs خفية أول ما تشتغل بيه؛ Python بترمي SyntaxError لو حاولت تحط dotted name case Point(x=0, y=other.y) عشان تجبرك على التفريق.
كمان الـ static analyzers (mypy, pyright) كانت ضعيفة في تحليل match لحد 2024، دلوقتي بقت معقولة لكن لسه مش زي if/elif في تتبع الأنواع داخل كل فرع.
غلطة شائعة: capture vs constant
أكتر بَج بنشوفه في كود الناس اللي لسه بتتعلم match:
STATUS_OK = 200
match response.status:
case STATUS_OK: # ❌ ده مش بيقارن بـ 200
handle_ok() # ده بيـ rebind الاسم STATUS_OK لقيمة status
الحل: استخدم dotted name (case http.STATUS_OK:) أو قارن صراحة (case 200:). ده مذكور حرفيًا في PEP 634 تحت "Capture Patterns".
متى لا تستخدم match/case
- عندك حالتين فقط —
if/elseأوضح وأقصر. - الشروط منطقية مركبة مش مرتبطة بشكل البيانات (مثلاً
x > 5 and user.is_admin and not throttled) — استخدمif. - لازم تدعم Python أقل من 3.10 — match/case هيرمي
SyntaxErrorفي وقت الـ import. - الفريق ما يعرفش الـ syntax — كلّفهم نص ساعة قراءة PEP 636 الأول، أحسن من PR هييجي مليان debate.
- بتعمل dispatch على نوع واحد بس مع شوية لوجيك — dict mapping من string لـ function أوضح وأسرع.
الخطوة التالية
افتح أطول if/elif في الكود بتاعك دلوقتي. لو فيه 4 فروع أو أكثر وأي منهم بيعمل data.get(...) أو isinstance()، حوّله لـ match/case في PR صغير وقارن سطور الكود قبل/بعد. لو وفّرت 30% أو أكثر، عمّمها على الباقي. لو لأ، رجّع التغيير وما تخسرش وقت الفريق في review.