Faragiri

همهٔ زبان‌ها

آموزش خطا و استثناسازه‌های اصلی برنامه

چهار لایه گرفتن خطا، و اینکه هر کدام چه چیزی را نگه می‌دارد و چه چیزی از آن رد می‌شود. یکی‌شان برنامه را غیرقابل توقف می‌کند.

۴بخش
۳دقیقه خواندن
۴عمق

چهار بند، چهار سوراخ

هر بند چه چیزی را نگه می‌دارد و چه چیزی از آن رد می‌شود

یک نمودار دفاعی که فقط بگوید هر لایه چه می‌گیرد، آگهی است؛ ستون دوم است که تصمیم را عوض می‌کند.

  1. except ValueErrorفقط همان یک شکست، با پیام دقیقKeyError و TypeError از همان سطر، که بالاتر گرفته می‌شوند
  2. except (ValueError, KeyError)چند شکست شناخته‌شده، با نگه داشتن خود خطاهر خطایی که در فهرست نیست، و این عمدی است
  3. except Exceptionتقریبا هر چیزی که یک برنامه بالا می‌آوردKeyboardInterrupt و SystemExit، که برنامه را قابل توقف نگه می‌دارند
  4. except خالیهمه چیز، تا آخرین موردهیچ، و مشکل همین است: برنامه دیگر با Ctrl+C هم بیرون نمی‌آید
سطری که می‌تواند بشکند

این لایه‌ها روی هم انباشته نمی‌شوند: در یک ساختار، اولین بندی که بخواند برنده است و بقیه اصلا نگاه نمی‌شوند.

یک except Exception همه چیز را نمی‌گیرد و این عمدی است. وقتی کاربر Ctrl+C می‌زند، چیزی که بالا می‌آید KeyboardInterrupt است و پدرش BaseException است نه Exception، پس از میان آن بند رد می‌شود و برنامه می‌ایستد. SystemExit هم همین‌طور. حالا همان بند را با یک except: خالی عوض کنید و برنامه در حلقه‌ای که هر بار خطا را می‌بلعد گیر می‌کند و دیگر با Ctrl+C هم بیرون نمی‌آید.

قاعده‌ای که از این بیرون می‌آید یک جمله است: بندی که نمی‌دانید چه می‌گیرد، تنها بندی است که نباید بنویسید. سه شکل درست، از تنگ به گشاد:

  • except ValueError: فقط همان یک شکست، و بقیه رد می‌شوند تا بالاتر گرفته شوند
  • except (ValueError, KeyError) as exc: چند شکست شناخته‌شده، با نگه داشتن خودِ خطا
  • except Exception: ته خط یک برنامه سرویس‌دهنده، جایی که هر درخواست باید جدا بشکند و بقیه ادامه دهند

خطای اصلی را در راه نگه دارید

وقتی یک خطای پایین‌سطح را به یک خطای بامعنی برای دامنه خودتان تبدیل می‌کنید، بند from را جا نیندازید:

  • except ValueError as exc:
  • raise RuntimeError("radif kharab") from exc

حالا e.__cause__ هنوز همان ValueError است و گزارش خطا هر دو را پشت سر هم چاپ می‌کند. بدون from، پایتون باز هم زنجیره را نگه می‌دارد ولی با عبارت «هنگام رسیدگی به خطای بالا خطای دیگری رخ داد» که یعنی این دومی اتفاقی بود. تفاوت بین «این را من ترجمه کردم» و «اینجا هم چیزی خراب شد» همین یک کلمه است، و کسی که لاگ را شش ماه بعد می‌خواند فقط همین را دارد.

بلوک try فقط سطری را بگیرد که می‌تواند بشکند

وسوسه این است که کل تابع داخل یک try گذاشته شود. بهایش این است که وقتی خطا آمد، دیگر معلوم نیست از کدام سطر آمده: یک KeyError ممکن است از خواندن پیکربندی باشد یا از پاسخ سرویس، و بند except هر دو را یک جور رسیدگی می‌کند. یک try که فقط یک سطر در آن است، هم پیام دقیق‌تری می‌سازد و هم اجازه می‌دهد سطرهای بعدی خطای خودشان را داشته باشند.

دو بند دیگر همین ساختار کمتر نوشته می‌شوند و هر دو کار مشخصی دارند. else بدنه‌ای است که فقط وقتی اجرا می‌شود که هیچ خطایی نیامده باشد، و همان چیزی است که سطرهای بعدی را از داخل try بیرون می‌آورد. finally در هر حالت اجرا می‌شود، حتی وقتی خطا در حال بالا رفتن است، و ترتیب اجرایش از درونی‌ترین فراخوانی به بیرون است: برنامه چطور یادش می‌ماند باید کجا برگردد.

جایی که کار گیر می‌کند، شبکه‌ای است که گاهی جواب می‌دهد

برنامه‌ای که از ایران به یک سرویس بیرونی وصل می‌شود، خرابی را به شکلی می‌بیند که در آموزش‌ها نیست: نه یک خطای تمیز، بلکه اتصالی که برقرار می‌شود و وسط پاسخ قطع می‌شود، یا نیم ساعت جواب می‌دهد و نیم ساعت نه. کسی که یک بار این را دیده، عادت می‌کند دور کل تماس یک except گشاد بگذارد و پیام «خطا در ارتباط» چاپ کند، و از آن به بعد یک کلید نامعتبر و یک وقفه زمانی و یک پاسخ ناقص هر سه یک پیام می‌گیرند. تشخیص همان‌جا از بین رفت. حداقل کاری که ارزشش را دارد این است که نوع خطا و متن اصلی‌اش در لاگ بماند، حتی وقتی چیزی که به کاربر نشان می‌دهید یک جمله ساده است.

ابزار، و سالی که این حرف درست است

در سال ۲۰۲۶ ماژول traceback در کتابخانه استاندارد و رایگان است و traceback.format_exception کل زنجیره را با همان بندهای __cause__ به رشته می‌دهد؛ جای چاپ کردن str(e) در لاگ را گرفته، که فقط پیام آخرین خطا را دارد و نه اینکه از کجا آمده. برای اینکه از اول متن خطا و بستر آن با هم ثبت شوند، ماژول logging همان کتابخانه پرچم exc_info را دارد و همین یک آرگومان جای کپی کردن دستی گزارش خطا در پیام لاگ را می‌گیرد.

پرسش بعدی این است که وقتی این گزارش‌ها روی هم انباشته شدند چه چیزی از آن‌ها باید نگه داشته شود تا خرابی بعدی قابل تشخیص باشد، که چه چیزی ثبت شود تا خرابی بعدی قابل تشخیص باشد جوابش است، و خواندن خود گزارش مهارتی جداست: سطری که مهم است معمولاً سطر اول نیست. در سمت سرویس، همین انتخاب تعیین می‌کند کاربر چه کد وضعیتی می‌گیرد: قرارداد بین دو برنامه.

کنار این

بالای این