بین فایل foroush.py و پردازنده چهار اتفاق جدا میافتد، و هر خطایی که تا امروز گرفتهاید متعلق به یکی از آنهاست. اینکه کدامیک، تعیین میکند سراغ نحو بروید یا سراغ نصب یا سراغ داده. این را میشود پرسید، و بیشتر آدمها حدس میزنند.
مرحله اول جدا از بقیه اجرا میشود: python3 -m py_compile foroush.py فایل را کامپایل میکند و اجرا نمیکند. اگر چیزی چاپ نشد فایل سالم است و بایتکدش کنار خودش نشسته؛ پوشه __pycache__ خروجی مرحله کامپایل است و کار ویرایشگر نیست. اگر یک براکت بسته نشده باشد، همینجا و فقط همینجا بیرون میآید:
File "foroush.py", line 4SyntaxError: '[' was never closed
خطای نحوی تنها از مرحله کامپایل بیرون میآید و به داده هیچ ربطی ندارد. فایل فروش در این لحظه هنوز باز نشده و ممکن است اصلاً وجود نداشته باشد.
چهار مرحله، و خطایی که فقط از هرکدام درمیآید
- کامپایل. متن فایل به بایتکد تبدیل میشود.
SyntaxErrorوIndentationErrorفقط از اینجا. - وارد کردن. هر
importبالای فایل اجرا میشود، پیش از سطرهای خودتان.ModuleNotFoundErrorفقط از اینجا. - اجرا. ماشین مجازی بایتکد را دستور به دستور میخواند.
ValueErrorوKeyErrorوUnboundLocalErrorاینجا میافتند، و هر سه به داده یا به مسیری که برنامه رفته وابستهاند. - پردازنده. هر دستور بایتکد خودش چند دستور ماشین است که مفسر پشت سر هم اجرا میکند. اینجا خطا نمیگیرید، اینجا کندی میگیرید.
مرز میان مرحله سوم و چهارم همان جایی است که زبانها از هم جدا میشوند. Rust و Swift و C این راه را تا ته میروند و چیزی که تحویل میدهند دستور ماشین است؛ CPython سر بایتکد میایستد و بقیه راه را هر بار از نو میرود. تفاوت این دو انتخاب، و بهایی که هرکدام دارد، موضوع ترجمهشده از پیش، یا خواندهشده در حین کار است.
py_compile نمیبیند که بستهای نصب نیست
فایلی که سطر اولش import openpyxl است و سطر دومش یک print، بدون هیچ پیامی کامپایل میشود. py_compile نمیتواند بگوید بستهای نصب نیست، چون مرحله وارد کردن را نمیبیند. حالا اجرایش کنید:
File "gozaresh.py", line 1, in <module>ModuleNotFoundError: No module named 'openpyxl'
آن print هرگز چاپ نشد: ModuleNotFoundError پیش از اجرای اولین سطر خود فایل رخ میدهد. برای کسی که از ایران کار میکند این گرانترین مرحله است، چون pip install openpyxl سراغ pypi.org میرود و همان درخواست است که معمولاً به نتیجه نمیرسد. پرچمی که مقصد را عوض میکند --index-url است و در کنارش --trusted-host؛ نشانی آینه چیزی است که هر تیم یک بار برای خودش تنظیم میکند و در فایل pip.conf میگذارد. هرچه در این صفحه است بدون شبکه اجرا میشود، چون همهاش کتابخانه استاندارد است.
جایی که آدمها گیر میکنند همینجاست و کسی اسمش را نمیگذارد. چون نصب کردن دعوا دارد، سال اول در یک ویرایشگر آنلاین یا یک نوتبوک میگذرد، جایی که مرحله دوم مال کس دیگری است و از پیش انجام شده. اولین فایلی که باید سرد، روی ماشین خودشان، اجرا شود روی چیزی میشکند که هرگز جزو زبان نبوده، و نتیجهای که میگیرند این است که برنامهنویس بدی هستند. نیستند. به مرحلهای رسیدهاند که کسی نگفته بود مرحله است.
کامپایلر پیش از اجرا تصمیم گرفته هر نام محلی است یا سراسری
این پنج سطر کامپایل میشوند و بعد میشکنند:
takhfif = 0def gheymat_nahaei(mablagh):if mablagh > 500000:takhfif = 20000return mablagh - takhfif
فراخوانی با مبلغ صد و هشتاد و پنج هزار تومان با UnboundLocalError متوقف میشود، در حالی که takhfif در سطر اول همین فایل مقدار گرفته است. جواب در بایتکد است: python3 -c "import takhfif, dis; dis.dis(takhfif.gheymat_nahaei)". دو دستور از آن فهرست تمام ماجرا را میگویند، LOAD_FAST_BORROW 0 (mablagh) برای پارامتری که حتماً مقدار دارد و LOAD_FAST_CHECK 1 (takhfif) برای نامی که ممکن است نداشته باشد. هر دو محلیاند. یک انتساب در هر جای تابع، آن نام را در کل تابع محلی میکند، و این تصمیم در مرحله اول گرفته شده بود، وقتی هنوز هیچ چیز اجرا نشده بود. UnboundLocalError یعنی نام محلی است و هنوز مقدار نگرفته.
نام خود دستورها بین نسخهها عوض میشود و همین یکی دو سال هم عوض شده است، پس آنها را از خروجی dis همان مفسری بخوانید که کدتان روی آن اجرا میشود، نه از آموزشی که چهار سال پیش نوشته شده. چیزی که عوض نمیشود این است که تصمیم محلی یا سراسری بودن، پیش از اجرا و برای کل تابع گرفته میشود.
یک لحظه
چهار قابی که وقتی فایل فروش ایستاد هنوز زنده بودند
هر فراخوانی زیر فراخوانی بالای خودش را نگه داشته، و هیچکدام تا وقتی بالایی برنگردد نمیتواند برگردد.
- tamiz(satr)int به یک ستون خالی رسید و همینجا خطا دادسطر ۲
- pak(satrha)برای هر سطر یک بار tamiz را صدا میزندسطر ۵
- gozaresh(masir)فایل را خوانده و سطرها را به pak داده بودسطر ۱۰
- خود ماژولفراخوانیای که همه اینها را شروع کردسطر ۱۲
traceback همین فهرست را وارونه چاپ میکند، از قدیمیترین قاب، و به همین دلیل سطر بهدردبخور آخرین سطر است.
قابها: آنچه در لحظه توقف هنوز برنگشته است
وقتی یکی از سطرهای فایل فروش ستون مبلغ خالی داشته باشد، برنامه با چهار سطر File میایستد: <module> در سطر ۱۲، gozaresh در سطر ۱۰، pak در سطر ۵ و tamiz در سطر ۲. هرکدام یک قاب است، و قابهای زنده فقط فراخوانیهایی هستند که هنوز برنگشتهاند؛ تابعی که کارش تمام شده در این فهرست نیست، هر قدر هم که کار کرده باشد. traceback قدیمیترین قاب را اول چاپ میکند، پس سطر آخر جایی است که شکسته. عادت خواندن از پایین، ارزانترین مهارت این صفحه است و در برنامه چطور یادش میماند کجا بوده تا ته میرود.
نامهای داخل هر قاب با برگشتن آن از بین میروند، اما چیزی که ساختهاند لزوماً نه: فهرستی که pak ساخته و برگردانده، بعد از رفتن قاب هم زنده است، چون هرگز داخل قاب نبود و فقط یک ارجاع به آن آنجا بود. آن جای دیگر موضوع جایی که مقدارها زندگی میکنند است.
ابزار، و سالی که این حرف درست است
در سال ۲۰۲۶ نسخه جاری پایتون 3.14 است و دلیل بهروزرسانی، پیامهای خطاست نه سرعت. SyntaxError: '[' was never closed که بالاتر دیدید سطر درست را نشان میدهد؛ تا پیش از پایتون 3.10 همین فایل «نحو نامعتبر» را روی سطری گزارش میکرد که هیچ ایرادی نداشت، و یک نسل تازهکار را فرستاد به تماشای کد سالم. py_compile و dis هر دو در کتابخانه استاندارد هستند و جای حدس زدن را گرفتهاند: یکی جای «اجرا کنیم ببینیم اصلاً پارس میشود»، دیگری جای «این سطر لابد گران است». هیچکدام به شبکه نیاز ندارند، که در ایران خودش یک ویژگی است. pip از حدود سال ۲۰۱۱ جانشین easy_install شد و تنها ابزار این صفحه است که بدون اتصال کاری از پیش نمیبرد.
هر چهار مرحله بالا فرض کردهاند یک رشته اجرا در کار است و کسی وسط کار چیزی را عوض نمیکند. وقتی دو کار همزمان پیش بروند، مرحله سوم دیگر یک صف نیست و خطاهایی پیدا میشود که با اجرای دوباره ناپدید میشوند: دو کار همزمان، و باگهایی که فقط از همین میآیند. و اگر برنامهای که هیچ خطایی نمیدهد کند است، جواب تقریباً هیچوقت در مرحله چهارم نیست، بلکه در تعداد دفعاتی است که مرحله سوم اجرا میشود، که الگوریتمها و پیش از آن سازههای اصلی برنامه تعیینش میکنند. همهشان زیر ایدههایی که با عوض شدن زبان از بین نمیروند ایستادهاند.