سرنخ — خودآموز برنامه‌نویسی برای هوش مصنوعی (مقدماتی)

فصل ۸ از ۱۱

پیشرفت ترم
۰٪

ترم ۱ · پایتون از صفر، داخلِ Colab

خطا دشمن نیست: خواندنِ traceback

فصل ۸پیش‌نمایش رایگان

در این فصل چه یاد می‌گیری#

هر برنامه‌نویسی، از روزِ اول تا آخرِ عمر، خطا می‌گیرد. چیزی که تازه‌کار را از حرفه‌ای جدا می‌کند این نیست که یکی خطا می‌گیرد و دیگری نه — این است که حرفه‌ای در ده ثانیه می‌فهمد خطا چه می‌گوید. در این فصل یاد می‌گیری آن متنِ قرمزِ چندخطی را از کدام سرش بخوانی، شش خطای پرتکرارِ پایتون را از روی پیامشان بشناسی، و با try/except خطاهای قابلِ‌پیش‌بینی را مدیریت کنی بدونِ اینکه خطاهای واقعی را پنهان کنی.

یک نردبانِ برش‌خورده که هر پله‌اش یک قابِ خالی است و یک ذره‌بین روی پایین‌ترین پله ایستاده

آخر این فصل می‌توانی:

  • یک traceback را از پایین به بالا بخوانی و بگویی خطا کجا و چرا رخ داده
  • شش خطای رایجِ پایتون را از روی پیامشان تشخیص بدهی
  • با try/except یک خطای پیش‌بینی‌شده را مدیریت کنی
  • بگویی چرا except بدونِ نامِ خطا یک اشتباهِ جدی است

قبل از شروع#

از فصل ۷: تابع، return، و اینکه تابع می‌تواند تابعِ دیگری را صدا بزند.

از فصل ۲ تا ۶: پیام‌های TypeError، IndexError و KeyError را جسته‌گریخته دیده‌ای؛ اینجا مرتبشان می‌کنیم.

📓 نوت‌بوک: نوت‌بوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و به‌ترتیب داخلش هست.

۱. یک خطای واقعی، با تابع#

اول یک تابعِ سالم بساز:

def average(numbers):
    """میانگینِ یک فهرست عدد."""
    return sum(numbers) / len(numbers)

print(average([10, 20, 30]))
20.0

حالا در یک سلولِ خالی همین تابع را با فهرستِ خالی صدا بزن:

print(average([]))
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[2], line 1
----> 1 print(average([]))

Cell In[1], line 3, in average(numbers)
      1 def average(numbers):
      2     """میانگینِ یک فهرست عدد."""
----> 3     return sum(numbers) / len(numbers)

ZeroDivisionError: division by zero

این همان چیزی است که traceback نامیده می‌شود. حجمش ترسناک است ولی ساختارش کاملاً منظم است.

۲. از پایین بخوانش#

قاعدهٔ طلایی: traceback را از خطِ آخر شروع کن.

خطِ آخر همیشه دو تکه دارد: نامِ خطا و توضیحش. اینجا ZeroDivisionError: division by zero — تقسیم بر صفر. در نود درصدِ موارد همین یک خط کافی است تا بفهمی چه شده.

اگر نبود، حالا به بالا نگاه کن. آن خط‌های وسط، مسیرِ صدا زدن را نشان می‌دهند، از بیرونی‌ترین به درونی‌ترین:

  • Cell In[2], line 1 — تو print(average([])) را صدا زدی.
  • Cell In[1], line 3, in average — و داخلِ تابعِ average، خطِ ۳، مشکل رخ داد.

فلشِ ----> دقیقاً روی خطی است که در هر مرحله مقصر بوده. پس درونی‌ترین بلوک (پایین‌ترین) جایی است که خطا واقعاً افتاد، و بالاترین بلوک جایی که تو شروعش کردی.

قبل از ادامه، جواب بده: خطای بالا در تابعِ average رخ داد. یعنی باگ حتماً در average است؟ جواب: نه لزوماً. average کارِ خودش را درست انجام داد؛ کسی فهرستِ خالی به آن داد. باگ می‌تواند اینجا باشد یا در صداکننده. traceback می‌گوید کجا منفجر شد، نه اینکه چه کسی فتیله را روشن کرد — و برای همین است که باید کلِ زنجیره را نگاه کنی، نه فقط خطِ آخر را.

۳. شش خطایی که بیشترین وقت را از تازه‌کارها می‌گیرند#

پیام یعنی چه معمولاً به‌خاطرِ
SyntaxError پایتون اصلاً نتوانست جمله را بخواند پرانتز یا گیومهٔ بسته‌نشده، دونقطهٔ جاافتاده
IndentationError تورفتگی سرِ جایش نیست بعد از دونقطه تورفته ننوشتی، یا Tab و فاصله قاطی شده
NameError اسمی به‌کار بردی که تعریف نشده غلطِ املایی، یا سلولی که هنوز اجرا نکرده‌ای
TypeError این کار با این نوع تعریف نشده جمعِ متن و عدد، یا تابعی که None برگردانده
IndexError / KeyError جایگاه یا کلیدی خواستی که وجود ندارد یکی بیشتر از طولِ فهرست، غلطِ املاییِ کلید
ValueError نوع درست است ولی مقدار بی‌معناست int("سلام") — متن هست، ولی عدد نیست

NameError در نوت‌بوک یک دلیلِ مخصوص هم دارد که در هیچ محیطِ دیگری نیست: سلولی که متغیر را می‌سازد هنوز اجرا نشده، یا اجرا شده و بعد پاکش کرده‌ای. فصلِ ۱۰ کاملاً به همین اختصاص دارد.

۴. try / except: خطای پیش‌بینی‌شده را مدیریت کن#

بعضی خطاها باگ‌اند و باید رفع شوند. بعضی‌ها بخشِ عادیِ زندگی‌اند: دادهٔ واقعی خراب دارد و تو از قبل می‌دانی.

samples = ["12", "3.5", "سلام", ""]

for text in samples:
    try:
        value = int(text)
        print(f"«{text}» → {value}")
    except ValueError as error:
        print(f"«{text}» → ValueError: {error}")
«12» → 12
«3.5» → ValueError: invalid literal for int() with base 10: '3.5'
«سلام» → ValueError: invalid literal for int() with base 10: 'سلام'
«» → ValueError: invalid literal for int() with base 10: ''

بلوکِ try کدی است که ممکن است بشکند؛ except ValueError یعنی «اگر دقیقاً این نوع خطا رخ داد، به‌جای توقف این کار را بکن». آن as error خودِ شیء خطا را در دستت می‌گذارد تا بتوانی پیامش را چاپ کنی.

سه چیزِ آموزنده در این خروجی هست. اول اینکه "3.5" هم رد شد — int() عددِ اعشاریِ نوشته‌شده به‌صورتِ متن را نمی‌پذیرد و این تقریباً همیشه غافلگیرکننده است. دوم اینکه رشتهٔ خالی هم ValueError است نه صفر. سوم اینکه حلقه ادامه پیدا کرد — بدونِ try، همان ردیفِ دوم کلِ اجرا را متوقف می‌کرد.

چک کن: هر چهار خط باید چاپ شوند. اگر فقط یک خط دیدی و بعدش traceback، یعنی try را جا انداخته‌ای یا تورفتگی‌اش داخلِ حلقه نیست.

📏 اندازه بگیر: حالا سؤالِ مهم: در دادهٔ واقعی، چند ردیف این‌طوری رد می‌شود؟ اگر سه ردیف از هزار تا خراب باشد یک چیز است؛ اگر ششصد تا خراب باشد، ستون اصلاً آن چیزی نیست که فکر می‌کردی. عادت کن به‌جای رد کردنِ ساکتِ خطاها، بشماری‌شان — و اگر شمارش بزرگ شد، به‌جای مدیریتِ خطا برو داده را نگاه کن. این دقیقاً همان کاری است که در ترمِ ۲ فصلِ ۵ روی دادهٔ کثیفِ واقعی انجام می‌دهیم.

۵. except بدونِ نام: اشتباهی که گران تمام می‌شود#

پایتون اجازه می‌دهد بنویسی except: و هر خطایی را بگیری. تقریباً همیشه اشتباه است:

def risky_average(numbers):
    try:
        return sum(numbers) / len(numbers)
    except:
        return None

print(risky_average([10, 20, 30]))
print(risky_average([]))
print(risky_average("متن، نه فهرست"))
20.0
None
None

سه بار صدا زدیم و هیچ‌وقت خطایی ندیدیم. ولی موردِ سوم یک باگِ واقعی بود: یک متن به‌جای فهرستِ عدد داده شد، sum روی آن TypeError داد، و except: بی‌سروصدا قورتش داد و None برگرداند. حالا برنامه‌ات دارد با یک مقدارِ غلط ادامه می‌دهد و تو خبر نداری.

نسخهٔ درست فقط همان خطایی را می‌گیرد که واقعاً انتظارش را داری:

def safe_average(numbers):
    """میانگین؛ برای فهرستِ خالی None. هر ورودیِ نامعتبرِ دیگری را عمداً رد نمی‌کند."""
    try:
        return sum(numbers) / len(numbers)
    except ZeroDivisionError:
        return None

print(safe_average([10, 20, 30]))
print(safe_average([]))
20.0
None

حالا فهرستِ خالی مدیریت می‌شود، ولی اگر کسی متن بدهد TypeError می‌گیرد و همان‌جا می‌فهمد.

⚠️ مواظب باش: قاعده را حفظ کن: فقط خطایی را بگیر که می‌دانی چرا رخ می‌دهد و می‌دانی با آن چه کنی. هر exceptِ گشاد یک باگ را از امروز به فردا منتقل می‌کند — و فردا پیدا کردنش ده برابر سخت‌تر است، چون دیگر traceback نداری.

🤖 از دستیارت بپرس: یک traceback واقعی را که خودت گرفته‌ای برایش بگذار و بپرس: «این traceback چه می‌گوید؟ خط به خط توضیح بده، ولی کد اصلاحی ننویس.» قیدِ آخر عمدی است. اگر بگذاری کد اصلاح‌شده بدهد، خطا را رفع می‌کنی بدونِ اینکه یاد بگیری؛ دفعهٔ بعد باز هم گیر می‌کنی. هدف این است که خودت بتوانی بخوانی‌اش.

واژه‌های تازهٔ این فصل#

کلمه تلفظ به حروف فارسی یعنی چه
traceback تریس‌بک گزارشِ چندخطیِ خطا؛ مسیرِ صدا زدن از بیرون تا نقطهٔ انفجار
exception اِکسپشِن خطای زمانِ اجرا؛ چیزی که برنامه را متوقف می‌کند
try / except ترای / اِکسپت «این را امتحان کن» / «اگر این خطا شد، این کار را بکن»
ValueError ولیو اِرور نوع درست است ولی مقدار بی‌معناست

تمرین‌ها

اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.

در فصل بعد#

حالا می‌توانی خطا را بخوانی. ولی امروز یک ابزارِ دیگر هم داری که تقریباً همه استفاده‌اش می‌کنند: دستیارِ هوش مصنوعی. فصلِ بعد صریح دربارهٔ آن حرف می‌زند — نه با انکار و نه با تسلیم. یاد می‌گیری چطور از دستیار برای یاد گرفتن استفاده کنی، چه سؤال‌هایی واقعاً کمکت می‌کنند، و چرا کدی که نمی‌توانی خط‌به‌خط توضیحش بدهی، کدِ تو نیست.

به آخر این فصل رسیدی!

اگر ساختی و جواب داد، این دکمه مال توست.