📰 مقالات › 📊 تحلیل › ایمپورت تنبل در پایتون؛ چگونه…
📝 مقاله 📊 تحلیل

ایمپورت تنبل در پایتون؛ چگونه PEP 810 هزینه پنهان import را کاهش می‌دهد؟

از PEP 690 تا PEP 810 — چرا Python 3.15 ایمپورت تنبل را به نحو رسمی زبان آورد، چه چیزی را بهتر می‌کند و مهاجرت ایمن چگونه است

ایمپورت تنبل در پایتون — PEP 810 و Python 3.15
pourya_sh1386
پوریا
نویسنده
📅 ۱۴۰۵/۰۷/۲۱
⏱ 22 دقیقه
👁 55
❤️ 4
📋 فهرست مطالب ▼

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

ایمپورت تنبل پایتون (Lazy Import) راهی برای به تعویق انداختن این هزینه است؛ نه با حذف قابلیت‌های زبان، بلکه با تغییر زمان بارگذاری ماژول‌ها. قابلیت جدیدی که در Python 3.15 با پیشنهاد PEP 810 معرفی شده، این ایده را به بخشی از نحو رسمی زبان تبدیل می‌کند.

تفاوت اصلی در یک کلمه خلاصه می‌شود: lazy. اما پشت همین کلمه، بحثی چندساله درباره سرعت، سازگاری، رفتار قابل پیش‌بینی و معماری درونی CPython قرار دارد.

💡
درباره نمونه‌کدها

نمونه‌کدهایی که واژهٔ lazy دارند فقط روی Python 3.15 یا جدیدتر اجرا می‌شوند؛ در نسخه‌های قدیمی‌تر SyntaxError می‌گیرید. نمونه‌های این مقاله روی Python 3.15 اجرا و بررسی شده‌اند.

۱. هر import یک صورت‌حساب دارد

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

برای مثال، این کد ساده را در نظر بگیرید:

🐍 PYTHON — برنامه‌ای که فقط یک پیام چاپ می‌کند
import argparse import json import pathlib import pandas as pd import matplotlib.pyplot as plt def main(): print("Hello, Python!") if __name__ == "__main__": main()

برنامه فقط یک پیام چاپ می‌کند. نه به pandas نیاز دارد، نه نمودار می‌سازد و نه فایلی می‌خواند. با این حال، در حالت معمول، همه این وابستگی‌ها پیش از اجرای main() بارگذاری می‌شوند.

مشکل فقط زمان اجرای دستورهای import نیست. هر ماژول ممکن است ماژول‌های دیگری را وارد کند و آن‌ها نیز به وابستگی‌های بیشتری نیاز داشته باشند. به این ترتیب، یک دستور ساده در فایل اصلی می‌تواند زنجیره‌ای طولانی از بارگذاری‌ها را آغاز کند.

این هزینه را می‌توان مالیات ایمپورت (Import Tax) نامید: زمانی که برنامه برای آماده‌سازی قابلیت‌هایی می‌پردازد که شاید هرگز از آن‌ها استفاده نکند.

ایمپورت تنبل این منطق را معکوس می‌کند. به جای آنکه همه وابستگی‌ها را در آغاز اجرا آماده کنیم، فقط وابستگی‌هایی را بارگذاری می‌کنیم که مسیر واقعی اجرای برنامه به آن‌ها نیاز دارد.

۲. چرا چند میلی‌ثانیه هم اهمیت دارد؟

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

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

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

داده‌ها چه می‌گویند؟

مطالعهٔ «The Import Tax» (مالیات ایمپورت)، نوشتهٔ Trinath Sai Subhash Reddy Pittala، که در ۲ سپتامبر ۲۰۲۶ در arXiv منتشر شد (arXiv:2609.02753)، هزینهٔ ایمپورت ۵۰۰ بستهٔ پرکاربرد PyPI را طی پنج سال، با نمونه‌برداری فصلی، روی شش نسخه از CPython (۳٫۹ تا ۳٫۱۴) و دو پلتفرم بررسی کرده است. این پژوهش ۶۳٬۴۳۱ اندازه‌گیری را در بر می‌گیرد و نشان می‌دهد که توزیع هزینهٔ ایمپورت به‌شدت کج است: میانگین، به دلیل رشد تعداد محدودی از بسته‌های سنگین، سریع‌تر از میانه افزایش یافته است.

هزینه ایمپورت در اعداد

شاخصمقدار
میانه زمان ایمپورت بسته‌هاکمتر از ۶ ms
صدک ۹۹ زمان ایمپورت۳۵۴ ms
نخستین ایمپورت پس از نصب (کامپایل بایت‌کد) نسبت به ایمپورت‌های بعدی۳ تا ۲۲ برابر
اختلاف هزینه ایمپورت زیرماژول با ایمپورت سطح بالاتا ۲۹۴ برابر
💡
درباره این اعداد

منبع: پژوهش «The Import Tax». این اعداد به مجموعه اندازه‌گیری‌شده و شرایط آزمایش مربوط‌اند و تضمین عملکرد همه برنامه‌ها نیستند.

این اعداد یک نکته مهم را آشکار می‌کنند: میانگین و میانه می‌توانند دو تصویر متفاوت از اکوسیستم پایتون ارائه دهند. میانه زمان ایمپورت بسته‌ها سالانه حدود ۱٫۶ تا ۲٫۴ درصد افزایش یافته، در حالی که رشد میانگین در همان مطالعه حدود ۱۱ تا ۱۳ درصد در سال بوده است. به بیان دیگر، مسئله فقط بزرگ‌تر شدن تدریجی همه بسته‌ها نیست؛ بخشی از وابستگی‌ها با سرعت بیشتری سنگین می‌شوند.

نخستین ایمپورت پس از نصب نیز با ایمپورت‌های بعدی یکسان نیست. اختلاف ۳ تا ۲۲ برابری که در این پژوهش گزارش شده، به اولین ایمپورت یک بسته پس از نصب مربوط است؛ زمانی که هنوز فایل بایت‌کد ذخیره‌شده (pyc) وجود ندارد و مفسر باید کد منبع را بخواند و کامپایل کند. پس از آن، همان بسته با بایت‌کد آماده بارگذاری می‌شود و در یک فرایند نیز ماژولی که یک بار بارگذاری شده، در sys.modules می‌ماند و ایمپورت دوباره‌اش ارزان است. به همین دلیل، این ضریب را باید نتیجهٔ شرایط آزمایش دانست، نه یک ضریب ثابت برای تمام برنامه‌ها.

نکته آخر درباره زیرماژول‌هاست. اندازه‌گیری صرفاً روی import package ممکن است هزینه واقعی استفاده از package.submodule را نشان ندهد. وقتی زیرماژولی به زنجیره وابستگی اضافه می‌شود، مجموعه‌ای از ماژول‌های دیگر نیز ممکن است وارد شوند؛ بنابراین، اندازه‌گیری باید مسیر واقعی استفاده از بسته را پوشش دهد.

۳. از PEP 690 تا PEP 810؛ دو فلسفه برای یک مسئله

برای درک طراحی جدید، باید به دو پیشنهاد رسمی پایتون نگاه کنیم. PEP مخفف Python Enhancement Proposal است؛ سندی که یک قابلیت تازه یا تغییر در زبان را تعریف می‌کند، دلایل آن را توضیح می‌دهد و مسیر بررسی و تصمیم‌گیری درباره آن را مشخص می‌کند.

PEP 690: اگر همه‌چیز تنبل شود چه؟

در سال ۲۰۲۲، گرمان مندز براوو و کارل مایر پیشنهاد PEP 690 را مطرح کردند. ایده اصلی این بود که ایمپورت‌های واجد شرایط، به صورت پیش‌فرض در یک حالت سراسری به تعویق بیفتند؛ به این معنا که صرف اجرای دستور import الزاماً به بارگذاری فوری ماژول منجر نشود. بارگذاری واقعی زمانی اتفاق بیفتد که برنامه به ماژول یا یکی از نام‌های واردشده از آن نیاز پیدا کند.

مزیت این روش روشن بود: توسعه‌دهنده لازم نبود صدها دستور ایمپورت را دستی تغییر دهد. اگر یک برنامه فقط بخشی از وابستگی‌هایش را استفاده می‌کرد، بخش‌های استفاده‌نشده بارگذاری نمی‌شدند. در متن‌ها و گزارش‌های آن زمان آمده بود که پیاده‌سازی مرجع این پیشنهاد (در Cinder، فورک پایتون در Meta) روی ابزارهای خط فرمان واقعی تا ۷۰ درصد بهبود زمان شروع و تا ۴۰ درصد کاهش مصرف حافظه داشته است.

اما این طراحی یک مشکل بنیادی داشت: رفتار کدی که ظاهراً تغییر نکرده بود، می‌توانست عوض شود.

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

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

PEP 690 در نهایت رد شد. دلیل اصلی این نبود که ایمپورت تنبل سودمند نیست؛ مشکل این بود که تغییر سراسری معنای import، با وجود مزایای عملکردی، خطر ناسازگاری و دشواری عیب‌یابی را به اکوسیستم تحمیل می‌کرد.

PEP 810: انتخاب صریح به جای تغییر پنهان

PEP 810 در سال ۲۰۲۵ مطرح شد و وضعیت نهایی آن در نوامبر همان سال به تصویب رسید. این پیشنهاد قابلیت ایمپورت تنبل صریح را برای Python 3.15 تعریف می‌کند. برخلاف PEP 690، توسعه‌دهنده باید در محل موردنظر، واژه lazy را بنویسد.

🐍 PYTHON — نحو lazy
lazy import json lazy from pathlib import Path

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

ویژگیPEP 690PEP 810
رویکرد اصلیایمپورت تنبل سراسری و ضمنیایمپورت تنبل صریح
کنترل رفتارحالت سراسری و استثناهاواژه lazy در محل ایمپورت
قابلیت مشاهده در کدمحدودترروشن و مستقیم
خطر تغییر ناخواسته رفتاربیشترمحدودتر به ایمپورت‌های انتخاب‌شده
هدف اصلیحذف گسترده هزینه‌های بلااستفادهبهینه‌سازی تدریجی و قابل‌کنترل
وضعیتردشدهپذیرفته‌شده برای Python 3.15

البته این تفاوت به معنای آن نیست که PEP 810 هیچ سازوکاری برای کنترل سراسری ندارد. این پیشنهاد امکان تنظیم حالت ایمپورت‌های تنبل و استفاده از فیلتر را نیز فراهم می‌کند؛ اما نقطه شروع آن، انتخاب صریح و محلی است.

💡
اصل سازگاری در PEP 810

بخش «Rationale» این PEP تأکید می‌کند که تا وقتی توسعه‌دهنده صراحتاً آن را انتخاب نکند، هیچ چیز تغییر نمی‌کند: ایمپورت تنبل کاملاً اختیاری (opt-in) است.

این همان فلسفه‌ای است که PEP 810 را برای استفاده تدریجی مناسب‌تر می‌کند. می‌توان ابتدا چند ایمپورت سنگین را بهینه کرد، نتیجه را سنجید و سپس دامنه تغییر را گسترش داد؛ بدون آنکه تمام وابستگی‌های برنامه ناگهان وارد یک مدل اجرایی تازه شوند.

۴. در Python 3.15 واقعاً چه اتفاقی می‌افتد؟

در Python 3.15، lazy یک واژه کلیدی نرم است. واژه کلیدی نرم فقط در جایگاه‌های نحوی مشخص معنای ویژه دارد و در سایر موقعیت‌ها همچنان می‌تواند شناسه‌ای عادی باشد.

نحو جدید دو شکل اصلی دارد:

🐍 PYTHON — lazy import
lazy import json

و:

🐍 PYTHON — lazy from
lazy from json import dumps, loads

در شکل نخست، نام json به یک شیء واسط تنبل متصل می‌شود. خود ماژول هنوز بارگذاری نشده است. وقتی برنامه برای نخستین بار به json دسترسی پیدا کند، مفسر ایمپورت را انجام می‌دهد و نام را به شیء واقعی ماژول تبدیل می‌کند.

در شکل دوم، نام‌های dumps و loads هرکدام اتصال تنبل خودشان را دارند. نخستین دسترسی به یکی از آن‌ها ماژول json را بارگذاری می‌کند، اما لازم نیست تمام نام‌های واردشده در همان لحظه به شیء نهایی تبدیل شوند.

یک مثال قابل اجرا

این نمونه برای Python 3.15 نوشته شده است:

🐍 PYTHON — نمونهٔ اجرایی (Python 3.15)
import sys lazy import json print("json" in sys.modules) # False payload = json.dumps({"ok": True}) print("json" in sys.modules) # True print(payload)

در خط نخست، json هنوز در sys.modules ثبت نشده است. با دسترسی به json.dumps، بارگذاری واقعی اتفاق می‌افتد و نتیجه در sys.modules قابل مشاهده می‌شود.

این رفتار با ایمپورت عادی فرق دارد، اما پس از بارگذاری، کد می‌تواند از شیء واقعی ماژول استفاده کند. هدف این نیست که برنامه همیشه با یک پروکسی کار کند؛ هدف آن است که پروکسی فقط تا زمانی وجود داشته باشد که بارگذاری واقعی ضرورت پیدا نکرده است.

چرا lazy را نمی‌توان همه‌جا نوشت؟

نحو PEP 810 عمداً محدود است. استفاده از lazy در جایگاه‌های زیر خطای نحوی ایجاد می‌کند:

  • داخل بدنه تابع یا متد؛
  • داخل بدنه کلاس؛
  • داخل بلوک try، از جمله ساختارهای مدیریت استثنا؛
  • برای ایمپورت ستاره‌ای؛
  • برای ایمپورت‌های __future__.

برای نمونه، این کد معتبر نیست:

🐍 PYTHON — نامعتبر: lazy داخل تابع
def load_data(): lazy import json

و این یکی نیز معتبر نیست:

🐍 PYTHON — نامعتبر: ایمپورت ستاره‌ای
lazy from json import *

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

محدودیت بلوک‌های try نیز به مسئله زمان بروز خطا مربوط است. اگر برنامه می‌خواهد خطای ImportError را در همان محل ایمپورت مدیریت کند، به تعویق انداختن خطا تا زمانی دیگر می‌تواند معنای کنترل جریان را تغییر دهد.

آیا هر ایمپورت تنبل واقعاً تنبل می‌ماند؟

خیر. ایمپورت تنبل در عمل به دو مرحله تقسیم می‌شود: ایجاد اتصال اولیه و تبدیل آن به شیء واقعی، که در مستندات CPython از آن با اصطلاح reification یاد می‌شود.

مفسر در مرحله اول یک شیء از نوع types.LazyImportType ایجاد می‌کند. با نخستین دسترسی مناسب، شیء واقعی ماژول یا نام موردنظر را بارگذاری می‌کند و اتصال را جایگزین می‌سازد. پس از این مرحله، هزینه اضافی نباید به صورت دائمی بر تمام دسترسی‌های بعدی تحمیل شود؛ مفسر تطبیقی پایتون می‌تواند دستورهای بایت‌کد را نیز برای دسترسی مستقیم به فضای نام بهینه کند.

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

۵. پروکسی، فضای نام و دلیل اهمیت جزئیات پیاده‌سازی

ایمپورت تنبل فقط مسئله‌ای مربوط به نحو زبان نیست. یکی از تصمیم‌های مهم در PEP 810 این است که مفسر چگونه یک نام واردشده را پیش از بارگذاری واقعی نمایش دهد.

چرا یک دیکشنری معمولی کافی نیست؟

هر ماژول پایتون فضای نامی دارد که نام متغیرها، توابع، کلاس‌ها و وابستگی‌های آن را نگه می‌دارد. این فضا معمولاً از طریق module.__dict__ در دسترس است و در سطح زبان، دیکشنری فضای نام با سازوکارهای بهینه‌شده مفسر ارتباط نزدیکی دارد.

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

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

PEP 810 راه دیگری را انتخاب می‌کند: پروکسی تنبل در فضای نام قرار می‌گیرد و هنگام استفاده، با شیء واقعی جایگزین می‌شود. در نتیجه، نیازی نیست رفتار عمومی دیکشنری‌های پایتون برای پشتیبانی از ایمپورت تنبل تغییر کند.

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

این تصمیم یک مزیت معماری دارد: پیچیدگی ایمپورت تنبل در همان زیرسامانه‌ای باقی می‌ماند که مسئول ایمپورت است، نه اینکه به ساختار داده‌ای فراگیر مانند dict منتقل شود.

چرا importlib.util.LazyLoader کافی نبود؟

پایتون پیش از PEP 810 نیز ابزار استانداردی برای بارگذاری تنبل داشت: importlib.util.LazyLoader. این ابزار بارگذاری و اجرای ماژول را تا زمان دسترسی به ویژگی‌های آن به تعویق می‌اندازد، اما استفاده از آن معمولاً به ساخت دستی مشخصات ماژول، loader و مدیریت sys.modules نیاز دارد.

محدودیت بنیادی‌تر آن به شکل ایمپورت مربوط است. در دستور زیر:

🐍 PYTHON — ایمپورت معمولی از یک ماژول
from foo import a, b

پایتون باید نام‌های a و b را از ماژول foo استخراج کند. این دسترسی به ویژگی‌های ماژول می‌تواند بارگذاری آن را همان موقع فعال کند؛ بنابراین، LazyLoader به‌تنهایی نمی‌تواند این دستور را مانند یک ایمپورت تنبل واقعی، بدون بارگذاری فوری ماژول اجرا کند.

PEP 810 این مشکل را در سطح زبان و مفسر حل می‌کند. در دستور lazy from foo import a, b، نام‌های واردشده ابتدا به پروکسی‌های تنبل متصل می‌شوند. نخستین استفاده از یکی از نام‌ها، ماژول را بارگذاری می‌کند و همان نام را به شیء واقعی تبدیل می‌کند. نام دیگر نیز در نخستین استفاده خودش حل می‌شود، هرچند ماژول از قبل بارگذاری شده باشد.

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

۶. پیش از PEP 810 چه راهکارهایی داشتیم؟

PEP 562 و ماژول __getattr__

در Python 3.7، پیشنهاد PEP 562 امکان تعریف __getattr__ در سطح ماژول را معرفی کرد. این قابلیت به توسعه‌دهنده اجازه می‌دهد وقتی کدی به نامی دسترسی پیدا می‌کند که در فضای نام ماژول وجود ندارد، تابع ویژه __getattr__ را برای حل آن نام اجرا کند.

از این قابلیت می‌توان برای ساخت یک API با بارگذاری تنبل استفاده کرد. برای نمونه، یک بسته می‌تواند زیرماژول‌های خود را تنها هنگام درخواست کاربر وارد کند:

🐍 PYTHON — ماژول __getattr__ (PEP 562)
# mypackage/__init__.py from importlib import import_module __all__ = ["heavy"] def __getattr__(name): if name == "heavy": module = import_module(".heavy", __name__) globals()[name] = module return module raise AttributeError( f"module {__name__!r} has no attribute {name!r}" )

اکنون کد مصرف‌کننده می‌تواند بنویسد:

🐍 PYTHON — استفاده از بسته
import mypackage mypackage.heavy

در نخستین دسترسی به heavy، زیرماژول بارگذاری می‌شود. سپس با ذخیره کردن نتیجه در globals()، دسترسی‌های بعدی از اجرای دوباره منطق __getattr__ بی‌نیاز می‌شوند.

این روش هنوز برای طراحی API عمومی بسته‌ها مفید است؛ به‌ویژه زمانی که بسته می‌خواهد زیرماژول‌هایش را بدون بارگذاری هم‌زمان همه آن‌ها در دسترس قرار دهد. اما __getattr__ فقط زمانی فراخوانی می‌شود که نام در فضای نام معمول ماژول پیدا نشود. این قابلیت جایگزین عمومی معنای import نیست و رفتار آن با پروکسی‌های داخلی PEP 810 تفاوت دارد.

کتابخانه‌های بارگذاری تنبل و الگوی TensorFlow

پیش از قابلیت رسمی زبان، کتابخانه‌های مختلف راهکارهای خود را پیاده‌سازی کرده بودند. بستهٔ lazy_loader (بر پایهٔ SPEC 1 در اکوسیستم علمی پایتون) انتشار تنبل زیرماژول‌ها را برای بسته‌هایی مانند scikit-image و NetworkX ساده می‌کند. در برخی پروژه‌ها نیز از الگوهایی با نام TensorFlow LazyLoader استفاده شده است تا بارگذاری زیرسامانه‌های سنگین به زمان نیاز واقعی موکول شود.

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

اما پیاده‌سازی این رفتار در سطح کتابخانه با پیاده‌سازی آن در سطح زبان یکسان نیست. راهکارهای کتابخانه‌ای ممکن است به پروکسی‌های سفارشی، __getattr__، هوک‌های ایمپورت یا پیکربندی اضافی متکی باشند. PEP 810 تلاش می‌کند این رفتار را با نحو استاندارد و سازوکار یکپارچه مفسر فراهم کند.

۷. آیا نتایج واقعی، ارزش این پیچیدگی را ثابت می‌کنند؟

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

تجربه Instagram و Meta

تیم‌های Meta سال‌ها پیش از PEP 810 از ایمپورت تنبل در مقیاس بزرگ استفاده می‌کردند. به گزارش LWN، کارل مایر دربارهٔ Instagram گفته بود که ایمپورت تنبل در آن به‌صورت سراسری اعمال شده و فقط پنج ماژول از آن مستثنا شده‌اند. اعدادی هم که پیش‌تر دربارهٔ PEP 690 گفته شد (تا ۷۰ درصد بهبود زمان شروع و تا ۴۰ درصد کاهش حافظه) به ابزارهای خط فرمان واقعی در Cinder مربوط‌اند و نباید به همهٔ برنامه‌ها تعمیم داده شوند.

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

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

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

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

هوگو ون کِمنِید (Hugo van Kemenade) در یادداشتی دستور pypistats --help را با ابزار hyperfine اندازه گرفته است. با انتقال ایمپورت‌ها به داخل توابع (ایمپورت درون‌خطی)، زمان اجرا از ۴۶ به ۳۵ میلی‌ثانیه رسید، یعنی حدود ۱٫۳۱ برابر سریع‌تر. با نگه داشتن همهٔ ایمپورت‌ها در بالای فایل و فعال کردن حالت تنبل سراسری، زمان از ۱۰۴ به ۳۶ میلی‌ثانیه رسید، یعنی حدود ۲٫۹۲ برابر سریع‌تر.

منبع: یادداشت «lazy imports» در hugovk.dev (۲۰۲۵). این اعداد به همان دستور، همان دستگاه و همان شرایط اجرا مربوط‌اند.

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

مطالعه اکوسیستم PyPI در سال ۲۰۲۶

پژوهش «The Import Tax» به جای تمرکز بر یک پروژه، ۵۰۰ بسته پرکاربرد PyPI را در چندین دوره زمانی و شش نسخه از CPython بررسی کرده است. یافته‌های آن نشان می‌دهد که حالت سراسری ایمپورت تنبل، دستورهای ایمپورت را تقریباً بدون هزینه بارگذاری فوری اجرا می‌کند، اما در آزمایش ۴۱۴ بسته سطح بالای بررسی‌شده، ۸ بسته با این حالت دچار ناسازگاری شدند.

این نتیجه هم‌زمان دو پیام دارد. نخست اینکه حذف هزینه ایمپورت می‌تواند سود بزرگی داشته باشد. دوم اینکه حتی در یک نمونه بزرگ، نمی‌توان فرض کرد همه کتابخانه‌ها بدون بررسی با معنای جدید ایمپورت سازگارند.

۸. چه چیزهایی باید همچنان eager بمانند؟

مهم‌ترین خطر ایمپورت تنبل، کاهش سرعت نیست؛ تغییر زمان اجرای کدهایی است که هنگام ایمپورت اثر جانبی دارند.

در پایتون، ایمپورت یک ماژول فقط تعریف تابع و کلاس نیست. بدنه ماژول در زمان بارگذاری اجرا می‌شود و ممکن است وضعیت برنامه را تغییر دهد. اگر اجرای این بدنه به تعویق بیفتد، اثرهای جانبی آن نیز به زمان دیگری منتقل می‌شوند.

ثبت افزونه‌ها و رجیستری‌ها

فرض کنید برنامه یک رجیستری ساده برای افزونه‌ها دارد:

🐍 PYTHON — registry.py
# registry.py plugins = {} def register(name, plugin): plugins[name] = plugin

هر افزونه هنگام ایمپورت خودش را ثبت می‌کند:

🐍 PYTHON — csv_plugin.py
# csv_plugin.py from registry import register class CSVPlugin: pass register("csv", CSVPlugin)

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

برای دیدن همین مشکل، یک فایل اصلی بسازید که رجیستری را ایمپورت می‌کند ولی افزونه را تنبل ایمپورت می‌کند:

🐍 PYTHON — main.py (Python 3.15)
import registry lazy import csv_plugin print(registry.plugins) # {}

چون هیچ‌جا به نام csv_plugin دسترسی پیدا نشده، افزونه هرگز ثبت نمی‌شود و رجیستری خالی می‌ماند. اگر همین فایل به جای lazy import از import csv_plugin استفاده کند، رجیستری شامل افزونهٔ csv خواهد بود.

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

تغییر وضعیت سراسری و دست‌کاری sys.modules

برخی ماژول‌ها هنگام ایمپورت، متغیرهای سراسری را تغییر می‌دهند، مسیر جست‌وجوی ماژول‌ها را دست‌کاری می‌کنند، تنظیمات محیطی را تغییر می‌دهند یا به sys.modules وابسته‌اند.

برای مثال، یک چارچوب ممکن است فرض کند ایمپورت یک ماژول، چند کلاس را در یک رجیستری ثبت کرده است. یا ممکن است کدی پیش از ایمپورت بعدی، حضور یک ماژول مشخص را در sys.modules بررسی کند. در هر دو حالت، به تعویق انداختن بارگذاری می‌تواند فرض‌های برنامه را نقض کند.

متاکلاس‌ها و __init_subclass__

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

اگر ماژول حاوی این کلاس‌ها دیرتر بارگذاری شود، فرایند ثبت یا آماده‌سازی نیز دیرتر انجام خواهد شد. بنابراین، ایمپورت تنبل یک ماژول فقط زمان ساخت توابع آن را تغییر نمی‌دهد؛ ممکن است زمان ساخت کلاس‌ها و اجرای منطق وابسته به آن‌ها را نیز جابه‌جا کند.

ترتیب ایمپورت و زمان بروز خطا

دو تغییر دیگر را نیز باید جدی گرفت.

نخست، خطاهایی مانند ImportError ممکن است به جای محل ایمپورت، هنگام نخستین استفاده از نام ظاهر شوند. دوم، ترتیب واقعی بارگذاری ماژول‌ها ممکن است با ترتیب ظاهری دستورهای ایمپورت در فایل متفاوت شود.

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

۹. ابزارهای مهاجرت: از حدس تا تحلیل ایستا

مهاجرت به ایمپورت تنبل نباید با افزودن کورکورانه lazy به همه دستورهای ایمپورت آغاز شود. راه بهتر، شناسایی ایمپورت‌های پرهزینه، بررسی اثرهای جانبی و اندازه‌گیری رفتار پیش و پس از تغییر است.

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

Lifeguard؛ یافتن الگوهای ناسازگار

پروژه Lifeguard از Meta یک تحلیل‌گر ایستا برای شناسایی الگوهایی است که ممکن است با ایمپورت تنبل ناسازگار باشند. این ابزار ساختار کد را بررسی می‌کند و به دنبال مواردی مانند ثبت افزونه، تغییر وضعیت سراسری، دست‌کاری sys.modules و فراخوانی‌هایی می‌گردد که اثر جانبی آن‌ها ممکن است به زمان ایمپورت وابسته باشد.

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

بنابراین، گزارش Lifeguard را باید راهنمای بررسی دانست، نه تضمین ریاضی سازگاری. هدف آن کاهش هزینه بررسی دستی در پروژه‌های بزرگ است. بر اساس README مخزن، Lifeguard هنوز در مرحلهٔ بتاست و تحلیل آن محافظه‌کارانه است: ماژولی که نتوان ایمن بودنش را اثبات کرد، ناایمن علامت‌گذاری می‌شود و برخی ماژول‌های سازگار هم ممکن است هشدار بگیرند.

lazysafe؛ سنجش ایمنی و تفاوت رفتار

ابزار lazysafe (یک پروژهٔ متن‌باز در GitHub) رویکردی ترکیبی دارد: دستور analyze ایمپورت‌های سطح بالا را در چهار دستهٔ SAFE، RISKY، UNSAFE و UNKNOWN طبقه‌بندی می‌کند؛ probe ماژول‌ها را در یک فرایند جدا برای بررسی اثرهای جانبی ایمپورت می‌کند؛ apply --dry-run بازنویسی‌ها را فقط پیش‌نمایش می‌دهد؛ و verify یک دستور را یک بار به شکل عادی و یک بار به شکل تنبل اجرا می‌کند و خروجی‌ها را بایت‌به‌بایت مقایسه می‌کند.

مستندات این ابزار وعدهٔ سرعت نمی‌دهد: دستور measure داده‌های پروفایل ایمپورت را جمع می‌کند، نه زمان کل اجرای دستور. بنابراین سود واقعی را باید خودتان با اندازه‌گیری زمان شروع و تعداد ماژول‌های بارگذاری‌شده بسنجید.

برای نمونه، می‌توان ابتدا پروژه را تحلیل کرد، سپس پروفایل ایمپورت گرفت و در پایان رفتار اجرای عادی و تنبل را مقایسه کرد:

💻 BASH — گردش کار lazysafe
lazysafe analyze src/ lazysafe measure --runs 3 -- python -m myapp lazysafe verify -- python -m myapp --help

این دستورها نمونه‌ای از گردش کار ابزار هستند. اجرای خود lazysafe به Python 3.11 یا جدیدتر نیاز دارد، اما برنامهٔ هدف باید روی Python 3.15 یا جدیدتر اجرا شود؛ پیش از استفاده در محیط واقعی، سازگاری نسخهٔ نصب‌شده و گزینه‌های آن را بررسی کنید. دستور verify خروجی دو اجرا را بایت‌به‌بایت مقایسه می‌کند، پس آن را برای دستوری به کار ببرید که خروجی قطعی دارد (مثلاً --help)، نه برای خروجی‌هایی که زمان یا مسیر موقت در آن‌ها چاپ می‌شود.

pylazyimports؛ راهکار برای نسخه‌های قدیمی‌تر

بستهٔ pylazyimports (که با نام lazyimports ایمپورت می‌شود) امکان پیاده‌سازی الگوهای ایمپورت تنبل را در نسخه‌های قدیمی‌تر پایتون فراهم می‌کند. این پروژه از یک مدیر زمینه (context manager) استفاده می‌کند تا بارگذاری ماژول‌ها یا اشیای مشخص را به تعویق بیندازد.

این ابزار با قابلیت داخلی PEP 810 یکسان نیست. به‌کارگیری آن به نسخه بسته، شیوه پیکربندی و الگوی استفاده وابسته است؛ بنابراین، نباید فرض کرد که هر رفتار آن دقیقاً با نحو رسمی Python 3.15 مطابقت دارد.

ابزارمسئله اصلیکاربرد مناسب
Lifeguardتحلیل ایستای الگوهای ناسازگاربررسی پروژه‌های بزرگ پیش از مهاجرت
lazysafeتحلیل ریسک، پروفایل ایمپورت و مقایسهٔ خروجی اجرای عادی و تنبلمهاجرت کنترل‌شده و بررسی تفاوت رفتار
pylazyimportsفراهم کردن الگوهای ایمپورت تنبل در نسخه‌های قدیمی‌ترپروژه‌هایی که هنوز به نحو Python 3.15 دسترسی ندارند

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

۱۰. مسیر مهاجرت به Python 3.15 و نسخه‌های قدیمی‌تر

یکی از نگرانی‌های کتابخانه‌نویسان، پشتیبانی هم‌زمان از چند نسخه پایتون است. اگر در فایل یک کتابخانه مستقیماً بنویسیم lazy import json، مفسرهای قدیمی‌تر که این نحو را نمی‌شناسند، فایل را با خطای نحوی رد می‌کنند.

PEP 810 برای این مسئله سازوکاری به نام __lazy_modules__ معرفی می‌کند. این متغیر سراسری فهرستی از نام ماژول‌ها را مشخص می‌کند که ایمپورت‌های معمول آن‌ها باید در Python 3.15 به صورت بالقوه تنبل رفتار کنند.

🐍 PYTHON — __lazy_modules__
__lazy_modules__ = { "json", "pathlib", } import json import pathlib import os

در Python 3.15، ایمپورت‌های مشخص‌شده در __lazy_modules__ مانند ایمپورت‌های دارای واژه lazy رفتار می‌کنند. در نسخه‌های قدیمی‌تر، این متغیر نادیده گرفته می‌شود و دستورهای import به شکل عادی اجرا می‌شوند.

این سازوکار یک مسیر سازگاری است، نه جایگزینی دائمی برای نحو صریح. اگر پروژه فقط Python 3.15 و نسخه‌های جدیدتر را هدف قرار می‌دهد، نوشتن lazy خواناتر و روشن‌تر است.

همچنین، __lazy_modules__ نباید به عنوان یک دستور سراسری برای تنبل کردن همه ایمپورت‌ها در نظر گرفته شود. طراحی PEP 810 استفاده از نام ماژول‌های مشخص را پیش‌بینی می‌کند و پشتیبانی مستقیم از الگوی ["*"] را نپذیرفته است.

کنترل حالت سراسری

PEP 810 علاوه بر نحو صریح، سازوکارهای کنترلی دیگری هم دارد. حالت سراسری ایمپورت تنبل دو مقدار می‌پذیرد: normal (پیش‌فرض) که فقط ایمپورت‌های علامت‌گذاری‌شده با lazy را تنبل می‌کند، و all که همهٔ ایمپورت‌های سطح ماژول را، به‌جز ایمپورت‌های داخل try و ایمپورت ستاره‌ای، بالقوه تنبل می‌کند.

برای آزمایش سازگاری یک برنامه یا کتابخانه با حالت سراسری می‌توان از این گزینه استفاده کرد:

💻 BASH — آزمایش حالت سراسری
python -X lazy_imports=all -m myapp

این حالت برای بررسی سازگاری و بازتولید مشکلات مفید است. پژوهش «The Import Tax» نیز همین حالت سراسری را سنجید و دید که ۸ بسته از ۴۱۴ بستهٔ پرکاربرد با آن ناسازگارند؛ بنابراین all را ابزار آزمایش بدانید، نه راه‌حل عمومی برای ناسازگاری‌ها.

در مستندات PEP 810، متغیر محیطی PYTHON_LAZY_IMPORTS نیز برای تنظیم حالت سراسری تعریف شده است. تقدم تنظیمات به این ترتیب است: فراخوانی sys.set_lazy_imports()، سپس گزینه -X lazy_imports=... و در نهایت متغیر محیطی.

برای کنترل دقیق‌تر، به‌ویژه همراه با حالت all، یک فیلتر نیز وجود دارد:

🐍 PYTHON — فیلتر ایمپورت‌های تنبل
import sys def lazy_filter(importer, name, fromlist): # False means: keep this import eager if name in {"plugin_registry", "legacy_setup"}: return False return True sys.set_lazy_imports_filter(lazy_filter) sys.set_lazy_imports("all")

تابع فیلتر برای هر ایمپورت بالقوه تنبل فراخوانی می‌شود و پارامترهایش importer، name و fromlist هستند. اگر True برگرداند، ایمپورت تنبل می‌ماند؛ اگر False برگرداند، همان ایمپورت فوری انجام می‌شود. بازگرداندن False برای ماژول‌های حساس، ایمپورت آن‌ها را فوری نگه می‌دارد.

اگر برای عیب‌یابی می‌خواهید ایمپورت‌های lazy موقتاً مثل ایمپورت عادی رفتار کنند، می‌توانید فیلتری ثبت کنید که همیشه False برگرداند. در آزمایش با Python 3.15 مشاهده شد که در این حالت، ماژولی مثل json بلافاصله بارگذاری می‌شود.

⚠️
حالت سراسری را بدون بررسی فعال نکنید

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

۱۱. از کجا بفهمیم کدام ایمپورت را تنبل کنیم؟

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

پیش از هر تغییری، زمان شروع برنامه را اندازه بگیرید. برای بررسی زمان ایمپورت در پایتون، می‌توانید از گزینه -X importtime استفاده کنید:

💻 BASH — بررسی زمان ایمپورت
python -X importtime -m myapp

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

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

چارچوب تصمیم‌گیری

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

برای نمونه، یک کتابخانه رسم نمودار که فقط در یکی از زیردستورهای ابزار خط فرمان استفاده می‌شود، نامزد خوبی برای تنبل شدن است. در مقابل، ماژولی که هنگام بارگذاری افزونه‌ها را ثبت می‌کند یا وضعیت سراسری برنامه را تغییر می‌دهد، نیازمند بررسی دقیق است.

آیا ایمپورت تنبل برای هر نوع برنامه مناسب است؟

در ابزارهای خط فرمان، برنامه‌های تعاملی و سرویس‌های بدون سرور، تأخیر شروع معمولاً محسوس است؛ بنابراین، ایمپورت تنبل می‌تواند سود مستقیمی داشته باشد.

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

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

بهترین راهبرد، اندازه‌گیری بر اساس شاخص‌های واقعی پروژه است: زمان شروع، زمان آماده‌شدن سرویس، تأخیر صدک ۹۹ درخواست‌ها، حافظه مصرفی و تعداد ماژول‌های بارگذاری‌شده. بهینه‌سازی موفق، بهینه‌سازی‌ای است که یکی از این شاخص‌ها را بهتر کند، بدون آنکه رفتار برنامه را مختل سازد.

۱۲. هزینه‌ای که نباید به آینده منتقل کنیم

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

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

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

یک برنامه مهاجرت عملی

برای پروژه‌ای که می‌خواهد از CPython 3.15 استفاده کند، این ترتیب منطقی است:

  1. زمان شروع برنامه را در چند اجرای تکراری اندازه بگیرید و ماژول‌های پرهزینه را شناسایی کنید.
  2. ایمپورت‌هایی را انتخاب کنید که در بسیاری از مسیرهای اجرا بلااستفاده می‌مانند؛ به‌ویژه وابستگی‌های سنگین مربوط به زیردستورها یا قابلیت‌های اختیاری.
  3. پیش از تغییر، اثرهای جانبی ماژول‌ها را بررسی کنید. ثبت افزونه، تغییر وضعیت سراسری، متاکلاس‌ها و وابستگی به ترتیب ایمپورت از مواردی هستند که باید جدی گرفته شوند.
  4. ابتدا چند ایمپورت محدود را با lazy تغییر دهید و آزمون‌های واحد، یکپارچه و مسیرهای اصلی اجرا را انجام دهید.
  5. پس از بررسی درستی رفتار، زمان شروع و مصرف حافظه را دوباره اندازه بگیرید. اگر سود واقعی مشاهده شد، دامنه تغییر را گسترش دهید.
  6. اگر باید نسخه‌های قدیمی‌تر پایتون را نیز پشتیبانی کنید، __lazy_modules__ را به عنوان مسیر سازگاری بررسی کنید؛ یا تا زمانی که امکان استفاده از نحو رسمی فراهم شود، از راهکارهای مناسب نسخه هدف بهره ببرید.

این روش تدریجی شاید از فعال کردن یک حالت سراسری کندتر به نظر برسد، اما ریسک تغییرات پنهان را کاهش می‌دهد و نتیجه قابل‌اندازه‌گیری‌تری به دست می‌دهد.

جمع‌بندی: مسئله اصلی سرعت نیست، زمان انجام کار است

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

PEP 690 تلاش کرد با تغییر سراسری رفتار ایمپورت، بخش بزرگی از هزینه‌های بلااستفاده را حذف کند. تجربه اکوسیستم نشان داد که چنین رویکردی می‌تواند سریع باشد، اما سازگاری و قابلیت پیش‌بینی را دشوارتر می‌کند. PEP 810 راهی صریح‌تر برگزید: توسعه‌دهنده خودش تصمیم می‌گیرد کدام وابستگی باید دیرتر بارگذاری شود.

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

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

معیار نهایی، تعداد کمتر ایمپورت‌ها یا کوتاه‌تر شدن زمان راه‌اندازی به‌تنهایی نیست. معیار این است که برنامه فقط زمانی هزینه آماده‌سازی یک قابلیت را بپردازد که واقعاً به آن نیاز دارد، و آن لحظه را نیز به شکلی قابل‌پیش‌بینی، آزمون‌پذیر و متناسب با نیاز کاربر انتخاب کند.

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

✅
خلاصه

ایمپورت تنبل هزینه را حذف نمی‌کند؛ زمان پرداخت آن را جابه‌جا می‌کند. با lazy صریح، اندازه‌گیری و آزمون، می‌توانید این جابه‌جایی را کنترل کنید.