فصل ۶: فایل‌ها و خطاها — pathlib، UTF-8، CSV، JSON و logging

raise، استثنای سفارشی و logging به‌جای print

خطای خودتان را بسازید و ردپا بگذارید

گرفتن خطا نیمی از ماجراست؛ نیم دیگر پرتاب خطای معنادار است. تابعی که ورودی نامعتبر می‌گیرد نباید مقدار عجیبی مثل ‎-1 یا None برگرداند که فراخواننده فراموش کند بررسی‌اش کند؛ باید با raise صریحاً اعلام کند.

def rug_area(width_cm: int, length_cm: int) -> float:
    if width_cm <= 0 or length_cm <= 0:
        raise ValueError(f"ابعاد نامعتبر: {width_cm}×{length_cm}")
    return width_cm * length_cm / 10_000

استثنای سفارشی

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

class OrderError(Exception):
    """پایه‌ی همه‌ی خطاهای مربوط به سفارش."""

class OutOfStockError(OrderError):
    def __init__(self, code: str, requested: int, available: int):
        super().__init__(f"موجودی {code} کافی نیست: {requested} درخواست، {available} موجود")
        self.code = code
        self.requested = requested
        self.available = available

try:
    raise OutOfStockError("KSH-101", 5, 2)
except OrderError as e:          # همه‌ی خطاهای سفارش را یک‌جا بگیر
    print(e, "| کد:", e.code)

حالا لایه‌ی رابط کاربری می‌تواند با یک except OrderError پیام مناسب نشان دهد و باگ‌های واقعی (TypeError و…) همچنان بالا بروند و دیده شوند.

زنجیره‌ی علت: raise ... from

def parse_qty(text: str) -> int:
    try:
        return int(text)
    except ValueError as e:
        raise OrderError(f"تعداد نامعتبر: {text!r}") from e

با from e خطای اصلی در traceback به‌عنوان «علت مستقیم» نمایش داده می‌شود و اطلاعات دیباگ گم نمی‌شود.

logging: print حرفه‌ای

print برای پیام به کاربر است، نه برای ردگیری رفتار برنامه. ماژول logging سطح اهمیت، زمان، نام ماژول و مقصد (فایل، کنسول) دارد و بدون حذف کد می‌توانید سطح جزئیات را کم و زیاد کنید:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
    filename="app.log",
    encoding="utf-8",          # بدون این، فارسی در ویندوز خراب می‌شود
)
log = logging.getLogger(__name__)

log.info("سفارش %s ثبت شد", "KSH-101")
log.warning("موجودی %s کمتر از ۳ تخته است", "TBZ-220")
try:
    1 / 0
except ZeroDivisionError:
    log.exception("خطا در محاسبه‌ی تخفیف")   # traceback کامل را هم ثبت می‌کند
سطحکاربرد
DEBUGجزئیات برای توسعه‌دهنده
INFOرویدادهای عادی: «سفارش ثبت شد»
WARNINGغیرعادی ولی قابل ادامه (سطح پیش‌فرض)
ERROR / CRITICALشکست یک عمل / شکست کل برنامه

نکته‌هایی که کمتر کسی می‌داند

  • در log پیام را با %s و آرگومان جدا بدهید، نه f-string؛ اگر آن سطح غیرفعال باشد، رشته اصلاً ساخته نمی‌شود و ابزارهای جمع‌آوری لاگ پیام‌های هم‌شکل را گروه‌بندی می‌کنند.
  • basicConfig فقط بار اول اثر دارد؛ اگر کتابخانه‌ای قبل از شما logging را پیکربندی کرده باشد، تنظیمات شما نادیده گرفته می‌شود. در این حالت force=True بدهید.
  • e.add_note("سفارش مشتری: رضا") (3.11+) به خطای موجود یادداشت اضافه می‌کند که در traceback چاپ می‌شود، بدون ساختن استثنای جدید.
  • نام کلاس استثنای سفارشی را با Error تمام کنید (قرارداد PEP 8) و مستقیماً از Exception ارث ببرید، نه از BaseException.
  • assert برای اعتبارسنجی ورودی کاربر نیست: با اجرای python -O همه‌ی assert ها حذف می‌شوند. برای ورودی‌ها از raise استفاده کنید.

برای ذخیره‌ی پیشرفت و شرکت در آزمون، وارد شوید — رایگان است.