8.6. الاستثناءات

تتصرف الاستثناءات داخل برنامج asyncio بشكل شبه مطابق لتصرفها في Python العادية -- فهي تنتشر صعوداً عبر سلسلة الاستدعاء حتى يلتقطها شيء ما. شبه مطابق لأن المهام تعمل بالتوازي، فالمسار "صعوداً" ليس هو المسار الذي أنشأ المهمة. تغطي هذه الصفحة وجهة الاستثناءات في كل من الأشكال الشائعة.

8.6.1. داخل دالة تعاونية واحدة

تلتقط try/except داخل دالة تعاونية الاستثناءات التي يثيرها أي شيء تنتظره بـ await، بالطريقة المعتادة:

async def fetch_with_retry(url):
    for attempt in range(3):
        try:
            return await fetch(url)
        except OSError as e:
            last_error = e
    raise last_error

لا شيء خاص بـ asyncio هنا -- فعبارة except ترى الاستثناءات المثارة داخل fetch كما لو كان استدعاء دالة عادية.

8.6.2. في مهمة ينتظرها التطبيق

عندما تثير دالة تعاونية تعمل كـ Task استثناءً، يُخزَّن الاستثناء على المهمة. وفي المرة التالية التي ينتظر فيها شيء ما تلك المهمة بـ await، يُعاد إثارة الاستثناء عند await

task = asyncio.create_task(may_fail())
try:
    result = await task
except OSError:
    log("may_fail failed")

ينطبق الأمر ذاته على asyncio.gather(). فالسلوك الافتراضي -- أحد الأبناء يثير استثناءً، ويُلغى الباقون، وينتشر الاستثناء خارج الـ gather -- ينبع من هذه الآلية.

8.6.3. في مهمة لا ينتظرها أحد

المهمة التي لا ينتظرها أحد قط بـ await هي الحالة التي تحتاج إلى انتباه. فالاستثناء لا يزال يقع؛ وتلاحظ الحلقة أن المهمة انتهت باستثناء غير معالَج؛ لكن لا يوجد await ليظهر عنده. والسلوك الافتراضي هو طباعة تتبع للمكدس عبر sys.stderr ومتابعة التشغيل -- وهو أمر لا بأس به لتشخيص غير مراقَب، لكنه غير ملائم لتطبيق أراد أن يعلم.

غالباً ما يكون الإصلاح الصحيح هو انتظار المهمة. إما مباشرةً، بتذكّر المقبض وانتظاره أثناء الإغلاق، أو ضمنياً عبر gather() أو wait_for(). نمط "إغلاق تطبيق" في صفحة المهل الزمنية والإلغاء يعالج هذه الحالة للمهام الخلفية طويلة العمر التي يولّدها البرنامج النصي النموذجي.

8.6.4. معالِج استثناءات مخصص

عندما لا يكفي التتبع الصامت والمتابعة، تكشف الحلقة عن خطّاف -- Loop.set_exception_handler -- يمكن للتطبيق تجاوزه للقيام بشيء آخر:

def handler(loop, context):
    print("asyncio:", context.get("message"))
    if "exception" in context:
        sys.print_exception(context["exception"])

loop = asyncio.get_event_loop()
loop.set_exception_handler(handler)

وسيطة context هي قاموس بمفاتيح 'message' و 'exception' و 'future'. وقد يكون الاستثناء مفقوداً في أحداث معينة من نمط التحذير، ولهذا يستخدم المثال .get().

الاستخدامات النموذجية هي تسجيل الفشل في الفلاش، أو وميض LED للخطأ، أو التصعيد إلى إعادة إقلاع عبر مراقب (watchdog). تغطي صفحة التحكم بالحلقة كامل سطح خطّافات الحلقة.

8.6.5. KeyboardInterrupt

عندما يُوقَف برنامج نصي من الخارج -- عادةً بطلب الـ IDE إيقافه -- يصل الطلب داخل البرنامج النصي على هيئة KeyboardInterrupt. وداخل asyncio.run() ينتشر كما ينتشر أي استثناء آخر غير معالَج: يُلغى main، وتُلغى أيضاً كل مهمة تتعقبها الحلقة، ويُعاد إثارة KeyboardInterrupt خارج asyncio.run(). وتعمل عبارات finally أثناء الخروج، فيتولّى الأمر نفسُ نمط التنظيف من صفحة الإلغاء.