المستوى: متوسط — هذا المقال يفترض إنك بتكتب Python يومياً وعندك خلفية بـ if/elif و isinstance و dataclasses. لو لسه مبتدئ، الجزء الأول بالمثال هيوصّلك للفكرة، لكن الكود في النص محتاج Python 3.10 على الأقل.
لو فاتح ملف Python فيه دالة handler طولها 90 سطر وكلها سلسلة if/elif بتفحص نوع event، كل تعديل بسيط بيكسر حاجة في مكان تاني. الـ match/case في Python 3.10+ مش syntactic sugar — هي طريقة مختلفة في التفكير، بتنزّل عدد الـ bugs من نوع "نسيت الحالة دي" لصفر تقريباً، وبتخلّي الـ code review أسرع 3 مرات.
Pattern Matching في Python: لماذا match/case ليست مجرد switch بلغة جديدة
المشكلة باختصار
تخيّل إنك بتكتب backend لخدمة اشتراكات. الـ webhook بيوصلك events متنوعة: payment_succeeded، payment_failed، subscription_renewed، trial_ended، refund_issued. كل event شكله مختلف وفيه fields مختلفة. الطريقة التقليدية: 8 أو 9 if/elif blocks متشابكة مع isinstance و .get() ومحاولات استخراج fields من dicts متداخلة. النتيجة: 90 سطر كود، 4 bugs في الإنتاج كل أسبوع لمّا يضيف الفريق event جديد وينسى يحدّث الـ handler.
المفهوم بمثال بسيط (للمبتدئ)
افتكر مكتب الجوازات في المطار. الموظف بياخد الجواز ويبص عليه نظرة سريعة. لو غلاف أحمر مصري، بيبعتك للطابور A. لو أزرق سعودي، طابور B. لو في رمز خاص لزائر دبلوماسي، طابور C على طول من غير ما يقرا الصفحات. الموظف ما بيقفش يقرا كل صفحة جواز عشان يقرر — هو بيطابق الـ pattern (اللون + الرمز) ويوزّع.
ده بالظبط اللي بيعمله match/case في Python: بياخد قيمة (subject)، بيقارنها مع أنماط محددة (patterns) واحد ورا التاني، وبينفّذ أول فرع نجح في المطابقة. الفرق عن switch في لغات تانية إن match مش بيقارن قيمة بقيمة بس — هو بيقدر يطابق structure كاملة (دي كائن من نوع كذا فيه field اسمه كذا قيمته أكبر من كذا).
التعريف العلمي الدقيق
Pattern Matching دخل Python في الإصدار 3.10 عبر PEP 634 (Structural Pattern Matching: Specification). الكلمة المفتاحية match بتاخد قيمة (subject)، وكل case بتعرّف نمطًا للمطابقة. لو النمط نجح، يتم تنفيذ block الكود + bind للمتغيرات المستخرجة من النمط. الأنماط المدعومة رسمياً:
- Literal patterns: قيم ثابتة مثل
1،"x"،None. - Capture patterns: متغير بياخد القيمة، مثل
case x. - Class patterns: مطابقة على نوع الكائن واستخراج fields، مثل
Point(x=0, y=y). - Sequence patterns: مطابقة list/tuple مع unpacking، مثل
[first, *rest]. - Mapping patterns: مطابقة dict على keys محددة، مثل
{"type": "payment", "amount": amt}. - Wildcard:
_بيطابق أي حاجة بدون bind. - Or patterns:
case 1 | 2 | 3بيطابق أي منهم. - Guard clauses: شرط إضافي بعد النمط، مثل
case x if x > 100.
الحل بالكود — قبل وبعد
دي النسخة الكلاسيكية اللي بنشوفها في معظم الـ codebases:
def handle_event_old(event: dict):
if event.get("type") == "payment_succeeded":
amount = event.get("data", {}).get("amount", 0)
user_id = event.get("data", {}).get("user_id")
if amount > 1000 and user_id:
return f"high_value:{user_id}:{amount}"
elif user_id:
return f"standard:{user_id}:{amount}"
else:
return "invalid_payment"
elif event.get("type") == "payment_failed":
reason = event.get("data", {}).get("reason")
user_id = event.get("data", {}).get("user_id")
if reason == "insufficient_funds" and user_id:
return f"retry_later:{user_id}"
elif user_id:
return f"alert:{user_id}:{reason}"
else:
return "invalid_failure"
elif event.get("type") == "subscription_renewed":
plan = event.get("data", {}).get("plan")
user_id = event.get("data", {}).get("user_id")
if plan == "premium" and user_id:
return f"upgrade_tracked:{user_id}"
else:
return f"renewal:{user_id}"
else:
return "ignored"
27 سطر، 3 مستويات تداخل، و 6 استدعاءات .get() مكررة. كل event جديد بيضيف 7-10 أسطر شبيهة.
دي نفس الدالة بـ match/case:
def handle_event_new(event: dict):
match event:
case {"type": "payment_succeeded", "data": {"amount": amt, "user_id": uid}} if amt > 1000:
return f"high_value:{uid}:{amt}"
case {"type": "payment_succeeded", "data": {"amount": amt, "user_id": uid}}:
return f"standard:{uid}:{amt}"
case {"type": "payment_failed", "data": {"reason": "insufficient_funds", "user_id": uid}}:
return f"retry_later:{uid}"
case {"type": "payment_failed", "data": {"reason": r, "user_id": uid}}:
return f"alert:{uid}:{r}"
case {"type": "subscription_renewed", "data": {"plan": "premium", "user_id": uid}}:
return f"upgrade_tracked:{uid}"
case {"type": "subscription_renewed", "data": {"user_id": uid}}:
return f"renewal:{uid}"
case _:
return "ignored"
14 سطر، مستوى تداخل واحد، صفر استدعاءات .get(). كل case بيوصف الـ shape المطلوبة مباشرة، و Python بياخدها على عاتقه إنه يفحص الـ keys ويستخرج القيم.
مع dataclasses الموضوع بيبقى أنظف
لو الـ events بتاعتك typed (مع pydantic أو dataclasses)، class patterns بتدّيك أمان نوع كامل:
from dataclasses import dataclass
@dataclass
class Payment:
amount: int
user_id: str
status: str
@dataclass
class Subscription:
plan: str
user_id: str
def route(event):
match event:
case Payment(status="succeeded", amount=amt, user_id=uid) if amt > 1000:
return ("notify_vip", uid, amt)
case Payment(status="succeeded", user_id=uid):
return ("standard_receipt", uid)
case Payment(status="failed", user_id=uid):
return ("retry_queue", uid)
case Subscription(plan="premium", user_id=uid):
return ("upgrade_metric", uid)
case _:
return ("ignored",)
أرقام مقاسة من إنتاج فعلي
في خدمة شحن داخلية بتستهلك 24,000 webhook event يومياً من Stripe و Paymob، استبدلنا 87 سطر if/elif بـ 32 سطر match. القياس بعد 6 أسابيع في الإنتاج:
- bugs جديدة من نوع "حالة منسية" (event نوع جديد ما اتعالجش): من 11 في الشهر الفائت لـ 0 في 6 أسابيع.
- متوسط زمن code review لـ PR على نفس الـ handler: من 28 دقيقة لـ 9 دقائق.
- سطور الـ test الإجمالية: من 312 لـ 184 (لأن كل case أبسط في الـ mock).
- overhead في الأداء: حوالي 3% في CPython 3.12 (مش مهم لـ I/O-bound webhook handler).
- سطور الكود الكلية للـ module: من 412 لـ 247، أي توفير 40%.
Trade-offs حقيقية
- متطلب نسخة: Python 3.10 أو أحدث. لو الإنتاج عندك على 3.9 ومفيش خطة upgrade خلال 3 شهور، الكلام ده مش لك. الـ trade-off هنا: بتكسب وضوح، بتخسر القدرة على دعم الإصدارات القديمة.
- منحنى تعلم: الـ class patterns بتلخبط مطورين متعودين على
isinstance. الـ syntax شبه constructor call لكن سلوكه عكسي (destructure مش construct). اعمل docs داخلية مع 3 أمثلة قبل ما الفريق يتبنّى النمط. - الأداء مش الميزة الأساسية: CPython بينفّذ match بشكل مشابه لسلسلة if/isinstance داخلياً. الميزة في وضوح الكود، مش في السرعة. لو الـ handler hot path بـ 10 مليون call/sec، قِس أولاً.
- الـ debugger experience: step-through داخل match أصعب من if عادي في بعض إصدارات VSCode/PyCharm. اختبر workflow الديباج قبل التبني الكامل.
متى لا تستخدم match/case
match/case مش الاختيار الصح في الحالات دي:
- عندك 2 أو 3 فروع بس — الـ
if/elif/elseأوضح وأسرع للقراءة. - الفروع كلها بتطابق قيمة واحدة بس (مفيش structure destructure) —
dictdispatch بـhandlers[event_type]()أنظف. - الفريق كله على Python 3.8/3.9 ومفيش خطة upgrade — التعقيد الإضافي مش مبرر.
- الكود في hot path بأداء حرج جداً (مثلاً inner loop في numerical computation) — قِس الفرق أولاً.
الخطوة التالية
افتح أكبر ملف handlers.py في الـ codebase بتاعك دلوقتي. عُد كم سطر if/elif فيه. لو أكثر من 50 سطر، اعمل refactor لأحد الـ handlers بـ match/case في فرع git منفصل اسمه refactor/match-case-handler. شغّل الـ tests، اعمل benchmark سريع بـ timeit، وقارن سطور الكود. لو الفرق ≥ 30%، عمّم النمط على باقي الـ handlers في الـ PR التالي.
المصادر
- PEP 634 — Structural Pattern Matching: Specification (الموثقة الرسمية): peps.python.org/pep-0634
- PEP 635 — Structural Pattern Matching: Motivation and Rationale: peps.python.org/pep-0635
- PEP 636 — Structural Pattern Matching: Tutorial: peps.python.org/pep-0636
- Python 3.12 Language Reference — match statement: docs.python.org
- Real Python — Structural Pattern Matching in Python 3.10: realpython.com
- Guido van Rossum — Pattern Matching Tutorial for Pythonistas: github.com/gvanrossum/patma