Switching from JavaScript to Python feels like moving to a city with the same street signs but different traffic laws. The syntax looks friendly and familiar. You see async and await sitting right there in the grammar, so you assume the mental model ports over cleanly. It does not. One habitual pattern from JavaScript silently tanks your Python performance without crashing, without logging an error, and without showing up on a quick code review.

كيف يعلمك JavaScript أن تبدأ ثم تنسى

في JavaScript، يكون استدعاء الدالة غير المتزامنة (async function) "متحمسًا" (eager)؛ ففي اللحظة التي تستدعيها فيها، ينشئ المحرك Promise ويبدأ العمل فورًا. حلقة الأحداث (event loop) تنطلق بالفعل. لهذا السبب يكتب مطورو JavaScript الكود بشكل طبيعي على النحو التالي:

const userPromise = fetchUser(id);
const ordersPromise = fetchOrders(id);
const user = await userPromise;
const orders = await ordersPromise;

كلا طلبي الشبكة قيد التنفيذ قبل الوصول إلى أي await. يقوم الـ await الأول بتعليق الدالة الحالية حتى يتم حل fetchUser (resolves)، ولكن fetchOrders يعمل بالفعل في الخلفية منذ السطر السابق. وبحلول الوقت الذي تحتاج فيه إلى متغير orders ، قد يكون الطلب الثاني قد اكتمل بالفعل. يبدو هذا النمط طبيعيًا جدًا في JavaScript لدرجة أن العديد من المطورين لا يعتبرونه حتى خدعة للتزامن (concurrency trick). إنها ببساطة الطريقة التي تعمل بها async.

مفاجأة Python: الـ Coroutine البارد

تستخدم Python تعاقدًا مختلفًا. عندما تستدعي دالة async def في Python، فأنت لا تبدأ أي عمل، بل تحصل على كائن coroutine. فكر في الأمر كأنه وصفة مكتوبة على الورق؛ المكونات مدرجة، والخطوات واضحة، ولكن لا يوجد شيء في الفرن. وحتى يقوم شيء ما بتشغيل هذا الـ coroutine صراحةً عبر حلقة الأحداث (event loop)، فإنه يظل خاملًا.

إليك الفخ. المهندس الذي اعتاد على JavaScript ويحتاج إلى مستخدم وطلباته قد يكتب هذا في Python:

user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro

يبدو الكود متزامنًا (concurrent). تشعر بأنه متزامن. لكنه في الواقع تسلسلي (sequential) تمامًا.

السطر الأول يخصص coroutine خامل لـ user_coro. والسطر الثاني يخصص coroutine خامل آخر لـ orders_coro. عندما يصل التنفيذ إلى await user_coro ، تبدأ Python أخيرًا المهمة الأولى وتشغلها حتى تكتمل. وفقط بعد انتهاء fetch_user يصل المفسر (interpreter) إلى await orders_coro ويبدأ المهمة الثانية. إجمالي وقت التنفيذ لديك هو مجموع عمليتي الإدخال والإخراج (I/O operations)، وليس الأطول بينهما. أنت لم تقم بتشغيلهما بالتوازي (in parallel)، بل قمت بتشغيلهما واحدة تلو الأخرى مع خطوات إضافية.

لماذا هذا الخطأ غير مرئي

هذا هو نوع تراجع الأداء الذي يستمر لشهور. الكود صحيح في Python، ويجتاز فاحصي الأنواع (type checkers)، ويعيد النتائج الصحيحة. إنه فقط يعمل بنصف السرعة، أو أسوأ. ولأنه لا يوجد تتبع للمكدس (stack trace) ولا يوجد تحذير، غالبًا ما تبحث الفرق الهندسية في كل مكان آخر أولاً؛ فيقومون بإضافة ذاكرة التخزين المؤقت Redis، أو ترقية مستويات قاعدة البيانات، أو تغيير مناطق الاستضافة. المتسبب الحقيقي هو عدم تطابق دقيق في التوقعات حول ما تفعله await في الواقع.

ثلاث طرق لجعل Python تعمل الأشياء بالتوازي فعليًا

لإصلاح ذلك، يجب أن تخبر حلقة الأحداث (event loop) في Python بجدولة العمل فورًا. أنت بحاجة إلى شيء أكثر نشاطًا من مجرد coroutine خام. أنت بحاجة إلى Task.

1. asyncio.create_task

الترجمة الأكثر مباشرة لنمط JavaScript هي تغليف الـ coroutine الخاص بك في Task. يتم جدولة الـ Task في حلقة الأحداث بمجرد إنشائه. إنه المعادل الأقرب في Python لـ JavaScript Promise أثناء التنفيذ.

user_task = asyncio.create_task(fetch_user(id))
orders_task = asyncio.create_task(fetch_orders(id))

user = await user_task
orders = await_orders_task

الآن أصبح كل من fetch_user و fetch_orders قيد التنفيذ قبل أول await. عندما تصل إلى await user_task ، فإنك تتوقف فقط حتى تكتمل تلك الـ Task المحددة، ولكن الـ Task الأخرى تستمر في العمل. إذا انتهى fetch_orders أولاً، فإن نتيجته تنتظر ببساطة داخل orders_task حتى تطلبها.

كن حذرًا، مع ذلك. إذا قمت بإنشاء Task ولم تنتظرها (await) أبدًا، فستصدر Python خطأً بشأن مهمة معلقة تم تدميرها. يجب عليك لا تزال جمع نتائجك.

2. asyncio.gather

إذا كان لديك عدة coroutines تحتاج جميعها إلى الانتهاء قبل المتابعة، فإن asyncio.gather تتولى كتابة الأكواد الروتينية (boilerplate) نيابة عنك. فهي تقوم بجدولة كل coroutine كـ Task داخليًا وتنتظرهم معًا.

user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))

هذا الأسلوب موجز وقابل للقراءة. وهو يتألق عندما تكون العمليات مستقلة وتريد سطرًا واحدًا يعبر عن "قم بتشغيل كل هذه، ثم أعطني كل نتيجة". كما أنه يحافظ على ترتيب الوسائط (arguments) في القائمة أو الـ tuple المسترجعة، حتى لو اكتملت المهام الأساسية بترتيب مختلف.

3. asyncio.TaskGroup

قدم Python 3.11 ميزة TaskGroup التي تجلب التزامن المهيكل (structured concurrency) إلى المكتبة القياسية. بدلاً من إنشاء المهام يدوياً، يمكنك استخدام مدير سياق (context manager) يضمن انتهاء كل مهمة تم إنشاؤها بشكل صحيح. إذا تسببت إحدى المهام في استثناء (exception)، يتم إلغاء المهام الأخرى تلقائياً.

async with asyncio.TaskGroup() as tg:
    user_task = tg.create_task(fetch_user(id))
    orders_task = tg.create_task(fetch_orders(id))

user = user_task.result()
orders = orders_task.result()

هذا النمط ممتاز لسير العمل المعقد. فهو يزيل خطر ترك مهمة (Task) يتيمة، ويجمع دورة حياة العمليات ذات الصلة تحت مظلة منطقية واحدة. إذا كان كود البرمجة الخاص بك يعمل على Python 3.11 أو أحدث، فغالباً ما تكون هذه هي البنية الأنظف للتزامن من نوع fan-out.

النموذج الذهني: await تعني "شغّل هذا الآن"

الدرس الأساسي هو لغوي. في JavaScript، يمكنك قراءة await بمعنى "في هذه الأثناء". تبدأ العمل، وتقوم بأشياء أخرى، وتتوقف فقط عندما تحتاج إلى القيمة. أما في Python، فتعني await "دفع هذه الـ coroutine إلى نقطة تعليقها التالية أو إلى الإكمال". إذا لم يتم جدولة الـ coroutine بعد، فإن await هي التي تقوم بجدولتها. لهذا السبب لا يمكنك بدء اثنين من الـ coroutines الخام ثم انتظارهم (await) لاحقاً؛ فأنت لم تعطِ حلقة الأحداث (event loop) أي شيء لتفعله في هذه الأثناء.

فكر في الـ coroutines في Python مثل دوال المولد (generator functions). استدعاء المولد لا يعني تكراره (iterate). أنت بحاجة إلى عمل حلقة (loop) عليه، أو استدعاء next()، أو تمريره إلى مستهلك (consumer). يعمل الـ async بنفس الطريقة. تُعد asyncio.create_task هي المستهلك الذي يقول "ضع هذا في حلقة الأحداث الآن". أما الـ await اللاحقة فتقوم ببساطة بانتظار إشارة الانتهاء.

عادة ملموسة تساعدك: كلما قمت بتعيين استدعاء دالة async إلى متغير بدون استخدام await ، اسأل نفسك عما إذا كنت قد قمت بجدولتها. إذا لم يكن الطرف الأيمن مغلفاً بـ create_task أو gather أو TaskGroup ، فإنه لا يعمل. إنه مجرد وصفة موضوعة على الطاولة.

الخلاصة

بيئة تشغيل الـ async في Python قوية، لكنها تتطلب نية صريحة. اللغة لا تبدأ العمل في الخلفية لمجرد أنك استدعيت دالة. إذا كنت قادماً من JavaScript، فراجع كل مكان تقوم فيه بتخزين coroutine في متغير ثم تنتظره (await) لاحقاً. ما لم تكن قد رفعتها إلى Task أولاً، فأنت قد كتبت كوداً تسلسلياً يرتدي ملابس الـ async. ابدأ العمل باستخدام Task، ثم انتظر النتائج. هكذا تحول الـ async في Python من عنق زجاجة صامت إلى أداة تزامن حقيقية.