Faragiri

همهٔ زبان‌ها

آموزش پشته فراخوانیکد چطور اجرا می‌شود

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

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

یک سفارش، دو بار لاگ

مسیری که یک فراخوانی می‌رود و همان راه را برمی‌گردد

تابع لاگ در دو ایستگاه مختلف دو جواب مختلف می‌دهد، چون جوابش از پشته می‌آید و نه از ورودی‌اش.

  1. ۰۱خود ماژولسطری که sefaresh را صدا زد و همه چیز از آن شروع شد
  2. ۰۲sefareshکد سفارش را می‌گیرد و اول از همه لاگ می‌زند
  3. ۰۳اولین توقف در sabtیک قاب پایین‌تر را نگاه می‌کند و نام sefaresh را می‌بیند
  4. ۰۴برگشت به sefareshاز همان نویسه بعد از فراخوانی ادامه می‌دهد
  5. ۰۵دومین توقف در sabt، این بار از tamdidهمان تابع، همان سطر، و جواب این بار tamdid است
  6. ۰۶برگشت به خود ماژولقاب‌های زیر رفته‌اند و نام‌هایشان هم با آن‌ها

این نقشه یک سفارش را می‌کشد. سفارش دوم همان راه را می‌رود و قاب‌هایی که می‌سازد قاب‌های تازه‌ای با همان نام‌هاست.

این تابع هیچ ورودی‌ای درباره صداکننده‌اش نمی‌گیرد و نامش را چاپ می‌کند:

  • def sabt(payam):
  • ja = sys._getframe(1).f_code.co_name
  • print(f"[{ja}] {payam}")

صدا زدنش از داخل sefaresh سطر [sefaresh] sefaresh sakhte shod را چاپ می‌کند و صدا زدن همان تابع از داخل tamdid سطر [tamdid] tamdid shod را. جواب از آرگومان نیامد، از پشته آمد: مفسر برای هر فراخوانی که هنوز برنگشته یک قاب نگه می‌دارد، و sys._getframe(1) یک قاب پایین‌تر از قاب خودتان را برمی‌گرداند. هر کتابخانه لاگی که نام ماژول را خودش می‌نویسد همین کار را می‌کند.

فهرست کامل همان لحظه با traceback.extract_stack() بیرون می‌آید و برخلاف انتظار، از قدیمی‌ترین قاب شروع می‌شود: اول <module>، بعد تابع میانی، و آخر از همه تابعی که همین حالا داخلش هستید. برای همین است که در گزارش یک خطا، سطر آخر جایی است که شکسته.

سقف پشته یک عدد است و می‌شود شمردش

یک تابع که فقط خودش را صدا می‌زند و شمارنده‌ای بالا می‌برد، روی پایتون 3.14 روی عدد ۹۹۸ می‌ایستد و RecursionError می‌دهد، در حالی که sys.getrecursionlimit() عدد ۱۰۰۰ را گزارش می‌کند. آن دو قاب گم‌شده قبلاً آنجا بودند: ماژول و خود تابعی که آزمون را شروع کرد. سقف روی قاب‌هاست نه روی فراخوانی‌های شما، و همین است که عدد دقیق روی هر برنامه‌ای فرق می‌کند.

نکته عملی این است که شکست در این عدد تقریباً همیشه یعنی شرط پایان هرگز درست نشده، نه اینکه مسئله واقعاً هزار سطح عمق دارد. تفاوت این دو حالت را تابعی که خودش را صدا می‌زند، و جای شرط پایان می‌گوید.

خطا نمی‌پرد، لایه‌لایه برمی‌گردد

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

نام‌ها با برگشتن قاب می‌روند، ساخته‌هایشان نه

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

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

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

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

در سال ۲۰۲۶ ماژول traceback در کتابخانه استاندارد است و رایگان؛ traceback.print_stack() جای اضافه کردن print در هر سطح را گرفته، که برنامه را درست وسط خواندنش عوض می‌کند. sys._getframe از همان اول در خود مفسر بوده و جای پاس دادن نام صداکننده به‌عنوان یک آرگومان اضافه را گرفته است؛ نامش زیرخط دارد چون تضمینی نیست که در پیاده‌سازی‌های دیگر هم باشد، و در PyPy و در برخی نسخه‌های ماشین مجازی جاوا برای پایتون وجود ندارد. اگر کدتان باید همه‌جا کار کند، معادل امنش inspect.currentframe() است که همان کتابخانه استاندارد دارد.

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

کنار این

بالای این