جداسازی ماژول‌های توسعه

چه کسانی باید این را بخوانند

این راهنما برای نگهدارندگان افزونه‌های C-API نوشته شده است که می‌خواهند استفاده از آن افزونه را در برنامه‌هایی که خود پایتون به‌عنوان کتابخانه استفاده می‌شود، ایمن‌تر کنند.

پیش‌زمینه

یک مفسر زمینه‌ای است که کد پایتون در آن اجرا می‌شود. این شامل پیکربندی (برای مثال مسیر ایمپورت) و وضعیت ران‌تایم (برای مثال مجموعه ماژول‌های ایمپورت‌شده) است.

پایتون از اجرای چندین مفسر در یک فرایند پشتیبانی می‌کند. دو حالت برای در نظر گرفتن وجود دارد—ممکن است کاربران مفسرها را اجرا کنند:

هر دو حالت (و ترکیب‌های آن‌ها) بیشترین کاربرد را هنگام تعبیه پایتون در یک کتابخانه خواهند داشت. کتابخانه‌ها عموماً نباید درباره‌ی برنامه‌ای که از آن‌ها استفاده می‌کند فرض‌هایی داشته باشند؛ از جمله فرض وجود یک «مفسر اصلی پایتون» در سطح کل فرایند.

از نظر تاریخی، ماژول‌های توسعه پایتون این مورد استفاده را به‌خوبی مدیریت نمی‌کنند. بسیاری از ماژول‌های توسعه (و حتی برخی ماژول‌های stdlib) از وضعیت سراسری به‌ازای هر فرایند استفاده می‌کنند، زیرا استفاده از متغیرهای static در C بسیار آسان است. در نتیجه، داده‌هایی که باید مختص به یک مفسر باشند، در نهایت بین مفسرها مشترک می‌شوند. مگر اینکه توسعه‌دهنده ماژول توسعه محتاط باشد، ایجاد حالت‌های مرزی که منجر به فروپاشی می‌شوند، هنگامی که یک ماژول در بیش از یک مفسر در همان فرایند بارگذاری می‌شود، بسیار آسان است.

متأسفانه، وضعیت به‌ازای هر مفسر به‌آسانی قابل دستیابی نیست. نویسندگان افزونه‌ها معمولاً هنگام توسعه، چندین مفسر را در نظر نمی‌گیرند و آزمایش این رفتار در حال حاضر دشوار است.

ورود به وضعیت هر ماژول

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

وضعیت به‌ازای هر ماژول، راه ساده‌ای برای فکر کردن درباره طول عمر و مالکیت منابع فراهم می‌کند: ماژول توسعه‌ای در زمان ایجاد یک شیء ماژول، مقداردهی اولیه می‌شود و در زمان آزاد شدن، پاک‌سازی می‌شود. از این نظر، یک ماژول دقیقاً مانند هر PyObject* دیگری است؛ هیچ قلابیبرای «خاموش شدن مفسر» وجود ندارد که بخواهید درباره آن فکر کنید—یا آن را فراموش کنید.

توجه داشته باشید که برای انواع مختلف «سراسری‌ها» موارد استفاده وجود دارد: وضعیت به‌ازای هر فرایند، به‌ازای هر مفسر، به‌ازای هر نخ یا به‌ازای هر وظیفه . با وضعیت به‌ازای هر ماژول به‌عنوان پیش‌فرض، این موارد همچنان امکان‌پذیر هستند، اما باید با آن‌ها به‌عنوان موارد استثنایی رفتار کنید: اگر به آن‌ها نیاز دارید، باید مراقبت و آزمون بیشتری برای آن‌ها در نظر بگیرید. (توجه داشته باشید که این راهنما آن‌ها را پوشش نمی‌دهد.)

اشیای ماژول جداشده

نکته کلیدی که باید هنگام توسعه‌ی یک ماژول توسعه در نظر داشته باشید این است که می‌توان چندین شیء ماژول را از یک کتابخانه مشترک واحد ایجاد کرد. برای مثال:

>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii  # create a new module object
>>> old_binascii == binascii
False

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

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

حالت‌های مرزی غافلگیرکننده

توجه داشته باشید که ماژول‌های ایزوله برخی حالت‌های مرزی غافلگیرکننده ایجاد می‌کنند. به‌ویژه، هر شیء ماژول به‌طور معمول کلاس‌ها و استثناهای خود را با سایر ماژول‌های مشابه به اشتراک نمی‌گذارد. در ادامه‌ی مثال بالا، توجه داشته باشید که old_binascii.Error و binascii.Error اشیاء جداگانه‌ای هستند. در کد زیر، استثنا گرفته نمی‌شود:

>>> old_binascii.Error == binascii.Error
False
>>> try:
...     old_binascii.unhexlify(b'qwertyuiop')
... except binascii.Error:
...     print('boo')
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
binascii.Error: Non-hexadecimal digit found

این مورد انتظار است. توجه داشته باشید که ماژول‌های پایتون خالص نیز به همین شکل رفتار می‌کنند: این بخشی از نحوه‌ی کار پایتون است.

هدف این است که ماژول‌های توسعه در سطح C امن باشند، نه اینکه هک‌ها رفتاری شهودی داشته باشند. تغییر sys.modules به‌صورت «دستی» یک هک محسوب می‌شود.

ایمن‌سازی ماژول‌ها با چند مفسر

مدیریت وضعیت سراسری

گاهی اوقات، وضعیت مرتبط با یک ماژول پایتون مختص به آن ماژول نیست، بلکه به کل فرایند (یا چیزی «سراسری‌تر» از یک ماژول) مربوط می‌شود. برای مثال:

  • ماژول readline پایانه را مدیریت می‌کند.

  • ماژولی که روی یک برد مدار اجرا می‌شود، می‌خواهد همان LED روی برد را کنترل کند.

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

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

مدیریت وضعیت هر ماژول

برای استفاده از وضعیت به‌ازای هر ماژول، از multi-phase extension module initialization استفاده کنید. این نشان می‌دهد که ماژول شما به‌درستی از چندین مفسر پشتیبانی می‌کند.

PyModuleDef.m_size را روی یک عدد مثبت تنظیم کنید تا آن تعداد بایت حافظه‌ی محلی ماژول درخواست شود. معمولاً، این مقدار روی اندازه‌ی یک struct اختصاصی برای ماژول تنظیم می‌شود که می‌تواند تمام وضعیت ماژول در سطح C را ذخیره کند. به‌طور خاص، این جایی است که باید اشاره‌گرهایی به کلاس‌ها (شامل استثناها، اما به‌جز انواع ایستا) و تنظیماتی (مانند field_size_limit در csv) را قرار دهید که کد C برای کار کردن به آن‌ها نیاز دارد.

توجه

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

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

اگر وضعیت ماژول شامل اشاره‌گرهای PyObject باشد، شیء ماژول باید ارجاع‌ها به آن اشیاء را حفظ کند و قلاب‌های سطح ماژول m_traverse، m_clear و m_free را پیاده‌سازی کند. این‌ها مانند tp_traverse، tp_clear و tp_free در یک کلاس کار می‌کنند. افزودن آن‌ها نیازمند کمی کار است و کد را طولانی‌تر می‌کند؛ این بهای ماژول‌هایی است که می‌توانند به‌تمیزی باربرداری شوند.

نمونه‌ای از یک ماژول با وضعیت به‌ازای هر ماژول، در حال حاضر به‌عنوان xxlimited در دسترس است؛ مقداردهی اولیه ماژول نمونه در پایان پرونده نشان داده شده است.

انصراف: محدود کردن به یک شیء ماژول در هر فرایند

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

// A process-wide flag
static int loaded = 0;

// Mutex to provide thread safety (only needed for free-threaded Python)
static PyMutex modinit_mutex = {0};

static int
exec_module(PyObject* module)
{
    PyMutex_Lock(&modinit_mutex);
    if (loaded) {
        PyMutex_Unlock(&modinit_mutex);
        PyErr_SetString(PyExc_ImportError,
                        "cannot load module more than once per process");
        return -1;
    }
    loaded = 1;
    PyMutex_Unlock(&modinit_mutex);
    // ... rest of initialization
}

اگر تابع PyModuleDef.m_clear ماژول شما قادر است برای مقداردهی اولیه مجدد در آینده آماده شود، باید پرچم loaded را پاک کند. در این صورت، ماژول شما از وجود همزمان چندین نمونه پشتیبانی نخواهد کرد، اما برای مثال، از بارگذاری شدن پس از خاموش شدن ران‌تایم پایتون (Py_FinalizeEx()) و مقداردهی اولیه مجدد (Py_Initialize()) پشتیبانی خواهد کرد.

دسترسی به وضعیت ماژول از توابع

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

static PyObject *
func(PyObject *module, PyObject *args)
{
    my_struct *state = (my_struct*)PyModule_GetState(module);
    if (state == NULL) {
        return NULL;
    }
    // ... rest of logic
}

توجه

PyModule_GetState ممکن است در صورتی که وضعیت ماژول وجود نداشته باشد، یعنی PyModuleDef.m_size صفر بوده است، NULL را بدون اینکه استثنایی تنظیم کند برگرداند. در ماژول خودتان، شما بر m_size کنترل دارید، بنابراین جلوگیری از این موضوع آسان است.

انواع Heap

به‌طور سنتی، نوع‌های تعریف‌شده در کد C ایستا هستند؛ یعنی ساختارهای static PyTypeObject که مستقیماً در کد تعریف شده‌اند و با استفاده از PyType_Ready() مقداردهی اولیه می‌شوند.

چنین نوع‌هایی لزوماً در سراسر فرایند به‌اشتراک گذاشته می‌شوند. به‌اشتراک گذاشتن آن‌ها بین اشیای ماژول نیازمند توجه به هر وضعیتی است که مالک آن هستند یا به آن دسترسی دارند. برای محدود کردن مسائل احتمالی، نوع‌های ایستا در سطح پایتون تغییرناپذیر هستند: برای مثال، نمی‌توانید str.myattribute = 123 را انتساب دهید.

اشتراک‌گذاری اشیاء واقعاً تغییرناپذیر میان مفسرها مشکلی ندارد، تا زمانی که آن‌ها دسترسی به اشیاء تغییرپذیر فراهم نکنند. با این حال، در CPython، هر شیء پایتون یک جزئیات پیاده‌سازی تغییرپذیر دارد: شمار ارجاع. تغییرات شمار ارجاع توسط GIL محافظت می‌شود. بنابراین، کدی که هر شیء پایتون را میان مفسرها به اشتراک می‌گذارد، به‌طور ضمنی به GIL فعلی CPython در سطح فرآیند وابسته است.

از آن‌جا که انواع ایستا تغییرناپذیر و سراسری در سطح فرایند هستند، امکان دسترسی به وضعیت ماژول «خود» را ندارند. اگر هر متدی از چنین نوعی نیازمند دسترسی به وضعیت ماژول باشد، آن نوع باید به یک نوع تخصیص‌یافته در heap (heap-allocated type)، یا به‌اختصار نوع heap (heap type) تبدیل شود. این موارد بیشتر با کلاس‌های ایجادشده با دستور class پایتون مطابقت دارند.

برای ماژول‌های جدید، استفاده از انواع heap (heap types) به‌صورت پیش‌فرض، قاعده سرانگشتی خوبی است.

تغییر نوع‌های ایستا به نوع‌های Heap

می‌توان انواع ایستا را به انواع heap (heap types) تبدیل کرد، اما توجه داشته باشید که API نوع heap برای تبدیل «بدون اتلاف» انواع ایستا طراحی نشده است—یعنی ایجاد نوعی که دقیقاً مانند یک نوع ایستای مشخص رفتار کند. بنابراین، هنگام بازنویسی تعریف کلاس در یک API جدید، احتمال دارد به‌طور ناخواسته چند جزئیات را تغییر دهید (مثلاً قابلیت pickle یا جایگاه‌های ارثی). همیشه جزئیاتی را که برای شما مهم هستند آزمایش کنید.

به‌ویژه به دو نکته‌ی زیر توجه داشته باشید (اما توجه داشته باشید که این یک فهرست جامع نیست):

  • برخلاف انواع ایستا، اشیای نوع heap به‌طور پیش‌فرض تغییرپذیر هستند. برای جلوگیری از تغییرپذیری، از پرچم Py_TPFLAGS_IMMUTABLETYPE استفاده کنید.

  • انواع Heap به‌طور پیش‌فرض tp_new را به ارث می‌برند، بنابراین ممکن است امکان نمونه‌سازی آن‌ها از کد پایتون فراهم شود. می‌توانید با پرچم Py_TPFLAGS_DISALLOW_INSTANTIATION از این موضوع جلوگیری کنید.

تعریف انواع هیپ

انواع Heap را می‌توان با پر کردن یک ساختار PyType_Spec، یک توصیف یا «نقشه» از یک کلاس، و فراخوانی PyType_FromModuleAndSpec() برای ساخت یک شیء کلاس جدید ایجاد کرد.

توجه

توابع دیگر، مانند PyType_FromSpec()، نیز می‌توانند نوع‌های heap (heap types) را ایجاد کنند، اما PyType_FromModuleAndSpec() ماژول را به کلاس مرتبط می‌کند و امکان دسترسی به وضعیت ماژول را از طریق متدها فراهم می‌کند.

این کلاس عموماً باید در هر دو وضعیت ماژول (برای دسترسی ایمن از C) و __dict__ ماژول (برای دسترسی از کد پایتون) ذخیره شود.

پروتکل زباله‌روبی

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

برای جلوگیری از نشت حافظه، نمونه‌های انواع heap (heap types) باید پروتکل زباله‌روبی را پیاده‌سازی کنند. یعنی، انواع heap باید:

  • پرچم Py_TPFLAGS_HAVE_GC را داشته باشد.

  • یک تابع پیمایش با استفاده از Py_tp_traverse تعریف کنید، که از نوع بازدید می‌کند (برای مثال با استفاده از Py_VISIT(Py_TYPE(self))).

برای ملاحظات بیشتر، لطفاً به مستندات Py_TPFLAGS_HAVE_GC و tp_traverse مراجعه کنید.

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

tp_traverse در پایتون 3.8 و پایین‌تر

الزام به بازدید از نوع از tp_traverse در Python 3.9 افزوده شد. اگر از Python 3.8 و پایین‌تر پشتیبانی می‌کنید، تابع پیمایش نباید از نوع بازدید کند، بنابراین باید پیچیده‌تر باشد:

static int my_traverse(PyObject *self, visitproc visit, void *arg)
{
    if (Py_Version >= 0x03090000) {
        Py_VISIT(Py_TYPE(self));
    }
    return 0;
}

متأسفانه، Py_Version تنها در پایتون 3.11 اضافه شده است. به‌عنوان جایگزین، از این استفاده کنید:

واگذاری tp_traverse

اگر تابع پیمایش شما به tp_traverse کلاس پایه خود (یا نوع دیگری) واگذار می‌کند، اطمینان حاصل کنید که Py_TYPE(self) تنها یک بار بازدید می‌شود. توجه داشته باشید که فقط انواع heap (heap type) انتظار می‌رود که نوع را در tp_traverse بازدید کنند.

برای مثال، اگر تابع پیمایش شما شامل موارد زیر باشد:

base->tp_traverse(self, visit, arg)

... و base ممکن است یک نوع ایستا باشد، در این صورت باید شامل موارد زیر نیز باشد:

if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
    // a heap type's tp_traverse already visited Py_TYPE(self)
} else {
    if (Py_Version >= 0x03090000) {
        Py_VISIT(Py_TYPE(self));
    }
}

لازم نیست شمار ارجاعِ نوع در tp_new و tp_clear مدیریت شود.

تعریف tp_dealloc

اگر نوع شما تابع tp_dealloc سفارشی دارد، باید:

  • PyObject_GC_UnTrack() را پیش از آنکه فیلدها بی‌اعتبار شوند فراخوانی کنید، و

  • شمار ارجاع‌های نوع را کاهش دهید.

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

static void my_dealloc(PyObject *self)
{
    PyObject_GC_UnTrack(self);
    ...
    PyTypeObject *type = Py_TYPE(self);
    type->tp_free(self);
    Py_DECREF(type);
}

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

عدم بازنویسی tp_free

جایگاه tp_free در یک نوع heap باید روی PyObject_GC_Del() تنظیم شود. این پیش‌فرض است؛ آن را بازنویسی نکنید.

پرهیز از PyObject_New

اشیای پیگیری‌شده توسط زباله‌روبی باید با استفاده از توابع آگاه از زباله‌روبی تخصیص داده شوند.

اگر از PyObject_New() یا PyObject_NewVar() استفاده می‌کنید:

  • در صورت امکان، جایگاه tp_alloc نوع را دریافت کرده و فراخوانی کنید. یعنی TYPE *o = PyObject_New(TYPE, typeobj) را با این جایگزین کنید:

    TYPE *o = typeobj->tp_alloc(typeobj, 0);
    

    o = PyObject_NewVar(TYPE, typeobj, size) را با همان جایگزین کنید، اما به‌جای 0 از size استفاده کنید.

  • اگر مورد بالا ممکن نیست (برای مثال داخل یک tp_alloc سفارشی)، PyObject_GC_New() یا PyObject_GC_NewVar() را فراخوانی کنید:

    TYPE *o = PyObject_GC_New(TYPE, typeobj);
    
    TYPE *o = PyObject_GC_NewVar(TYPE, typeobj, size);
    

دسترسی به وضعیت ماژول از کلاس‌ها

اگر یک شیء نوع تعریف‌شده با PyType_FromModuleAndSpec() دارید، می‌توانید PyType_GetModule() را فراخوانی کنید تا ماژول مرتبط را دریافت کنید، و سپس PyModule_GetState() را فراخوانی کنید تا وضعیت ماژول را دریافت کنید.

برای پرهیز از برخی کدهای پیش‌ساخته ملال‌آور مدیریت خطا، می‌توانید این دو مرحله را با PyType_GetModuleState() ترکیب کنید، که نتیجه به این صورت است:

my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state == NULL) {
    return NULL;
}

دسترسی به وضعیت ماژول از متدهای معمولی

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

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

کلاس تعریف‌کننده را با Py_TYPE(self) اشتباه نگیرید. اگر متد بر روی یک زیرکلاس از نوع شما فراخوانی شود، Py_TYPE(self) به آن زیرکلاس ارجاع خواهد داشت، که ممکن است در ماژولی متفاوت از ماژول شما تعریف شده باشد.

توجه

کد پایتون زیر می‌تواند این مفهوم را نشان دهد. Base.get_defining_class حتی اگر type(self) == Sub باشد، Base را برمی‌گرداند:

class Base:
    def get_type_of_self(self):
        return type(self)

    def get_defining_class(self):
        return __class__

class Sub(Base):
    pass

برای اینکه یک متد «کلاس تعریف‌کننده» خود را دریافت کند، باید از METH_METHOD | METH_FASTCALL | METH_KEYWORDS قرارداد فراخوانی و امضای متناظر PyCMethod استفاده کند:

PyObject *PyCMethod(
    PyObject *self,               // object the method was called on
    PyTypeObject *defining_class, // defining class
    PyObject *const *args,        // C array of arguments
    Py_ssize_t nargs,             // length of "args"
    PyObject *kwnames)            // NULL, or dict of keyword arguments

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

برای مثال:

static PyObject *
example_method(PyObject *self,
        PyTypeObject *defining_class,
        PyObject *const *args,
        Py_ssize_t nargs,
        PyObject *kwnames)
{
    my_struct *state = (my_struct*)PyType_GetModuleState(defining_class);
    if (state == NULL) {
        return NULL;
    }
    ... // rest of logic
}

PyDoc_STRVAR(example_method_doc, "...");

static PyMethodDef my_methods[] = {
    {"example_method",
      (PyCFunction)(void(*)(void))example_method,
      METH_METHOD|METH_FASTCALL|METH_KEYWORDS,
      example_method_doc}
    {NULL},
}

دسترسی به وضعیت ماژول از متدهای جایگاه، getters و setters

توجه

این مورد در پایتون 3.11 جدید است.

متدهای جایگاه (slot methods)—معادل‌های سریع C برای متدهای ویژه، مانند nb_add برای __add__ یا tp_new برای مقداردهی اولیه—دارای API بسیار ساده‌ای هستند که برخلاف PyCMethod اجازه‌ی ارسال کلاس تعریف‌کننده را نمی‌دهد. همین موضوع برای setters و getters که با PyGetSetDef تعریف‌شده‌اند نیز صدق می‌کند.

برای دسترسی به وضعیت ماژول در این موارد، از تابع PyType_GetModuleByDef() استفاده کنید و تعریف ماژول را به آن ارسال کنید. هنگامی که ماژول را دریافت کردید، برای دریافت وضعیت، PyModule_GetState() را فراخوانی کنید:

PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &module_def);
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
    return NULL;
}

PyType_GetModuleByDef() با جست‌وجو در method resolution order (یعنی همه‌ی ابرکلاس‌ها) برای نخستین ابرکلاسی که ماژول متناظر دارد، کار می‌کند.

توجه

در موارد بسیار خاص (زنجیره‌های وراثتی که چندین ماژول ایجادشده از یک تعریف را در بر می‌گیرند)، ممکن است PyType_GetModuleByDef() ماژولِ کلاس تعریف‌کننده واقعی را برنگرداند. با این حال، همیشه ماژولی با همان تعریف را برمی‌گرداند و چیدمان حافظه‌ای سازگار در C را تضمین می‌کند.

طول عمر وضعیت ماژول

وقتی یک شیء ماژول زباله‌روبی می‌شود، وضعیت ماژول آن آزاد می‌شود. برای هر اشاره‌گر به (بخشی از) وضعیت ماژول، باید یک ارجاع به شیء ماژول نگه دارید.

معمولاً این مسئله مشکلی نیست، زیرا انواع ساخته‌شده با PyType_FromModuleAndSpec() و نمونه‌هایشان، یک ارجاع به ماژول نگه می‌دارند. با این حال، وقتی از جاهای دیگر، مانند کال‌بک‌هایی برای کتابخانه‌های خارجی، به وضعیت ماژول ارجاع می‌دهید، باید در شمارش ارجاع دقت کنید.

مسائل باز

چندین مسئله پیرامون وضعیت به‌ازای هر ماژول و انواع heap (heap types) هنوز باز هستند.

بهتر است بحث‌های مربوط به بهبود وضعیت در انجمن discuss ذیل برچسب c-api انجام شوند.

محدوده به‌ازای هر کلاس

در حال حاضر (در پایتون 3.11) امکان الصاق وضعیت به انواع منفرد بدون تکیه بر جزئیات پیاده‌سازی CPython وجود ندارد (که ممکن است در آینده تغییر کنند—شاید، از سر طعنه، تا راه‌حل مناسبی برای محدوده به‌ازای هر کلاس فراهم شود).

تبدیل بدون اتلاف به نوع‌های Heap

API نوع heap برای تبدیل «بدون اتلاف» از انواع ایستا طراحی نشده است؛ یعنی ایجاد نوعی که دقیقاً مانند یک نوع ایستای معین کار کند.