تعریف ماژول‌های توسعه‌ای

یک افزونه‌ی C برای سی‌پایتون یک کتابخانه‌ی مشترک است (برای مثال، یک پرونده‌ی .so روی لینوکس، DLL مربوط به .pyd روی ویندوز) که قابل بارگذاری در فرایند پایتون است (برای مثال، با تنظیمات کامپایلر سازگار کامپایل شده است) و یک تابع قلاب اکسپورت (یا یک تابع راه‌اندازی به سبک قدیم) را صادر می‌کند.

برای اینکه به‌صورت پیش‌فرض ایمپورت‌پذیر باشد (یعنی توسط importlib.machinery.ExtensionFileLoader)، کتابخانه‌ی اشتراکی باید در sys.path موجود باشد و باید بر اساس نام ماژول به‌علاوه‌ی یکی از پسوند‌های فهرست‌شده در importlib.machinery.EXTENSION_SUFFIXES نام‌گذاری شود.

نکته

ساخت، بسته‌بندی و توزیع ماژول‌های توسعه‌ای بهتر است با ابزارهای شخص ثالث انجام شود و خارج از محدوده‌ی این سند است. یکی از ابزارهای مناسب، Setuptools است که می‌توانید مستندات آن را در https://setuptools.pypa.io/en/latest/setuptools.html بیابید.

قلاب اکسپورت افزونه

اضافه شده در نسخه‌ی 3.15: پشتیبانی از قلاب اکسپورت PyModExport_<name> در نسخه‌ی ۳.۱۵ پایتون اضافه شد. روش قدیمی‌تر تعریف ماژول‌ها همچنان در دسترس است: اگر قصد دارید از نسخه‌های قدیمی‌تر پایتون پشتیبانی کنید، به بخش تابع PyInit یا نسخه‌های قبلی این مستندات مراجعه کنید.

قلاب اکسپورت باید یک تابع صادرشده با امضای زیر باشد:

PySlot *PyModExport_modulename(void)

برای ماژول‌هایی با نام‌های تماماً اسکی، export hook باید به صورت PyModExport_<name> نام‌گذاری شود که در آن <name> با نام ماژول جایگزین می‌شود.

برای نام‌های ماژول غیر اسکی، قلاب اکسپورت باید به‌جای آن PyModExportU_<name> نام‌گذاری شود (به U توجه کنید)، که در آن <name> با استفاده از کدگذاری punycode پایتون و با جایگزینی خط تیره با زیرخط رمزگذاری شده است. در پایتون:

def hook_name(name):
    try:
        suffix = b'_' + name.encode('ascii')
    except UnicodeEncodeError:
        suffix = b'U_' + name.encode('punycode').replace(b'-', b'_')
    return b'PyModExport' + suffix

قلاب اکسپورت آرایه‌ای از درایه‌های PySlot را برمی‌گرداند که با درایه‌ای با شناسه‌ی جایگاه 0 خاتمه می‌یابد. این جایگاه‌ها نحوه ایجاد و مقداردهی اولیه ماژول را توصیف می‌کنند.

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

قلاب اکسپورت ممکن است برای سیگنال‌دهی به خطا، مقدار NULL را به همراه یک استثنای تنظیم‌شده برگرداند.

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

PyMODEXPORT_FUNC
This macro is available when compiling for the ABI پایدار از نسخه‌ی 3.15.

یک قلاب اکسپورت ماژول توسعه‌ای را اعلان کنید. این کلان‌دستور:

  • نوع داده‌ی بازگشتی PySlot* را مشخص می‌کند،

  • هرگونه اعلان پیوند خاص مورد نیاز پلتفرم را می‌افزاید و

  • برای C++، تابع را به‌صورت extern "C" اعلان می‌کند.

برای مثال، ماژولی به نام spam به شکل زیر تعریف می‌شود:

PyABIInfo_VAR(abi_info);

static PySlot spam_slots[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "spam"),
    PySlot_FUNC(Py_mod_init, spam_init_function),
    ...
    PySlot_END
};

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
    return spam_slots;
}

قلاب اکسپورت معمولاً تنها آیتم غیرstatic است که در کد منبع C ماژول تعریف می‌شود.

این قلاب باید کوتاه نگه داشته شود. اگر کاری بیش از return کردن یک آرایه‌ی ثابت انجام دهد، چند نکته حائز اهمیت است:

  • اگر نیاز به استفاده از هرگونه API سی پایتون دارید، توصیه می‌شود در موارد رایج عدم تطابق ABI، به جای ایجاد فروپاشی، ابتدا PyABIInfo_Check() را برای ایجاد یک استثنا فراخوانی کنید.

  • کد موجود در قلاب اکسپورت نباید به GIL اتکا کند: ساخت‌های نخ‌آزاد پایتون تنها پس از بازگشت قلاب می‌توانند جایگاه Py_mod_gil (یا نبود آن) را بررسی کنند،

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

برای نمونه:

PyMODEXPORT_FUNC
PyModExport_modulename(void)
{
   if (PyABIInfo_Check(&abi_info, "modulename") < 0) {
      /* ABI mismatch. It's not safe to examine the raised exception. */
      return NULL;
   }

   /* use Python API (as little as possible); don't rely on GIL */

   return modulename_slots;
}

نکته

امکان اکسپورت کردن چندین ماژول از یک کتابخانه‌ی مشترک با تعریف چندین قلاب اکسپورت وجود دارد. با این حال، ایمپورت کردن آن‌ها نیازمند یک ایمپورت‌کننده‌ی سفارشی یا کپی‌ها/پیوندهای با نام مناسب از پرونده‌ی توسعه‌ای است، زیرا مکانیزم ایمپورت پایتون فقط تابع متناظر با نام پرونده را پیدا می‌کند. برای جزئیات بیشتر بخش Multiple modules in one library را در PEP 489 ببینید.

مقداردهی اولیه چندمرحله‌ای

فرایند ایجاد یک ماژول توسعه‌ای شامل چند مرحله است:

  • پایتون قلاب اکسپورت را پیدا کرده و فراخوانی می‌کند تا اطلاعاتی درباره‌ی نحوه‌ی ایجاد ماژول به دست آورد.

  • پیش از اجرای هرگونه کد قابل‌توجه، پایتون می‌تواند تعیین کند که ماژول از چه قابلیت‌هایی پشتیبانی می‌کند، و می‌تواند محیط را تنظیم کند یا از بارگذاری یک اکستنشن ناسازگار خودداری کند. جایگاه‌هایی مانند Py_mod_abi، Py_mod_gil و Py_mod_multiple_interpreters روی این مرحله تأثیر می‌گذارند.

  • به طور پیش‌فرض، خودِ پایتون شیء ماژول را ایجاد می‌کند -- یعنی کار معادل با فراخوانی __new__() را هنگام ایجاد یک شیء انجام می‌دهد. این مرحله را می‌توان با استفاده از جایگاه Py_mod_create بازنویسی (override) کرد.

  • پایتون ویژگی‌های اولیه ماژول مانند __package__ و __loader__ را تنظیم کرده و شیء ماژول را در sys.modules قرار می‌دهد.

  • پس از آن، شیء ماژول به روشی مختصِ اکستنشن مقداردهی اولیه می‌شود -- معادل __init__() هنگام ایجاد یک شیء، یا اجرای کد سطح بالا در یک ماژول به زبان پایتون. رفتار مورد نظر با استفاده از جایگاه Py_mod_exec مشخص می‌شود.

به این کار مقداردهی اولیه‌ی چندمرحله‌ای گفته می‌شود تا از مقداردهی اولیه‌ی قدیمی (اما همچنان پشتیبانی‌شده) single-phase initialization متمایز شود؛ در روش قدیمی، یک تابع مقداردهی اولیه، یک ماژول کاملاً ساخته‌شده را برمی‌گرداند.

تغییر یافته در نسخه‌ی 3.5: پشتیبانی از مقداردهی اولیه چندمرحله‌ای اضافه شد (PEP 489).

نمونه‌های متعدد ماژول

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

ممکن است نمونه‌های اضافی ماژول در زیرمفسرها یا پس از راه‌اندازی مجدد ران‌تایم پایتون (Py_Finalize() و Py_Initialize()) ایجاد شوند. در این موارد، اشتراک‌گذاری اشیاء پایتون بین نمونه‌های ماژول به احتمال زیاد باعث فروپاشی یا رفتار تعریف‌نشده می‌شود.

برای پرهیز از چنین مشکلاتی، هر نمونه از یک ماژول توسعه‌ای باید مجزا باشد: تغییرات در یک نمونه نباید به‌طور ضمنی بر نمونه‌های دیگر تأثیر بگذارد، و تمام وضعیت‌های متعلق به ماژول، از جمله ارجاع‌ها به اشیاء پایتون، باید مختص به یک نمونه ماژول خاص باشند. برای جزئیات بیشتر و راهنمای عملی، جداسازی ماژول‌های توسعه را ببینید.

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

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

تابع PyInit

منسوخ‌سازی نرم <Soft deprecated> از نسخه‌ی 3.15: این قابلیت ویژگی‌های جدیدی دریافت نخواهد کرد، اما هیچ برنامه‌ای برای حذف آن وجود ندارد.

به جای PyModExport_modulename()، یک ماژول توسعه‌ای می‌تواند یک تابع مقداردهی اولیه به سبک قدیمی‌تر با امضای زیر تعریف کند:

PyObject *PyInit_modulename(void)

نام آن باید به صورت PyInit_<name> باشد، به طوری که <name> با نام ماژول جایگزین شود. برای نام‌های ماژول غیر اسکی، به جای آن از PyInitU_<name> استفاده کنید، که در آن <name> درست مانند export hook (یعنی با استفاده از Punycode و زیرخط‌ها) کدگذاری شده است.

اگر یک ماژول هم PyInit_<name> و هم PyModExport_<name> را اکسپورت کند، تابع PyInit_<name> نادیده گرفته می‌شود.

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

PyMODINIT_FUNC

یک تابع مقداردهی اولیه‌ی ماژول توسعه‌ای را اعلان می‌کند. این ماکرو:

  • نوع بازگشتی PyObject* را تعیین می‌کند،

  • هرگونه اعلان پیوند خاص مورد نیاز پلتفرم را می‌افزاید و

  • برای C++، تابع را به‌صورت extern "C" اعلان می‌کند.

معمولاً، تابع مقداردهی اولیه (PyInit_modulename) یک نمونه‌ی PyModuleDef با مقدار m_slots غیر-NULL برمی‌گرداند. این امر به پایتون اجازه می‌دهد از مقداردهی اولیه‌ی چندمرحله‌ای استفاده کند.

پیش از بازگردانده شدن نمونه‌ی PyModuleDef، باید با استفاده از تابع زیر مقداردهی اولیه شود:

PyObject *PyModuleDef_Init(PyModuleDef *def)
قسمتی از ABI پایدار از نسخه‌ی 3.5.

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

def قالب‌ریزی‌شده به PyObject* را برمی‌گرداند، یا NULL را در صورت وقوع خطا.

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

توجه داشته باشید که پایتون فرض می‌کند ساختارهای PyModuleDef به‌صورت ایستا تخصیص یافته‌اند. این تابع ممکن است یک ارجاع جدید یا یک ارجاع امانتی برگرداند؛ این ارجاع نباید آزاد شود.

اضافه شده در نسخه‌ی 3.5.

برای مثال، ماژولی به نام spam به شکل زیر تعریف می‌شود:

static struct PyModuleDef spam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    ...
};

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}

مقداردهی اولیه تک‌مرحله‌ای قدیمی

منسوخ‌سازی نرم <Soft deprecated> از نسخه‌ی 3.15: راه‌اندازی تک‌فازی (single-phase initialization) سازوکاری قدیمی برای راه‌اندازی ماژول‌های توسعه‌ای است و معایب شناخته‌شده و نقص‌های طراحی دارد. به نویسندگان ماژول‌های توسعه‌ای توصیه می‌شود که به‌جای آن از راه‌اندازی چندفازی (multi-phase initialization) استفاده کنند.

با این حال، هیچ برنامه‌ای برای حذف پشتیبانی از آن وجود ندارد.

در مقداردهی اولیه‌ی تک‌مرحله‌ای، تابع مقداردهی اولیه‌ی سبک قدیمی (PyInit_modulename) باید یک شیء ماژول را ایجاد کرده، مقداردهی کند و برگرداند. این کار معمولاً با استفاده از PyModule_Create() و توابعی مانند PyModule_AddObjectRef() انجام می‌شود.

مقداردهی اولیه تک‌فازی در موارد زیر با پیش‌فرض تفاوت دارد:

  • ماژول‌های تک‌فاز (single-phase) «تک‌نمونه» هستند، یا به بیان دقیق‌تر، حاوی «تک‌نمونه» هستند.

    هنگامی که ماژول برای نخستین بار مقداردهی اولیه می‌شود، پایتون محتویات __dict__ ماژول را ذخیره می‌کند (یعنی، به‌طور معمول، توابع و نوع‌های ماژول).

    برای ایمپورت‌های بعدی، پایتون تابع مقداردهی اولیه را دوباره فراخوانی نمی‌کند. در عوض، شیء ماژول جدیدی با __dict__ جدید می‌سازد و محتویات ذخیره‌شده را در آن کپی می‌کند. برای مثال، با فرض یک ماژول تک‌فاز _testsinglephase [1] که تابعی به نام sum و کلاس استثنایی به نام error را تعریف می‌کند:

    >>> import sys
    >>> import _testsinglephase as one
    >>> del sys.modules['_testsinglephase']
    >>> import _testsinglephase as two
    >>> one is two
    False
    >>> one.__dict__ is two.__dict__
    False
    >>> one.sum is two.sum
    True
    >>> one.error is two.error
    True
    

    رفتار دقیق باید به‌عنوان جزئیات پیاده‌سازی سی‌پایتون در نظر گرفته شود.

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

    یک تابع تک‌فازه PyInit_modulename باید شیء ماژول «خود» را در اسرع وقت ایجاد کند، پیش از آن‌که بتوان هر شیء ماژول دیگری را ایجاد کرد.

  • نام‌های ماژول غیراسکی (PyInitU_modulename) پشتیبانی نمی‌شوند.

  • ماژول‌های تک‌فازی از توابع جستجوی ماژول مانند PyState_FindModule() پشتیبانی می‌کنند.

  • فیلد PyModuleDef.m_slots در ماژول باید NULL باشد.